@vfarcic/dot-ai 0.4.9 → 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.
- package/.claude/commands/context-load.md +11 -0
- package/.claude/commands/context-save.md +16 -0
- package/.claude/commands/prd-done.md +115 -0
- package/.claude/commands/prd-get.md +25 -0
- package/.claude/commands/prd-start.md +87 -0
- package/.claude/commands/task-done.md +77 -0
- package/.claude/commands/tests-reminder.md +32 -0
- package/.claude/settings.local.json +20 -0
- package/.eslintrc.json +25 -0
- package/.github/workflows/ci.yml +170 -0
- package/.prettierrc.json +10 -0
- package/.teller.yml +8 -0
- package/CLAUDE.md +162 -0
- package/assets/images/logo.png +0 -0
- package/bin/dot-ai.ts +47 -0
- package/destroy.sh +45 -0
- package/devbox.json +13 -0
- package/devbox.lock +225 -0
- package/docs/API.md +449 -0
- package/docs/CONTEXT.md +49 -0
- package/docs/DEVELOPMENT.md +203 -0
- package/docs/NEXT_STEPS.md +97 -0
- package/docs/STAGE_BASED_API.md +97 -0
- package/docs/cli-guide.md +798 -0
- package/docs/design.md +750 -0
- package/docs/discovery-engine.md +515 -0
- package/docs/error-handling.md +429 -0
- package/docs/function-registration.md +157 -0
- package/docs/mcp-guide.md +416 -0
- package/package.json +2 -123
- package/renovate.json +51 -0
- package/setup.sh +111 -0
- package/{dist/cli.js → src/cli.ts} +26 -19
- package/src/core/claude.ts +280 -0
- package/src/core/deploy-operation.ts +127 -0
- package/src/core/discovery.ts +900 -0
- package/src/core/error-handling.ts +562 -0
- package/src/core/index.ts +143 -0
- package/src/core/kubernetes-utils.ts +218 -0
- package/src/core/memory.ts +148 -0
- package/src/core/schema.ts +830 -0
- package/src/core/session-utils.ts +97 -0
- package/src/core/workflow.ts +234 -0
- package/src/index.ts +18 -0
- package/src/interfaces/cli.ts +872 -0
- package/src/interfaces/mcp.ts +183 -0
- package/src/mcp/server.ts +131 -0
- package/src/tools/answer-question.ts +807 -0
- package/src/tools/choose-solution.ts +169 -0
- package/src/tools/deploy-manifests.ts +94 -0
- package/src/tools/generate-manifests.ts +502 -0
- package/src/tools/index.ts +41 -0
- package/src/tools/recommend.ts +370 -0
- package/tests/__mocks__/@kubernetes/client-node.ts +106 -0
- package/tests/build-system.test.ts +345 -0
- package/tests/configuration.test.ts +226 -0
- package/tests/core/deploy-operation.test.ts +38 -0
- package/tests/core/discovery.test.ts +1648 -0
- package/tests/core/error-handling.test.ts +632 -0
- package/tests/core/schema.test.ts +1658 -0
- package/tests/core/session-utils.test.ts +245 -0
- package/tests/core.test.ts +439 -0
- package/tests/fixtures/configmap-no-labels.yaml +8 -0
- package/tests/fixtures/crossplane-app-configuration.yaml +6 -0
- package/tests/fixtures/crossplane-providers.yaml +45 -0
- package/tests/fixtures/crossplane-rbac.yaml +48 -0
- package/tests/fixtures/invalid-configmap.yaml +8 -0
- package/tests/fixtures/invalid-deployment.yaml +17 -0
- package/tests/fixtures/test-deployment.yaml +28 -0
- package/tests/fixtures/valid-configmap.yaml +15 -0
- package/tests/infrastructure.test.ts +426 -0
- package/tests/interfaces/cli.test.ts +1036 -0
- package/tests/interfaces/mcp.test.ts +139 -0
- package/tests/kubernetes-utils.test.ts +200 -0
- package/tests/mcp/server.test.ts +126 -0
- package/tests/setup.ts +31 -0
- package/tests/tools/answer-question.test.ts +367 -0
- package/tests/tools/choose-solution.test.ts +481 -0
- package/tests/tools/deploy-manifests.test.ts +185 -0
- package/tests/tools/generate-manifests.test.ts +441 -0
- package/tests/tools/index.test.ts +111 -0
- package/tests/tools/recommend.test.ts +180 -0
- package/tsconfig.json +34 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/core/claude.d.ts +0 -42
- package/dist/core/claude.d.ts.map +0 -1
- package/dist/core/claude.js +0 -229
- package/dist/core/deploy-operation.d.ts +0 -38
- package/dist/core/deploy-operation.d.ts.map +0 -1
- package/dist/core/deploy-operation.js +0 -101
- package/dist/core/discovery.d.ts +0 -162
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -758
- package/dist/core/error-handling.d.ts +0 -167
- package/dist/core/error-handling.d.ts.map +0 -1
- package/dist/core/error-handling.js +0 -399
- package/dist/core/index.d.ts +0 -42
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -123
- package/dist/core/kubernetes-utils.d.ts +0 -38
- package/dist/core/kubernetes-utils.d.ts.map +0 -1
- package/dist/core/kubernetes-utils.js +0 -177
- package/dist/core/memory.d.ts +0 -45
- package/dist/core/memory.d.ts.map +0 -1
- package/dist/core/memory.js +0 -113
- package/dist/core/schema.d.ts +0 -187
- package/dist/core/schema.d.ts.map +0 -1
- package/dist/core/schema.js +0 -655
- package/dist/core/session-utils.d.ts +0 -29
- package/dist/core/session-utils.d.ts.map +0 -1
- package/dist/core/session-utils.js +0 -121
- package/dist/core/workflow.d.ts +0 -70
- package/dist/core/workflow.d.ts.map +0 -1
- package/dist/core/workflow.js +0 -161
- package/dist/index.d.ts +0 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -32
- package/dist/interfaces/cli.d.ts +0 -74
- package/dist/interfaces/cli.d.ts.map +0 -1
- package/dist/interfaces/cli.js +0 -769
- package/dist/interfaces/mcp.d.ts +0 -30
- package/dist/interfaces/mcp.d.ts.map +0 -1
- package/dist/interfaces/mcp.js +0 -105
- package/dist/mcp/server.d.ts +0 -9
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -151
- package/dist/tools/answer-question.d.ts +0 -27
- package/dist/tools/answer-question.d.ts.map +0 -1
- package/dist/tools/answer-question.js +0 -696
- package/dist/tools/choose-solution.d.ts +0 -23
- package/dist/tools/choose-solution.d.ts.map +0 -1
- package/dist/tools/choose-solution.js +0 -171
- package/dist/tools/deploy-manifests.d.ts +0 -25
- package/dist/tools/deploy-manifests.d.ts.map +0 -1
- package/dist/tools/deploy-manifests.js +0 -74
- package/dist/tools/generate-manifests.d.ts +0 -23
- package/dist/tools/generate-manifests.d.ts.map +0 -1
- package/dist/tools/generate-manifests.js +0 -424
- package/dist/tools/index.d.ts +0 -11
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -34
- package/dist/tools/recommend.d.ts +0 -23
- package/dist/tools/recommend.d.ts.map +0 -1
- package/dist/tools/recommend.js +0 -332
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommend Tool - AI-powered Kubernetes resource recommendations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { ErrorHandler, ErrorCategory, ErrorSeverity } from '../core/error-handling';
|
|
7
|
+
import { ResourceRecommender, AIRankingConfig } from '../core/schema';
|
|
8
|
+
import { ClaudeIntegration } from '../core/claude';
|
|
9
|
+
import { DotAI } from '../core/index';
|
|
10
|
+
import { Logger } from '../core/error-handling';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
|
+
import { getAndValidateSessionDirectory } from '../core/session-utils';
|
|
15
|
+
|
|
16
|
+
// Tool metadata for direct MCP registration
|
|
17
|
+
export const RECOMMEND_TOOL_NAME = 'recommend';
|
|
18
|
+
export const RECOMMEND_TOOL_DESCRIPTION = 'Deploy, create, run, or setup applications on Kubernetes with AI-powered recommendations. Ask the user to describe their application first, then use their response here.';
|
|
19
|
+
|
|
20
|
+
// Zod schema for MCP registration
|
|
21
|
+
export const RECOMMEND_TOOL_INPUT_SCHEMA = {
|
|
22
|
+
intent: z.string().min(1).max(1000).describe('What the user wants to deploy, create, run, or setup on Kubernetes (based on their description). Ask the user to describe their application first, then use their response here. Examples: "deploy a web application", "create a database cluster", "run my Node.js API", "setup a Redis cache", "launch a microservice", "build a CI/CD pipeline", "deploy a WordPress site", "create a monitoring stack", "run a Python Flask app", "setup MongoDB", "deploy a React frontend", "create a load balancer"')
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate intent meaningfulness using AI
|
|
28
|
+
*/
|
|
29
|
+
async function validateIntentWithAI(intent: string, claudeIntegration: any): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
// Load prompt template
|
|
32
|
+
const promptPath = path.join(process.cwd(), 'prompts', 'intent-validation.md');
|
|
33
|
+
const template = fs.readFileSync(promptPath, 'utf8');
|
|
34
|
+
|
|
35
|
+
// Replace template variables
|
|
36
|
+
const validationPrompt = template.replace('{intent}', intent);
|
|
37
|
+
|
|
38
|
+
// Send to Claude for validation
|
|
39
|
+
const response = await claudeIntegration.sendMessage(validationPrompt);
|
|
40
|
+
|
|
41
|
+
// Parse JSON response with robust error handling
|
|
42
|
+
let jsonContent = response.content;
|
|
43
|
+
|
|
44
|
+
// Try to find JSON object wrapped in code blocks
|
|
45
|
+
const codeBlockMatch = response.content.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
46
|
+
if (codeBlockMatch) {
|
|
47
|
+
jsonContent = codeBlockMatch[1];
|
|
48
|
+
} else {
|
|
49
|
+
// Try to find JSON object that starts with { and find the matching closing }
|
|
50
|
+
const startIndex = response.content.indexOf('{');
|
|
51
|
+
if (startIndex !== -1) {
|
|
52
|
+
let braceCount = 0;
|
|
53
|
+
let endIndex = startIndex;
|
|
54
|
+
|
|
55
|
+
for (let i = startIndex; i < response.content.length; i++) {
|
|
56
|
+
if (response.content[i] === '{') braceCount++;
|
|
57
|
+
if (response.content[i] === '}') braceCount--;
|
|
58
|
+
if (braceCount === 0) {
|
|
59
|
+
endIndex = i;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (braceCount === 0) {
|
|
65
|
+
jsonContent = response.content.substring(startIndex, endIndex + 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const validation = JSON.parse(jsonContent.trim());
|
|
71
|
+
|
|
72
|
+
// Validate response structure
|
|
73
|
+
if (typeof validation.isSpecific !== 'boolean' ||
|
|
74
|
+
typeof validation.reason !== 'string' ||
|
|
75
|
+
!Array.isArray(validation.suggestions)) {
|
|
76
|
+
throw new Error('AI response has invalid structure');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If intent is not specific enough, throw error with suggestions
|
|
80
|
+
if (!validation.isSpecific) {
|
|
81
|
+
const suggestions = validation.suggestions.length
|
|
82
|
+
? validation.suggestions.map((s: string) => `• ${s}`).join('\n')
|
|
83
|
+
: '• Include specific technology (Node.js, PostgreSQL, React, etc.)\n• Describe the purpose or function\n• Add context about requirements';
|
|
84
|
+
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Intent needs more specificity: ${validation.reason}\n\n` +
|
|
87
|
+
`Suggestions to improve your intent:\n${suggestions}\n\n` +
|
|
88
|
+
`Original intent: "${intent}"`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// If it's our validation error, re-throw it
|
|
94
|
+
if (error instanceof Error && error.message.includes('Intent needs more specificity')) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// For other errors (AI service issues, JSON parsing, etc.),
|
|
99
|
+
// continue without blocking the user - log the issue but don't fail
|
|
100
|
+
console.warn('Intent validation failed, continuing with original intent:', error);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate unique solution ID with timestamp and random component
|
|
108
|
+
*/
|
|
109
|
+
function generateSolutionId(): string {
|
|
110
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '').split('T');
|
|
111
|
+
const dateTime = timestamp[0] + 'T' + timestamp[1].substring(0, 6);
|
|
112
|
+
const randomHex = crypto.randomBytes(6).toString('hex');
|
|
113
|
+
return `sol_${dateTime}_${randomHex}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Write solution data to file atomically (temp file + rename)
|
|
118
|
+
*/
|
|
119
|
+
function writeSolutionFile(sessionDir: string, solutionId: string, solutionData: any): void {
|
|
120
|
+
const fileName = `${solutionId}.json`;
|
|
121
|
+
const filePath = path.join(sessionDir, fileName);
|
|
122
|
+
const tempPath = filePath + '.tmp';
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Write to temporary file first
|
|
126
|
+
fs.writeFileSync(tempPath, JSON.stringify(solutionData, null, 2));
|
|
127
|
+
|
|
128
|
+
// Atomically rename to final location
|
|
129
|
+
fs.renameSync(tempPath, filePath);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Clean up temp file if it exists
|
|
132
|
+
try {
|
|
133
|
+
if (fs.existsSync(tempPath)) {
|
|
134
|
+
fs.unlinkSync(tempPath);
|
|
135
|
+
}
|
|
136
|
+
} catch (cleanupError) {
|
|
137
|
+
// Ignore cleanup errors
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new Error(`Failed to write solution file ${fileName}: ${error}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Direct MCP tool handler for recommend functionality
|
|
146
|
+
*/
|
|
147
|
+
export async function handleRecommendTool(
|
|
148
|
+
args: { intent: string },
|
|
149
|
+
dotAI: DotAI,
|
|
150
|
+
logger: Logger,
|
|
151
|
+
requestId: string
|
|
152
|
+
): Promise<{ content: { type: 'text'; text: string }[] }> {
|
|
153
|
+
return await ErrorHandler.withErrorHandling(
|
|
154
|
+
async () => {
|
|
155
|
+
logger.debug('Handling recommend request', { requestId, intent: args?.intent });
|
|
156
|
+
|
|
157
|
+
// Input validation is handled automatically by MCP SDK with Zod schema
|
|
158
|
+
// args are already validated and typed when we reach this point
|
|
159
|
+
|
|
160
|
+
// Check for Claude API key
|
|
161
|
+
const claudeApiKey = dotAI.getAnthropicApiKey();
|
|
162
|
+
if (!claudeApiKey) {
|
|
163
|
+
throw ErrorHandler.createError(
|
|
164
|
+
ErrorCategory.AI_SERVICE,
|
|
165
|
+
ErrorSeverity.HIGH,
|
|
166
|
+
'ANTHROPIC_API_KEY environment variable must be set for AI-powered resource recommendations',
|
|
167
|
+
{
|
|
168
|
+
operation: 'api_key_check',
|
|
169
|
+
component: 'RecommendTool',
|
|
170
|
+
requestId,
|
|
171
|
+
suggestedActions: [
|
|
172
|
+
'Set ANTHROPIC_API_KEY environment variable',
|
|
173
|
+
'Verify the API key is valid and active',
|
|
174
|
+
'Check that the API key has sufficient credits'
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Validate session directory configuration
|
|
181
|
+
let sessionDir: string;
|
|
182
|
+
try {
|
|
183
|
+
sessionDir = getAndValidateSessionDirectory(args, true); // requireWrite=true
|
|
184
|
+
logger.debug('Session directory validated', { requestId, sessionDir });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw ErrorHandler.createError(
|
|
187
|
+
ErrorCategory.VALIDATION,
|
|
188
|
+
ErrorSeverity.HIGH,
|
|
189
|
+
`Session directory validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
190
|
+
{
|
|
191
|
+
operation: 'session_directory_validation',
|
|
192
|
+
component: 'RecommendTool',
|
|
193
|
+
requestId,
|
|
194
|
+
suggestedActions: [
|
|
195
|
+
'Ensure session directory exists and is writable',
|
|
196
|
+
'Set --session-dir parameter or DOT_AI_SESSION_DIR environment variable',
|
|
197
|
+
'Check directory permissions'
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
error instanceof Error ? error : new Error(String(error))
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logger.info('Starting resource recommendation process', {
|
|
205
|
+
requestId,
|
|
206
|
+
intent: args.intent,
|
|
207
|
+
hasApiKey: !!claudeApiKey
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Validate intent specificity with AI before expensive resource discovery
|
|
211
|
+
logger.debug('Validating intent specificity', { requestId, intent: args.intent });
|
|
212
|
+
try {
|
|
213
|
+
const claudeIntegration = new ClaudeIntegration(claudeApiKey);
|
|
214
|
+
await validateIntentWithAI(args.intent, claudeIntegration);
|
|
215
|
+
logger.debug('Intent validation passed', { requestId });
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof Error && error.message.includes('Intent needs more specificity')) {
|
|
218
|
+
// This is a validation error that should be returned to the user
|
|
219
|
+
throw ErrorHandler.createError(
|
|
220
|
+
ErrorCategory.VALIDATION,
|
|
221
|
+
ErrorSeverity.MEDIUM,
|
|
222
|
+
error.message,
|
|
223
|
+
{
|
|
224
|
+
operation: 'intent_validation',
|
|
225
|
+
component: 'RecommendTool',
|
|
226
|
+
requestId,
|
|
227
|
+
input: { intent: args.intent },
|
|
228
|
+
suggestedActions: [
|
|
229
|
+
'Provide more specific details about your deployment',
|
|
230
|
+
'Include technology stack information',
|
|
231
|
+
'Describe the purpose or function of what you want to deploy'
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
error
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
// For other errors, log but continue (don't block user due to AI service issues)
|
|
238
|
+
logger.warn('Intent validation failed, continuing with recommendation', { requestId, error: error instanceof Error ? error.message : 'Unknown error' });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Initialize AI-powered ResourceRecommender
|
|
242
|
+
const rankingConfig: AIRankingConfig = { claudeApiKey };
|
|
243
|
+
const recommender = new ResourceRecommender(rankingConfig);
|
|
244
|
+
|
|
245
|
+
// Create discovery functions
|
|
246
|
+
const discoverResourcesFn = async () => {
|
|
247
|
+
logger.debug('Discovering cluster resources', { requestId });
|
|
248
|
+
return await dotAI.discovery.discoverResources();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const explainResourceFn = async (resource: string) => {
|
|
252
|
+
logger.debug(`Explaining resource: ${resource}`, { requestId });
|
|
253
|
+
return await dotAI.discovery.explainResource(resource);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Find best solutions for the user intent
|
|
257
|
+
logger.debug('Generating recommendations with AI', { requestId });
|
|
258
|
+
const solutions = await recommender.findBestSolutions(
|
|
259
|
+
args.intent,
|
|
260
|
+
discoverResourcesFn,
|
|
261
|
+
explainResourceFn
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
logger.info('Recommendation process completed', {
|
|
265
|
+
requestId,
|
|
266
|
+
solutionCount: solutions.length,
|
|
267
|
+
topScore: solutions[0]?.score
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Create solution files and build response
|
|
271
|
+
const solutionSummaries = [];
|
|
272
|
+
const timestamp = new Date().toISOString();
|
|
273
|
+
|
|
274
|
+
// Limit to top 5 solutions (respecting quality thresholds from AI ranking)
|
|
275
|
+
const topSolutions = solutions.slice(0, 5);
|
|
276
|
+
|
|
277
|
+
for (const solution of topSolutions) {
|
|
278
|
+
const solutionId = generateSolutionId();
|
|
279
|
+
|
|
280
|
+
// Create complete solution file with all data
|
|
281
|
+
const solutionFileData = {
|
|
282
|
+
solutionId,
|
|
283
|
+
intent: args.intent,
|
|
284
|
+
type: solution.type,
|
|
285
|
+
score: solution.score,
|
|
286
|
+
description: solution.description,
|
|
287
|
+
reasons: solution.reasons,
|
|
288
|
+
analysis: solution.analysis,
|
|
289
|
+
resources: solution.resources.map(r => ({
|
|
290
|
+
kind: r.kind,
|
|
291
|
+
apiVersion: r.apiVersion,
|
|
292
|
+
group: r.group,
|
|
293
|
+
description: r.description
|
|
294
|
+
})),
|
|
295
|
+
questions: solution.questions,
|
|
296
|
+
answers: {}, // Empty initially - will be filled by answerQuestion tool
|
|
297
|
+
timestamp
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Write solution to file
|
|
301
|
+
try {
|
|
302
|
+
writeSolutionFile(sessionDir, solutionId, solutionFileData);
|
|
303
|
+
logger.debug('Solution file created', { requestId, solutionId, fileName: `${solutionId}.json` });
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw ErrorHandler.createError(
|
|
306
|
+
ErrorCategory.STORAGE,
|
|
307
|
+
ErrorSeverity.HIGH,
|
|
308
|
+
`Failed to store solution file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
309
|
+
{
|
|
310
|
+
operation: 'solution_file_creation',
|
|
311
|
+
component: 'RecommendTool',
|
|
312
|
+
requestId,
|
|
313
|
+
input: { solutionId },
|
|
314
|
+
suggestedActions: [
|
|
315
|
+
'Check session directory write permissions',
|
|
316
|
+
'Ensure sufficient disk space',
|
|
317
|
+
'Verify session directory is accessible'
|
|
318
|
+
]
|
|
319
|
+
},
|
|
320
|
+
error instanceof Error ? error : new Error(String(error))
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Add to response summary (decision-making data only)
|
|
325
|
+
solutionSummaries.push({
|
|
326
|
+
solutionId,
|
|
327
|
+
type: solution.type,
|
|
328
|
+
score: solution.score,
|
|
329
|
+
description: solution.description,
|
|
330
|
+
primaryResources: solution.resources.slice(0, 3).map(r => r.kind),
|
|
331
|
+
reasons: solution.reasons,
|
|
332
|
+
analysis: solution.analysis
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Build new response format
|
|
337
|
+
const response = {
|
|
338
|
+
intent: args.intent,
|
|
339
|
+
solutions: solutionSummaries,
|
|
340
|
+
nextAction: "Call chooseSolution with your preferred solutionId",
|
|
341
|
+
guidance: "🛑 NEVER choose automatically - Present ALL solutions to user and ask them to choose by calling chooseSolution(solutionId)",
|
|
342
|
+
timestamp
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
logger.info('Solution files created and response prepared', {
|
|
346
|
+
requestId,
|
|
347
|
+
solutionCount: solutionSummaries.length,
|
|
348
|
+
sessionDir
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
content: [{
|
|
354
|
+
type: 'text' as const,
|
|
355
|
+
text: JSON.stringify(response, null, 2)
|
|
356
|
+
}]
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
operation: 'recommend_tool',
|
|
361
|
+
component: 'RecommendTool',
|
|
362
|
+
requestId,
|
|
363
|
+
input: args
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
convertToMcp: true,
|
|
367
|
+
retryCount: 1
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Mock for @kubernetes/client-node to handle ES module compatibility issues
|
|
2
|
+
export const KubeConfig = jest.fn().mockImplementation(() => ({
|
|
3
|
+
loadFromDefault: jest.fn(),
|
|
4
|
+
loadFromFile: jest.fn().mockImplementation((path: string) => {
|
|
5
|
+
// Simulate error for invalid paths in tests
|
|
6
|
+
if (path.includes('/invalid/path') || path.includes('nonexistent')) {
|
|
7
|
+
throw new Error(`Failed to load kubeconfig from ${path}`);
|
|
8
|
+
}
|
|
9
|
+
}),
|
|
10
|
+
loadFromString: jest.fn(),
|
|
11
|
+
loadFromCluster: jest.fn(),
|
|
12
|
+
getCurrentContext: jest.fn().mockReturnValue('test-context'),
|
|
13
|
+
getCurrentCluster: jest.fn().mockReturnValue({
|
|
14
|
+
name: 'test-cluster',
|
|
15
|
+
server: 'https://test-cluster.example.com'
|
|
16
|
+
}),
|
|
17
|
+
makeApiClient: jest.fn().mockReturnValue({
|
|
18
|
+
listNamespace: jest.fn().mockImplementation(() => {
|
|
19
|
+
// Check if this is being called from an invalid discovery instance
|
|
20
|
+
// by checking the call stack or using a flag
|
|
21
|
+
return Promise.resolve({
|
|
22
|
+
items: [
|
|
23
|
+
{ metadata: { name: 'default' } },
|
|
24
|
+
{ metadata: { name: 'kube-system' } },
|
|
25
|
+
{ metadata: { name: 'test-namespace' } }
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
}),
|
|
29
|
+
listCustomResourceDefinition: jest.fn().mockResolvedValue({
|
|
30
|
+
items: [
|
|
31
|
+
{
|
|
32
|
+
metadata: { name: 'test-crd.example.com' },
|
|
33
|
+
spec: {
|
|
34
|
+
group: 'example.com',
|
|
35
|
+
versions: [{ name: 'v1' }],
|
|
36
|
+
names: { kind: 'TestCRD' },
|
|
37
|
+
scope: 'Namespaced'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}),
|
|
42
|
+
getAPIVersions: jest.fn().mockResolvedValue({
|
|
43
|
+
groups: [
|
|
44
|
+
{ name: 'apps', preferredVersion: { groupVersion: 'apps/v1' } },
|
|
45
|
+
{ name: 'v1', preferredVersion: { groupVersion: 'v1' } }
|
|
46
|
+
]
|
|
47
|
+
}),
|
|
48
|
+
// Add more API methods as needed
|
|
49
|
+
getNamespace: jest.fn().mockResolvedValue({
|
|
50
|
+
metadata: { name: 'test-namespace' }
|
|
51
|
+
}),
|
|
52
|
+
listNode: jest.fn().mockResolvedValue({
|
|
53
|
+
items: [
|
|
54
|
+
{ metadata: { name: 'test-node' } }
|
|
55
|
+
]
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
export const CoreV1Api = jest.fn().mockImplementation(() => ({
|
|
61
|
+
listNamespace: jest.fn().mockResolvedValue({
|
|
62
|
+
items: [
|
|
63
|
+
{ metadata: { name: 'default' } },
|
|
64
|
+
{ metadata: { name: 'kube-system' } }
|
|
65
|
+
]
|
|
66
|
+
}),
|
|
67
|
+
getNamespace: jest.fn().mockResolvedValue({
|
|
68
|
+
metadata: { name: 'test-namespace' }
|
|
69
|
+
}),
|
|
70
|
+
listNode: jest.fn().mockResolvedValue({
|
|
71
|
+
items: [{ metadata: { name: 'test-node' } }]
|
|
72
|
+
})
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
export const ApiextensionsV1Api = jest.fn().mockImplementation(() => ({
|
|
76
|
+
listCustomResourceDefinition: jest.fn().mockResolvedValue({
|
|
77
|
+
items: [
|
|
78
|
+
{
|
|
79
|
+
metadata: { name: 'test-crd.example.com' },
|
|
80
|
+
spec: {
|
|
81
|
+
group: 'example.com',
|
|
82
|
+
versions: [{ name: 'v1' }],
|
|
83
|
+
names: { kind: 'TestCRD' },
|
|
84
|
+
scope: 'Namespaced'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
})
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
export const VersionApi = jest.fn().mockImplementation(() => ({
|
|
92
|
+
getCode: jest.fn().mockResolvedValue({
|
|
93
|
+
gitVersion: 'v1.28.0',
|
|
94
|
+
platform: 'linux/amd64'
|
|
95
|
+
})
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
// Export all the common types that might be used
|
|
99
|
+
export const k8s = {
|
|
100
|
+
KubeConfig,
|
|
101
|
+
CoreV1Api,
|
|
102
|
+
ApiextensionsV1Api,
|
|
103
|
+
VersionApi
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default k8s;
|