@prodisco/k8s-mcp 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -38
- package/dist/kube/client.d.ts +6 -0
- package/dist/kube/client.d.ts.map +1 -1
- package/dist/kube/client.js +11 -0
- package/dist/kube/client.js.map +1 -1
- package/dist/kube/client.test.d.ts +2 -0
- package/dist/kube/client.test.d.ts.map +1 -0
- package/dist/kube/client.test.js +59 -0
- package/dist/kube/client.test.js.map +1 -0
- package/dist/server.js +129 -50
- package/dist/server.js.map +1 -1
- package/dist/tools/api/apiTools.d.ts +6 -0
- package/dist/tools/api/apiTools.d.ts.map +1 -0
- package/dist/tools/api/apiTools.js +132 -0
- package/dist/tools/api/apiTools.js.map +1 -0
- package/dist/tools/kubernetes/metadata.d.ts +169 -27
- package/dist/tools/kubernetes/metadata.d.ts.map +1 -1
- package/dist/tools/kubernetes/metadata.js +2 -7
- package/dist/tools/kubernetes/metadata.js.map +1 -1
- package/dist/tools/kubernetes/searchTools.d.ts +307 -5
- package/dist/tools/kubernetes/searchTools.d.ts.map +1 -1
- package/dist/tools/kubernetes/searchTools.js +2058 -273
- package/dist/tools/kubernetes/searchTools.js.map +1 -1
- package/dist/util/apiClient.d.ts +20 -0
- package/dist/util/apiClient.d.ts.map +1 -0
- package/dist/util/apiClient.js +90 -0
- package/dist/util/apiClient.js.map +1 -0
- package/dist/util/k8sClient.d.ts +38 -0
- package/dist/util/k8sClient.d.ts.map +1 -0
- package/dist/util/k8sClient.js +85 -0
- package/dist/util/k8sClient.js.map +1 -0
- package/dist/util/logger.d.ts +26 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +62 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/summary.d.ts +16 -16
- package/package.json +4 -1
|
@@ -1,36 +1,90 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import * as k8s from '@kubernetes/client-node';
|
|
3
3
|
import * as ts from 'typescript';
|
|
4
|
-
import { readFileSync, existsSync, readdirSync, mkdirSync, symlinkSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
4
|
+
import { readFileSync, existsSync, readdirSync, mkdirSync, symlinkSync, realpathSync, unlinkSync } from 'fs';
|
|
5
|
+
import { join, basename } from 'path';
|
|
6
6
|
import * as os from 'os';
|
|
7
7
|
import { PACKAGE_ROOT } from '../../util/paths.js';
|
|
8
|
+
import { create, insert, search, remove } from '@orama/orama';
|
|
9
|
+
import chokidar from 'chokidar';
|
|
10
|
+
import { logger } from '../../util/logger.js';
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Search Configuration Constants
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/** Maximum number of resource types to extract from script content to prevent noise */
|
|
15
|
+
const MAX_RESOURCE_TYPES_FROM_CONTENT = 10;
|
|
16
|
+
/** Multiplier for initial search results to allow for post-filtering and pagination */
|
|
17
|
+
const SEARCH_RESULTS_MULTIPLIER = 3;
|
|
18
|
+
/** Minimum number of search results to fetch before post-filtering */
|
|
19
|
+
const MIN_SEARCH_RESULTS = 100;
|
|
20
|
+
/** Maximum number of relevant scripts to show in method search results */
|
|
21
|
+
const MAX_RELEVANT_SCRIPTS = 5;
|
|
22
|
+
/** Default maximum number of properties to show when formatting type definitions */
|
|
23
|
+
const DEFAULT_MAX_TYPE_PROPERTIES = 20;
|
|
24
|
+
// ============================================================================
|
|
8
25
|
const SearchToolsInputSchema = z.object({
|
|
26
|
+
// Mode selection - determines which operation to perform
|
|
27
|
+
mode: z
|
|
28
|
+
.enum(['methods', 'types', 'scripts', 'prometheus'])
|
|
29
|
+
.default('methods')
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Search mode: "methods" for K8s API, "types" for type defs, "scripts" for cached scripts, "prometheus" for metrics/analytics libraries'),
|
|
32
|
+
// === Method mode parameters (mode: 'methods') ===
|
|
9
33
|
resourceType: z
|
|
10
34
|
.string()
|
|
11
|
-
.
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('(methods mode) Kubernetes resource type (e.g., "Pod", "Deployment", "Service", "ConfigMap")'),
|
|
12
37
|
action: z
|
|
13
38
|
.string()
|
|
14
39
|
.optional()
|
|
15
|
-
.describe('API action: list, read, create, delete, patch, replace, connect, get, watch
|
|
40
|
+
.describe('(methods mode) API action: list, read, create, delete, patch, replace, connect, get, watch'),
|
|
16
41
|
scope: z
|
|
17
42
|
.enum(['namespaced', 'cluster', 'all'])
|
|
18
43
|
.optional()
|
|
19
44
|
.default('all')
|
|
20
|
-
.describe('Resource scope: "namespaced"
|
|
45
|
+
.describe('(methods mode) Resource scope: "namespaced", "cluster", or "all"'),
|
|
21
46
|
exclude: z
|
|
22
47
|
.object({
|
|
23
48
|
actions: z
|
|
24
49
|
.array(z.string())
|
|
25
50
|
.optional()
|
|
26
|
-
.describe('Actions to exclude (e.g., ["connect", "watch"])
|
|
51
|
+
.describe('Actions to exclude (e.g., ["connect", "watch"])'),
|
|
27
52
|
apiClasses: z
|
|
28
53
|
.array(z.string())
|
|
29
54
|
.optional()
|
|
30
|
-
.describe('API classes to exclude (e.g., ["CustomObjectsApi"])
|
|
55
|
+
.describe('API classes to exclude (e.g., ["CustomObjectsApi"])'),
|
|
31
56
|
})
|
|
32
57
|
.optional()
|
|
33
|
-
.describe('
|
|
58
|
+
.describe('(methods mode) Exclusion criteria'),
|
|
59
|
+
// === Type mode parameters (mode: 'types') ===
|
|
60
|
+
types: z
|
|
61
|
+
.array(z.string())
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('(types mode) Type names or paths (e.g., ["V1Pod", "V1Deployment.spec.template.spec"])'),
|
|
64
|
+
depth: z
|
|
65
|
+
.number()
|
|
66
|
+
.int()
|
|
67
|
+
.positive()
|
|
68
|
+
.max(2)
|
|
69
|
+
.default(1)
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('(types mode) Depth of nested type definitions (1-2, default: 1)'),
|
|
72
|
+
// === Script mode parameters (mode: 'scripts') ===
|
|
73
|
+
searchTerm: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('(scripts mode) Search term to find cached scripts (e.g., "pod", "logs"). If omitted, shows all scripts.'),
|
|
77
|
+
// === Prometheus mode parameters (mode: 'prometheus') ===
|
|
78
|
+
category: z
|
|
79
|
+
.enum(['query', 'metadata', 'alerts', 'metrics', 'all'])
|
|
80
|
+
.optional()
|
|
81
|
+
.default('all')
|
|
82
|
+
.describe('(prometheus mode) Filter by category: "query" (PromQL), "metadata" (labels/series), "alerts", or "metrics" (discover cluster metrics)'),
|
|
83
|
+
methodPattern: z
|
|
84
|
+
.string()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('(prometheus mode) Search pattern for method names (e.g., "mean", "query", "percentile")'),
|
|
87
|
+
// === Shared parameters ===
|
|
34
88
|
limit: z
|
|
35
89
|
.number()
|
|
36
90
|
.int()
|
|
@@ -39,9 +93,1310 @@ const SearchToolsInputSchema = z.object({
|
|
|
39
93
|
.default(10)
|
|
40
94
|
.optional()
|
|
41
95
|
.describe('Maximum number of results to return'),
|
|
96
|
+
offset: z
|
|
97
|
+
.number()
|
|
98
|
+
.int()
|
|
99
|
+
.nonnegative()
|
|
100
|
+
.default(0)
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('Number of results to skip for pagination (default: 0)'),
|
|
42
103
|
});
|
|
43
|
-
|
|
44
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Extract JSDoc comment from a node
|
|
106
|
+
*/
|
|
107
|
+
function getJSDocDescription(node) {
|
|
108
|
+
const jsDocComments = ts.getJSDocCommentsAndTags(node);
|
|
109
|
+
for (const comment of jsDocComments) {
|
|
110
|
+
if (ts.isJSDoc(comment) && comment.comment) {
|
|
111
|
+
if (typeof comment.comment === 'string') {
|
|
112
|
+
return comment.comment;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extract nested type references from a TypeNode using TypeScript AST
|
|
120
|
+
*/
|
|
121
|
+
function extractNestedTypeRefsFromNode(typeNode) {
|
|
122
|
+
if (!typeNode) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
const refs = [];
|
|
126
|
+
function visit(node) {
|
|
127
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
128
|
+
const typeName = node.typeName.getText();
|
|
129
|
+
// Only include K8s types (V1*, K8*, Core*)
|
|
130
|
+
if ((typeName.startsWith('V') || typeName.startsWith('K') || typeName.startsWith('Core')) &&
|
|
131
|
+
!refs.includes(typeName)) {
|
|
132
|
+
refs.push(typeName);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
ts.forEachChild(node, visit);
|
|
136
|
+
}
|
|
137
|
+
visit(typeNode);
|
|
138
|
+
return refs;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract type definition from TypeScript declaration file using TypeScript compiler API
|
|
142
|
+
*/
|
|
143
|
+
function extractTypeDefinitionWithTS(typeName, filePath) {
|
|
144
|
+
const sourceCode = readFileSync(filePath, 'utf-8');
|
|
145
|
+
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true);
|
|
146
|
+
let typeInfo = null;
|
|
147
|
+
const nestedTypes = new Set();
|
|
148
|
+
function visit(node) {
|
|
149
|
+
if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && node.name && node.name.text === typeName) {
|
|
150
|
+
const properties = [];
|
|
151
|
+
const description = getJSDocDescription(node);
|
|
152
|
+
node.members?.forEach((member) => {
|
|
153
|
+
if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
|
|
154
|
+
if (member.name) {
|
|
155
|
+
const propName = member.name.getText(sourceFile);
|
|
156
|
+
const propType = member.type?.getText(sourceFile) || 'any';
|
|
157
|
+
const isOptional = !!member.questionToken;
|
|
158
|
+
const propDescription = getJSDocDescription(member);
|
|
159
|
+
properties.push({
|
|
160
|
+
name: propName.replace(/['"]/g, ''),
|
|
161
|
+
type: propType,
|
|
162
|
+
optional: isOptional,
|
|
163
|
+
description: propDescription,
|
|
164
|
+
});
|
|
165
|
+
const typeRefs = extractNestedTypeRefsFromNode(member.type);
|
|
166
|
+
for (const ref of typeRefs) {
|
|
167
|
+
if (ref !== typeName) {
|
|
168
|
+
nestedTypes.add(ref);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
typeInfo = {
|
|
175
|
+
name: typeName,
|
|
176
|
+
properties,
|
|
177
|
+
description,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
ts.forEachChild(node, visit);
|
|
181
|
+
}
|
|
182
|
+
visit(sourceFile);
|
|
183
|
+
if (!typeInfo) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
typeInfo,
|
|
188
|
+
nestedTypes: Array.from(nestedTypes),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Extract the main type identifier from a TypeScript type node
|
|
193
|
+
* Handles: Array<V1Pod>, V1PodSpec | undefined, V1Container[], etc.
|
|
194
|
+
*/
|
|
195
|
+
function extractTypeIdentifier(typeNode) {
|
|
196
|
+
if (ts.isUnionTypeNode(typeNode)) {
|
|
197
|
+
for (const type of typeNode.types) {
|
|
198
|
+
if (type.kind === ts.SyntaxKind.UndefinedKeyword || type.kind === ts.SyntaxKind.NullKeyword) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
return extractTypeIdentifier(type);
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (ts.isArrayTypeNode(typeNode)) {
|
|
206
|
+
return extractTypeIdentifier(typeNode.elementType);
|
|
207
|
+
}
|
|
208
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
209
|
+
const typeName = typeNode.typeName.getText();
|
|
210
|
+
if (typeName === 'Array' && typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
211
|
+
const firstArg = typeNode.typeArguments[0];
|
|
212
|
+
if (firstArg) {
|
|
213
|
+
return extractTypeIdentifier(firstArg);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return typeName;
|
|
217
|
+
}
|
|
218
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Format type info as a readable string
|
|
225
|
+
*/
|
|
226
|
+
function formatTypeInfo(typeInfo, maxProperties = DEFAULT_MAX_TYPE_PROPERTIES) {
|
|
227
|
+
let result = `${typeInfo.name} {\n`;
|
|
228
|
+
const propsToShow = typeInfo.properties.slice(0, maxProperties);
|
|
229
|
+
const hasMore = typeInfo.properties.length > maxProperties;
|
|
230
|
+
for (const prop of propsToShow) {
|
|
231
|
+
const optionalMarker = prop.optional ? '?' : '';
|
|
232
|
+
result += ` ${prop.name}${optionalMarker}: ${prop.type}\n`;
|
|
233
|
+
}
|
|
234
|
+
if (hasMore) {
|
|
235
|
+
result += ` ... ${typeInfo.properties.length - maxProperties} more properties\n`;
|
|
236
|
+
}
|
|
237
|
+
result += `}`;
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Find type definition file in Kubernetes client-node package
|
|
242
|
+
*/
|
|
243
|
+
function findTypeDefinitionFile(typeName, basePath) {
|
|
244
|
+
const k8sPath = join(basePath, 'node_modules', '@kubernetes', 'client-node', 'dist', 'gen', 'models');
|
|
245
|
+
const filePath = join(k8sPath, `${typeName}.d.ts`);
|
|
246
|
+
if (existsSync(filePath)) {
|
|
247
|
+
return filePath;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Parse a type path into base type and property path
|
|
253
|
+
* e.g., "V1Deployment.spec.template" -> { baseType: "V1Deployment", path: ["spec", "template"] }
|
|
254
|
+
*/
|
|
255
|
+
function parseTypePath(typePath) {
|
|
256
|
+
const parts = typePath.split('.');
|
|
257
|
+
const baseType = parts[0];
|
|
258
|
+
if (!baseType) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const path = parts.slice(1);
|
|
262
|
+
return { baseType, path };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Navigate through type properties to find a subtype
|
|
266
|
+
*/
|
|
267
|
+
function navigateToSubtype(typeInfo, propertyPath, basePath, cache) {
|
|
268
|
+
if (propertyPath.length === 0) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
let currentTypeInfo = typeInfo;
|
|
272
|
+
let currentTypeName = typeInfo.name;
|
|
273
|
+
const pathSegments = [currentTypeName];
|
|
274
|
+
for (let i = 0; i < propertyPath.length; i++) {
|
|
275
|
+
const propertyName = propertyPath[i];
|
|
276
|
+
if (!propertyName) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const property = currentTypeInfo.properties.find(p => p.name === propertyName);
|
|
280
|
+
if (!property) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
pathSegments.push(propertyName);
|
|
284
|
+
const filePath = findTypeDefinitionFile(currentTypeName, basePath);
|
|
285
|
+
if (!filePath) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const sourceCode = readFileSync(filePath, 'utf-8');
|
|
289
|
+
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true);
|
|
290
|
+
let propertyTypeNode = null;
|
|
291
|
+
function findPropertyType(node) {
|
|
292
|
+
if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) &&
|
|
293
|
+
node.name && node.name.text === currentTypeName) {
|
|
294
|
+
node.members?.forEach((member) => {
|
|
295
|
+
if ((ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) &&
|
|
296
|
+
member.name && member.type) {
|
|
297
|
+
const memberName = member.name.getText(sourceFile).replace(/['"]/g, '');
|
|
298
|
+
if (memberName === propertyName) {
|
|
299
|
+
propertyTypeNode = member.type;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (!propertyTypeNode) {
|
|
305
|
+
ts.forEachChild(node, findPropertyType);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
findPropertyType(sourceFile);
|
|
309
|
+
if (!propertyTypeNode) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const nextTypeName = extractTypeIdentifier(propertyTypeNode);
|
|
313
|
+
if (!nextTypeName) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (i === propertyPath.length - 1) {
|
|
317
|
+
return {
|
|
318
|
+
typeInfo: currentTypeInfo,
|
|
319
|
+
propertyPath: pathSegments.join('.'),
|
|
320
|
+
typeName: nextTypeName,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
let nextTypeInfo = cache.get(nextTypeName);
|
|
324
|
+
if (!nextTypeInfo) {
|
|
325
|
+
const filePath = findTypeDefinitionFile(nextTypeName, basePath);
|
|
326
|
+
if (!filePath) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
const extracted = extractTypeDefinitionWithTS(nextTypeName, filePath);
|
|
330
|
+
if (!extracted) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
nextTypeInfo = extracted.typeInfo;
|
|
334
|
+
cache.set(nextTypeName, nextTypeInfo);
|
|
335
|
+
}
|
|
336
|
+
currentTypeInfo = nextTypeInfo;
|
|
337
|
+
currentTypeName = nextTypeName;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Get type information for a subtype at a specific path
|
|
343
|
+
*/
|
|
344
|
+
function getSubtypeInfo(baseTypeName, propertyPath, basePath, cache) {
|
|
345
|
+
let baseTypeInfo = cache.get(baseTypeName);
|
|
346
|
+
if (!baseTypeInfo) {
|
|
347
|
+
const filePath = findTypeDefinitionFile(baseTypeName, basePath);
|
|
348
|
+
if (!filePath) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const extracted = extractTypeDefinitionWithTS(baseTypeName, filePath);
|
|
352
|
+
if (!extracted) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
baseTypeInfo = extracted.typeInfo;
|
|
356
|
+
cache.set(baseTypeName, baseTypeInfo);
|
|
357
|
+
}
|
|
358
|
+
if (propertyPath.length === 0) {
|
|
359
|
+
return {
|
|
360
|
+
typeInfo: baseTypeInfo,
|
|
361
|
+
fullPath: baseTypeName,
|
|
362
|
+
originalType: baseTypeName,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const result = navigateToSubtype(baseTypeInfo, propertyPath, basePath, cache);
|
|
366
|
+
if (!result) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
const targetTypeName = result.typeName;
|
|
370
|
+
let targetTypeInfo = cache.get(targetTypeName);
|
|
371
|
+
if (!targetTypeInfo) {
|
|
372
|
+
const filePath = findTypeDefinitionFile(targetTypeName, basePath);
|
|
373
|
+
if (filePath) {
|
|
374
|
+
const extracted = extractTypeDefinitionWithTS(targetTypeName, filePath);
|
|
375
|
+
if (extracted) {
|
|
376
|
+
targetTypeInfo = extracted.typeInfo;
|
|
377
|
+
cache.set(targetTypeName, targetTypeInfo);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!targetTypeInfo) {
|
|
382
|
+
const lastProp = propertyPath[propertyPath.length - 1];
|
|
383
|
+
if (!lastProp) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const property = result.typeInfo.properties.find(p => p.name === lastProp);
|
|
387
|
+
if (property) {
|
|
388
|
+
targetTypeInfo = {
|
|
389
|
+
name: `${result.propertyPath}`,
|
|
390
|
+
properties: [{
|
|
391
|
+
name: lastProp,
|
|
392
|
+
type: property.type,
|
|
393
|
+
optional: property.optional,
|
|
394
|
+
description: property.description || undefined,
|
|
395
|
+
}],
|
|
396
|
+
description: `Property type: ${property.type}`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
typeInfo: targetTypeInfo,
|
|
405
|
+
fullPath: result.propertyPath,
|
|
406
|
+
originalType: targetTypeName,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// End Type Definition Helper Functions
|
|
411
|
+
// ============================================================================
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// SearchToolsService Class - Encapsulates All Module State
|
|
414
|
+
// ============================================================================
|
|
415
|
+
/**
|
|
416
|
+
* Service class that encapsulates the search tools state and operations.
|
|
417
|
+
* This provides:
|
|
418
|
+
* - Proper lifecycle management (initialize/shutdown)
|
|
419
|
+
* - Testability through class instantiation
|
|
420
|
+
* - Clean separation of state from functions
|
|
421
|
+
*/
|
|
422
|
+
class SearchToolsService {
|
|
423
|
+
/** Cache for Kubernetes API methods */
|
|
424
|
+
apiMethodsCache = null;
|
|
425
|
+
/** Orama database instance cache */
|
|
426
|
+
oramaDb = null;
|
|
427
|
+
/** Track indexed scripts to support incremental re-indexing */
|
|
428
|
+
indexedScriptPaths = new Set();
|
|
429
|
+
/** Filesystem watcher instance */
|
|
430
|
+
scriptWatcher = null;
|
|
431
|
+
/** Whether the service has been initialized */
|
|
432
|
+
initialized = false;
|
|
433
|
+
/** Prometheus metrics indexing status */
|
|
434
|
+
metricsIndexingStatus = 'unavailable';
|
|
435
|
+
/** Interval for refreshing Prometheus metrics */
|
|
436
|
+
metricsRefreshInterval = null;
|
|
437
|
+
/** Refresh interval in milliseconds (30 minutes) */
|
|
438
|
+
static METRICS_REFRESH_INTERVAL = 30 * 60 * 1000;
|
|
439
|
+
/**
|
|
440
|
+
* Initialize the search index and start the script watcher.
|
|
441
|
+
* This is called automatically on first use, but can be called explicitly
|
|
442
|
+
* for pre-warming during server startup.
|
|
443
|
+
*/
|
|
444
|
+
async initialize() {
|
|
445
|
+
if (this.initialized) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
await this.initializeOramaDb();
|
|
449
|
+
this.initialized = true;
|
|
450
|
+
// Start background Prometheus metrics indexing (non-blocking)
|
|
451
|
+
if (process.env.PROMETHEUS_URL) {
|
|
452
|
+
this.startPrometheusMetricsIndexing();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get the current Prometheus metrics indexing status
|
|
457
|
+
*/
|
|
458
|
+
getMetricsIndexingStatus() {
|
|
459
|
+
return this.metricsIndexingStatus;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Shutdown the service, stopping the script watcher.
|
|
463
|
+
* Call this during graceful shutdown.
|
|
464
|
+
*/
|
|
465
|
+
async shutdown() {
|
|
466
|
+
if (this.scriptWatcher) {
|
|
467
|
+
await this.scriptWatcher.close();
|
|
468
|
+
this.scriptWatcher = null;
|
|
469
|
+
logger.info('Orama: Stopped script watcher');
|
|
470
|
+
}
|
|
471
|
+
if (this.metricsRefreshInterval) {
|
|
472
|
+
clearInterval(this.metricsRefreshInterval);
|
|
473
|
+
this.metricsRefreshInterval = null;
|
|
474
|
+
logger.info('Orama: Stopped Prometheus metrics refresh');
|
|
475
|
+
}
|
|
476
|
+
this.oramaDb = null;
|
|
477
|
+
this.apiMethodsCache = null;
|
|
478
|
+
this.indexedScriptPaths.clear();
|
|
479
|
+
this.metricsIndexingStatus = 'unavailable';
|
|
480
|
+
this.initialized = false;
|
|
481
|
+
// Clear module-level caches
|
|
482
|
+
clearPrometheusMethodsCache();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get the Orama database instance, initializing it if needed
|
|
486
|
+
*/
|
|
487
|
+
async getOramaDb() {
|
|
488
|
+
if (!this.oramaDb) {
|
|
489
|
+
await this.initializeOramaDb();
|
|
490
|
+
}
|
|
491
|
+
return this.oramaDb;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Get the cached API methods, extracting them if needed
|
|
495
|
+
*/
|
|
496
|
+
getApiMethods() {
|
|
497
|
+
if (!this.apiMethodsCache) {
|
|
498
|
+
this.apiMethodsCache = this.extractKubernetesApiMethods();
|
|
499
|
+
}
|
|
500
|
+
return this.apiMethodsCache;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Initialize and populate the Orama search database
|
|
504
|
+
*/
|
|
505
|
+
async initializeOramaDb() {
|
|
506
|
+
if (this.oramaDb) {
|
|
507
|
+
return this.oramaDb;
|
|
508
|
+
}
|
|
509
|
+
// Create Orama instance with optimized configuration
|
|
510
|
+
const db = await create({
|
|
511
|
+
schema: oramaSchema,
|
|
512
|
+
components: {
|
|
513
|
+
tokenizer: {
|
|
514
|
+
stemming: true,
|
|
515
|
+
// Skip stemming for code identifiers - they should match exactly
|
|
516
|
+
stemmerSkipProperties: ['methodName', 'resourceType', 'apiClass', 'id'],
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
// Get all API methods and index them
|
|
521
|
+
const methods = this.getApiMethods();
|
|
522
|
+
for (const method of methods) {
|
|
523
|
+
// Skip WithHttpInfo variants
|
|
524
|
+
if (method.methodName.toLowerCase().includes('withhttpinfo')) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
// Build searchTokens from identifiers for better matching
|
|
528
|
+
const searchTokens = [
|
|
529
|
+
method.resourceType,
|
|
530
|
+
method.methodName,
|
|
531
|
+
method.apiClass,
|
|
532
|
+
].join(' ');
|
|
533
|
+
const doc = {
|
|
534
|
+
id: `${method.apiClass}.${method.methodName}`,
|
|
535
|
+
documentType: 'method',
|
|
536
|
+
resourceType: method.resourceType,
|
|
537
|
+
methodName: method.methodName,
|
|
538
|
+
description: method.description,
|
|
539
|
+
searchTokens,
|
|
540
|
+
action: extractAction(method.methodName),
|
|
541
|
+
scope: extractScope(method.methodName),
|
|
542
|
+
apiClass: method.apiClass,
|
|
543
|
+
filePath: '',
|
|
544
|
+
};
|
|
545
|
+
await insert(db, doc);
|
|
546
|
+
}
|
|
547
|
+
// Index cached scripts
|
|
548
|
+
const scriptCount = await this.indexCachedScripts(db);
|
|
549
|
+
// Index prometheus library methods
|
|
550
|
+
const prometheusCount = await this.indexPrometheusMethods(db);
|
|
551
|
+
// Start filesystem watcher for script changes
|
|
552
|
+
this.startScriptWatcher(db);
|
|
553
|
+
this.oramaDb = db;
|
|
554
|
+
logger.info(`Orama: Indexed ${methods.length} API methods, ${scriptCount} cached scripts, and ${prometheusCount} prometheus methods`);
|
|
555
|
+
return db;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Index cached scripts into the Orama database.
|
|
559
|
+
*/
|
|
560
|
+
async indexCachedScripts(db) {
|
|
561
|
+
const scriptsDirectory = join(os.homedir(), '.prodisco', 'scripts', 'cache');
|
|
562
|
+
let indexedCount = 0;
|
|
563
|
+
try {
|
|
564
|
+
if (!existsSync(scriptsDirectory)) {
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
const files = readdirSync(scriptsDirectory)
|
|
568
|
+
.filter(f => f.endsWith('.ts'))
|
|
569
|
+
.map(f => join(scriptsDirectory, f));
|
|
570
|
+
for (const filePath of files) {
|
|
571
|
+
// Skip if already indexed
|
|
572
|
+
if (this.indexedScriptPaths.has(filePath)) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const script = parseScriptFile(filePath);
|
|
576
|
+
if (!script) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const doc = buildScriptDocument(script);
|
|
580
|
+
await insert(db, doc);
|
|
581
|
+
this.indexedScriptPaths.add(filePath);
|
|
582
|
+
indexedCount++;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
logger.error('Error indexing cached scripts', error);
|
|
587
|
+
}
|
|
588
|
+
return indexedCount;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Index prometheus library methods into the Orama database.
|
|
592
|
+
*/
|
|
593
|
+
async indexPrometheusMethods(db) {
|
|
594
|
+
const methods = getPrometheusMethods();
|
|
595
|
+
let indexedCount = 0;
|
|
596
|
+
for (const method of methods) {
|
|
597
|
+
// Build searchTokens from identifiers for better matching
|
|
598
|
+
const searchTokens = [
|
|
599
|
+
method.methodName,
|
|
600
|
+
method.className || '',
|
|
601
|
+
method.library,
|
|
602
|
+
method.category,
|
|
603
|
+
method.description,
|
|
604
|
+
].join(' ');
|
|
605
|
+
const doc = {
|
|
606
|
+
id: `prometheus:${method.library}:${method.className || 'fn'}:${method.methodName}`,
|
|
607
|
+
documentType: 'prometheus',
|
|
608
|
+
resourceType: method.category, // Use category as resourceType for search
|
|
609
|
+
methodName: method.methodName,
|
|
610
|
+
description: method.description,
|
|
611
|
+
searchTokens,
|
|
612
|
+
action: 'prometheus',
|
|
613
|
+
scope: 'prometheus',
|
|
614
|
+
apiClass: method.library,
|
|
615
|
+
filePath: '',
|
|
616
|
+
library: method.library,
|
|
617
|
+
category: method.category,
|
|
618
|
+
};
|
|
619
|
+
await insert(db, doc);
|
|
620
|
+
indexedCount++;
|
|
621
|
+
}
|
|
622
|
+
return indexedCount;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Start background Prometheus metrics indexing (non-blocking).
|
|
626
|
+
* Fetches metric metadata from Prometheus and indexes into Orama.
|
|
627
|
+
*/
|
|
628
|
+
startPrometheusMetricsIndexing() {
|
|
629
|
+
this.metricsIndexingStatus = 'in_progress';
|
|
630
|
+
// Run indexing in background (don't await)
|
|
631
|
+
this.indexPrometheusMetrics()
|
|
632
|
+
.then(() => {
|
|
633
|
+
this.metricsIndexingStatus = 'ready';
|
|
634
|
+
// Schedule incremental refresh
|
|
635
|
+
this.metricsRefreshInterval = setInterval(() => {
|
|
636
|
+
this.refreshPrometheusMetrics();
|
|
637
|
+
}, SearchToolsService.METRICS_REFRESH_INTERVAL);
|
|
638
|
+
})
|
|
639
|
+
.catch((error) => {
|
|
640
|
+
logger.error('Failed to index Prometheus metrics', error);
|
|
641
|
+
this.metricsIndexingStatus = 'unavailable';
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Index Prometheus metrics from the cluster into Orama.
|
|
646
|
+
*/
|
|
647
|
+
async indexPrometheusMetrics() {
|
|
648
|
+
const prometheusUrl = process.env.PROMETHEUS_URL;
|
|
649
|
+
if (!prometheusUrl) {
|
|
650
|
+
return 0;
|
|
651
|
+
}
|
|
652
|
+
const { PrometheusDriver } = await import('prometheus-query');
|
|
653
|
+
const prom = new PrometheusDriver({ endpoint: prometheusUrl });
|
|
654
|
+
const metadata = await prom.metadata();
|
|
655
|
+
const db = await this.getOramaDb();
|
|
656
|
+
let count = 0;
|
|
657
|
+
for (const [name, info] of Object.entries(metadata)) {
|
|
658
|
+
const metricInfo = Array.isArray(info) ? info[0] : info;
|
|
659
|
+
const metricType = metricInfo?.type || 'unknown';
|
|
660
|
+
const description = metricInfo?.help || 'No description available';
|
|
661
|
+
const doc = {
|
|
662
|
+
id: `metric:${name}`,
|
|
663
|
+
documentType: 'prometheus-metric',
|
|
664
|
+
resourceType: '',
|
|
665
|
+
methodName: name,
|
|
666
|
+
description,
|
|
667
|
+
searchTokens: `${name.replace(/_/g, ' ')} ${metricType} ${description}`,
|
|
668
|
+
action: 'metric',
|
|
669
|
+
scope: 'prometheus',
|
|
670
|
+
apiClass: 'prometheus-metric',
|
|
671
|
+
filePath: '',
|
|
672
|
+
metricType,
|
|
673
|
+
};
|
|
674
|
+
await insert(db, doc);
|
|
675
|
+
count++;
|
|
676
|
+
}
|
|
677
|
+
logger.info(`Orama: Indexed ${count} Prometheus metrics from cluster`);
|
|
678
|
+
return count;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Incrementally refresh Prometheus metrics.
|
|
682
|
+
* Adds new metrics, removes stale ones.
|
|
683
|
+
*/
|
|
684
|
+
async refreshPrometheusMetrics() {
|
|
685
|
+
const prometheusUrl = process.env.PROMETHEUS_URL;
|
|
686
|
+
if (!prometheusUrl) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const { PrometheusDriver } = await import('prometheus-query');
|
|
691
|
+
const prom = new PrometheusDriver({ endpoint: prometheusUrl });
|
|
692
|
+
const metadata = await prom.metadata();
|
|
693
|
+
const db = await this.getOramaDb();
|
|
694
|
+
const currentMetrics = new Set(Object.keys(metadata));
|
|
695
|
+
// Get existing indexed metrics
|
|
696
|
+
const existingResults = await search(db, {
|
|
697
|
+
term: '',
|
|
698
|
+
properties: ['methodName'],
|
|
699
|
+
limit: 10000,
|
|
700
|
+
});
|
|
701
|
+
const existingMetrics = new Map();
|
|
702
|
+
for (const hit of existingResults.hits) {
|
|
703
|
+
if (hit.document.documentType === 'prometheus-metric') {
|
|
704
|
+
existingMetrics.set(hit.document.methodName, hit.document.id);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
let added = 0;
|
|
708
|
+
let removed = 0;
|
|
709
|
+
// Add new metrics
|
|
710
|
+
for (const [name, info] of Object.entries(metadata)) {
|
|
711
|
+
if (!existingMetrics.has(name)) {
|
|
712
|
+
const metricInfo = Array.isArray(info) ? info[0] : info;
|
|
713
|
+
const metricType = metricInfo?.type || 'unknown';
|
|
714
|
+
const description = metricInfo?.help || 'No description available';
|
|
715
|
+
const doc = {
|
|
716
|
+
id: `metric:${name}`,
|
|
717
|
+
documentType: 'prometheus-metric',
|
|
718
|
+
resourceType: '',
|
|
719
|
+
methodName: name,
|
|
720
|
+
description,
|
|
721
|
+
searchTokens: `${name.replace(/_/g, ' ')} ${metricType} ${description}`,
|
|
722
|
+
action: 'metric',
|
|
723
|
+
scope: 'prometheus',
|
|
724
|
+
apiClass: 'prometheus-metric',
|
|
725
|
+
filePath: '',
|
|
726
|
+
metricType,
|
|
727
|
+
};
|
|
728
|
+
await insert(db, doc);
|
|
729
|
+
added++;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Remove stale metrics
|
|
733
|
+
for (const [name, id] of existingMetrics) {
|
|
734
|
+
if (!currentMetrics.has(name)) {
|
|
735
|
+
try {
|
|
736
|
+
await remove(db, id);
|
|
737
|
+
removed++;
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// Ignore removal errors
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (added > 0 || removed > 0) {
|
|
745
|
+
logger.info(`Orama: Prometheus metrics refresh - added ${added}, removed ${removed}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
logger.error('Failed to refresh Prometheus metrics', error);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Start filesystem watcher for cached scripts directory.
|
|
754
|
+
*/
|
|
755
|
+
startScriptWatcher(db) {
|
|
756
|
+
const scriptsDirectory = join(os.homedir(), '.prodisco', 'scripts', 'cache');
|
|
757
|
+
// Ensure directory exists before watching
|
|
758
|
+
if (!existsSync(scriptsDirectory)) {
|
|
759
|
+
try {
|
|
760
|
+
mkdirSync(scriptsDirectory, { recursive: true });
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
this.scriptWatcher = chokidar.watch(join(scriptsDirectory, '*.ts'), {
|
|
767
|
+
persistent: true,
|
|
768
|
+
ignoreInitial: true,
|
|
769
|
+
});
|
|
770
|
+
this.scriptWatcher.on('add', async (filePath) => {
|
|
771
|
+
const script = parseScriptFile(filePath);
|
|
772
|
+
if (script) {
|
|
773
|
+
const doc = buildScriptDocument(script);
|
|
774
|
+
await insert(db, doc);
|
|
775
|
+
this.indexedScriptPaths.add(filePath);
|
|
776
|
+
logger.debug(`Orama: Indexed new script ${basename(filePath)}`);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
this.scriptWatcher.on('unlink', async (filePath) => {
|
|
780
|
+
const docId = `script:${basename(filePath)}`;
|
|
781
|
+
try {
|
|
782
|
+
await remove(db, docId);
|
|
783
|
+
this.indexedScriptPaths.delete(filePath);
|
|
784
|
+
logger.debug(`Orama: Removed script ${basename(filePath)} from index`);
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
logger.debug(`Could not remove script ${basename(filePath)} from index: ${error instanceof Error ? error.message : String(error)}`);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
this.scriptWatcher.on('change', async (filePath) => {
|
|
791
|
+
const docId = `script:${basename(filePath)}`;
|
|
792
|
+
try {
|
|
793
|
+
await remove(db, docId);
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
logger.debug(`Script ${basename(filePath)} was not in index, will add: ${error instanceof Error ? error.message : String(error)}`);
|
|
797
|
+
}
|
|
798
|
+
const script = parseScriptFile(filePath);
|
|
799
|
+
if (script) {
|
|
800
|
+
const doc = buildScriptDocument(script);
|
|
801
|
+
await insert(db, doc);
|
|
802
|
+
logger.debug(`Orama: Re-indexed modified script ${basename(filePath)}`);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
logger.info(`Orama: Watching for script changes in ${scriptsDirectory}`);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Search using Orama with advanced features
|
|
809
|
+
*/
|
|
810
|
+
async searchWithOrama(resourceType, action, scope, exclude, limit, offset = 0) {
|
|
811
|
+
const db = await this.getOramaDb();
|
|
812
|
+
const searchParams = {
|
|
813
|
+
term: resourceType,
|
|
814
|
+
properties: ['resourceType', 'methodName', 'description', 'searchTokens'],
|
|
815
|
+
boost: {
|
|
816
|
+
resourceType: 3,
|
|
817
|
+
searchTokens: 2.5,
|
|
818
|
+
methodName: 2,
|
|
819
|
+
description: 1,
|
|
820
|
+
},
|
|
821
|
+
tolerance: 1,
|
|
822
|
+
limit: Math.max((offset + limit) * SEARCH_RESULTS_MULTIPLIER, MIN_SEARCH_RESULTS),
|
|
823
|
+
facets: {
|
|
824
|
+
apiClass: {},
|
|
825
|
+
action: {},
|
|
826
|
+
scope: {},
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
const startTime = performance.now();
|
|
830
|
+
const searchResult = await search(db, searchParams);
|
|
831
|
+
const searchTime = performance.now() - startTime;
|
|
832
|
+
// Separate results by documentType FIRST
|
|
833
|
+
const allScriptHits = searchResult.hits.filter(hit => hit.document.documentType === 'script');
|
|
834
|
+
let methodHits = searchResult.hits.filter(hit => hit.document.documentType === 'method');
|
|
835
|
+
// Apply method-specific filters to methods only
|
|
836
|
+
if (action) {
|
|
837
|
+
const lowerAction = action.toLowerCase();
|
|
838
|
+
methodHits = methodHits.filter(hit => hit.document.action === lowerAction);
|
|
839
|
+
}
|
|
840
|
+
if (scope === 'namespaced') {
|
|
841
|
+
methodHits = methodHits.filter(hit => hit.document.scope === 'namespaced');
|
|
842
|
+
}
|
|
843
|
+
else if (scope === 'cluster') {
|
|
844
|
+
methodHits = methodHits.filter(hit => hit.document.scope === 'cluster' || hit.document.scope === 'forAllNamespaces');
|
|
845
|
+
}
|
|
846
|
+
// Apply exclusions to methods only
|
|
847
|
+
if (exclude) {
|
|
848
|
+
methodHits = methodHits.filter(hit => {
|
|
849
|
+
const doc = hit.document;
|
|
850
|
+
const hasActions = exclude.actions && exclude.actions.length > 0;
|
|
851
|
+
const hasApiClasses = exclude.apiClasses && exclude.apiClasses.length > 0;
|
|
852
|
+
if (hasActions && hasApiClasses) {
|
|
853
|
+
const matchesAction = exclude.actions.some(a => doc.action === a.toLowerCase() || doc.methodName.toLowerCase().includes(a.toLowerCase()));
|
|
854
|
+
const matchesApiClass = exclude.apiClasses.includes(doc.apiClass);
|
|
855
|
+
return !(matchesAction && matchesApiClass);
|
|
856
|
+
}
|
|
857
|
+
else if (hasActions) {
|
|
858
|
+
const matchesAction = exclude.actions.some(a => doc.action === a.toLowerCase() || doc.methodName.toLowerCase().includes(a.toLowerCase()));
|
|
859
|
+
return !matchesAction;
|
|
860
|
+
}
|
|
861
|
+
else if (hasApiClasses) {
|
|
862
|
+
return !exclude.apiClasses.includes(doc.apiClass);
|
|
863
|
+
}
|
|
864
|
+
return true;
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
// Extract facets (filter out script-related values)
|
|
868
|
+
const facets = {
|
|
869
|
+
apiClass: {},
|
|
870
|
+
action: {},
|
|
871
|
+
scope: {},
|
|
872
|
+
};
|
|
873
|
+
if (searchResult.facets) {
|
|
874
|
+
if (searchResult.facets.apiClass?.values) {
|
|
875
|
+
for (const [key, value] of Object.entries(searchResult.facets.apiClass.values)) {
|
|
876
|
+
if (key !== 'CachedScript') {
|
|
877
|
+
facets.apiClass[key] = value;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (searchResult.facets.action?.values) {
|
|
882
|
+
for (const [key, value] of Object.entries(searchResult.facets.action.values)) {
|
|
883
|
+
if (key !== 'script') {
|
|
884
|
+
facets.action[key] = value;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (searchResult.facets.scope?.values) {
|
|
889
|
+
for (const [key, value] of Object.entries(searchResult.facets.scope.values)) {
|
|
890
|
+
if (key !== 'script') {
|
|
891
|
+
facets.scope[key] = value;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// Sort methods to prioritize exact resourceType matches
|
|
897
|
+
const sortedMethodHits = methodHits.sort((a, b) => {
|
|
898
|
+
const aExact = a.document.resourceType.toLowerCase() === resourceType.toLowerCase();
|
|
899
|
+
const bExact = b.document.resourceType.toLowerCase() === resourceType.toLowerCase();
|
|
900
|
+
if (aExact && !bExact)
|
|
901
|
+
return -1;
|
|
902
|
+
if (!aExact && bExact)
|
|
903
|
+
return 1;
|
|
904
|
+
return (b.score || 0) - (a.score || 0);
|
|
905
|
+
});
|
|
906
|
+
// Sort scripts by relevance score
|
|
907
|
+
const sortedScriptHits = allScriptHits.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
908
|
+
const totalMethodCount = sortedMethodHits.length;
|
|
909
|
+
const totalScriptCount = sortedScriptHits.length;
|
|
910
|
+
return {
|
|
911
|
+
methodResults: sortedMethodHits.slice(offset, offset + limit).map(hit => hit.document),
|
|
912
|
+
scriptResults: sortedScriptHits.slice(0, MAX_RELEVANT_SCRIPTS).map(hit => hit.document),
|
|
913
|
+
totalMethodCount,
|
|
914
|
+
totalScriptCount,
|
|
915
|
+
facets,
|
|
916
|
+
searchTime,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Extract all API methods from @kubernetes/client-node
|
|
921
|
+
*/
|
|
922
|
+
extractKubernetesApiMethods() {
|
|
923
|
+
if (this.apiMethodsCache) {
|
|
924
|
+
return this.apiMethodsCache;
|
|
925
|
+
}
|
|
926
|
+
const methods = [];
|
|
927
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
928
|
+
const apiClasses = [
|
|
929
|
+
{ class: 'CoreV1Api', constructor: k8s.CoreV1Api, description: 'Core Kubernetes resources (Pods, Services, ConfigMaps, Secrets, Namespaces, Nodes, etc.)' },
|
|
930
|
+
{ class: 'AppsV1Api', constructor: k8s.AppsV1Api, description: 'Applications API (Deployments, StatefulSets, DaemonSets, ReplicaSets)' },
|
|
931
|
+
{ class: 'BatchV1Api', constructor: k8s.BatchV1Api, description: 'Batch operations (Jobs, CronJobs)' },
|
|
932
|
+
{ class: 'NetworkingV1Api', constructor: k8s.NetworkingV1Api, description: 'Networking resources (Ingresses, NetworkPolicies, IngressClasses)' },
|
|
933
|
+
{ class: 'RbacAuthorizationV1Api', constructor: k8s.RbacAuthorizationV1Api, description: 'RBAC (Roles, RoleBindings, ClusterRoles, ClusterRoleBindings, ServiceAccounts)' },
|
|
934
|
+
{ class: 'StorageV1Api', constructor: k8s.StorageV1Api, description: 'Storage resources (StorageClasses, PersistentVolumes, VolumeAttachments)' },
|
|
935
|
+
{ class: 'CustomObjectsApi', constructor: k8s.CustomObjectsApi, description: 'Custom Resource Definitions (CRDs) and custom resources' },
|
|
936
|
+
{ class: 'ApiextensionsV1Api', constructor: k8s.ApiextensionsV1Api, description: 'API extensions (CustomResourceDefinitions for discovering and managing CRDs)' },
|
|
937
|
+
{ class: 'AutoscalingV1Api', constructor: k8s.AutoscalingV1Api, description: 'Autoscaling resources (HorizontalPodAutoscalers)' },
|
|
938
|
+
{ class: 'PolicyV1Api', constructor: k8s.PolicyV1Api, description: 'Policy resources (PodDisruptionBudgets)' },
|
|
939
|
+
];
|
|
940
|
+
for (const { class: className, constructor: ApiClass, description: classDesc } of apiClasses) {
|
|
941
|
+
if (!ApiClass)
|
|
942
|
+
continue;
|
|
943
|
+
const proto = ApiClass.prototype;
|
|
944
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
945
|
+
for (const methodName of methodNames) {
|
|
946
|
+
if (methodName === 'constructor' || methodName.startsWith('_') ||
|
|
947
|
+
methodName === 'setDefaultAuthentication' || typeof proto[methodName] !== 'function') {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const resourceType = extractResourceType(methodName);
|
|
951
|
+
const description = generateDescriptionFromMethodName(methodName, classDesc);
|
|
952
|
+
const parameters = inferParameters(methodName, className);
|
|
953
|
+
const example = generateUsageExample(className, methodName, parameters);
|
|
954
|
+
const inputSchema = generateInputSchema(parameters);
|
|
955
|
+
const outputSchema = generateOutputSchema(methodName, resourceType);
|
|
956
|
+
const typeDefinitionFile = `node_modules/@kubernetes/client-node/dist/gen/apis/${className}.d.ts`;
|
|
957
|
+
const typeDefinitions = extractMethodTypeDefinitions(className, methodName, resourceType);
|
|
958
|
+
methods.push({
|
|
959
|
+
apiClass: className,
|
|
960
|
+
methodName,
|
|
961
|
+
resourceType,
|
|
962
|
+
description,
|
|
963
|
+
parameters,
|
|
964
|
+
returnType: 'Promise<any>',
|
|
965
|
+
example,
|
|
966
|
+
typeDefinitionFile,
|
|
967
|
+
inputSchema,
|
|
968
|
+
outputSchema,
|
|
969
|
+
typeDefinitions: Object.keys(typeDefinitions).length > 0 ? typeDefinitions : undefined,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
this.apiMethodsCache = methods;
|
|
974
|
+
logger.info(`Indexed ${methods.length} Kubernetes API methods`);
|
|
975
|
+
return methods;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// Export singleton for production use
|
|
979
|
+
export const searchToolsService = new SearchToolsService();
|
|
980
|
+
// Export class for testing
|
|
981
|
+
export { SearchToolsService };
|
|
982
|
+
// ============================================================================
|
|
983
|
+
// Orama Search Engine Configuration
|
|
984
|
+
// ============================================================================
|
|
985
|
+
/**
|
|
986
|
+
* Orama schema for Kubernetes API methods and Prometheus library methods
|
|
987
|
+
*
|
|
988
|
+
* Design decisions based on Orama best practices:
|
|
989
|
+
* - `string` types for full-text searchable fields (resourceType, methodName, description)
|
|
990
|
+
* - `enum` types for exact-match filterable fields (action, scope, apiClass)
|
|
991
|
+
* - stemmerSkipProperties for code identifiers that shouldn't be stemmed
|
|
992
|
+
* - Boosting configured at search time for relevance tuning
|
|
993
|
+
*/
|
|
994
|
+
const oramaSchema = {
|
|
995
|
+
// Document type discriminator
|
|
996
|
+
documentType: 'enum', // "method" | "script" | "prometheus" | "prometheus-metric"
|
|
997
|
+
// Full-text searchable fields
|
|
998
|
+
resourceType: 'string', // "Pod", "Deployment" - boosted 3x
|
|
999
|
+
methodName: 'string', // "listNamespacedPod" or script filename - boosted 2x
|
|
1000
|
+
description: 'string', // Full description text - boosted 1x
|
|
1001
|
+
// Enhanced search field: CamelCase split for better matching
|
|
1002
|
+
// e.g., "PodExec" becomes "Pod Exec", "ServiceAccountToken" becomes "Service Account Token"
|
|
1003
|
+
searchTokens: 'string',
|
|
1004
|
+
// Filterable enum fields (exact match, used in where clause)
|
|
1005
|
+
action: 'enum', // "list", "create", "read", "delete", "patch", "replace", "connect", "watch", "script", "prometheus"
|
|
1006
|
+
scope: 'enum', // "namespaced", "cluster", "forAllNamespaces", "script", "prometheus"
|
|
1007
|
+
apiClass: 'enum', // "CoreV1Api", "AppsV1Api", "CachedScript", "prometheus-query"
|
|
1008
|
+
// Stored metadata
|
|
1009
|
+
id: 'string', // Unique identifier: apiClass.methodName or script:filename
|
|
1010
|
+
filePath: 'string', // Full path for scripts (empty for methods)
|
|
1011
|
+
// Prometheus-specific fields
|
|
1012
|
+
library: 'enum', // "prometheus-query" (empty for non-prometheus)
|
|
1013
|
+
category: 'enum', // "query" | "metadata" | "alerts" (empty for non-prometheus)
|
|
1014
|
+
// Prometheus metric fields (for prometheus-metric documentType)
|
|
1015
|
+
metricType: 'enum', // "gauge" | "counter" | "histogram" | "summary" | "unknown"
|
|
1016
|
+
};
|
|
1017
|
+
/**
|
|
1018
|
+
* Extract the action from a method name
|
|
1019
|
+
*/
|
|
1020
|
+
function extractAction(methodName) {
|
|
1021
|
+
const lowerMethod = methodName.toLowerCase();
|
|
1022
|
+
const actions = ['list', 'read', 'create', 'delete', 'patch', 'replace', 'connect', 'watch', 'get'];
|
|
1023
|
+
for (const action of actions) {
|
|
1024
|
+
if (lowerMethod.startsWith(action)) {
|
|
1025
|
+
return action;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return 'unknown';
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Extract the scope from a method name
|
|
1032
|
+
*/
|
|
1033
|
+
function extractScope(methodName) {
|
|
1034
|
+
const lowerMethod = methodName.toLowerCase();
|
|
1035
|
+
if (lowerMethod.includes('forallnamespaces')) {
|
|
1036
|
+
return 'forAllNamespaces';
|
|
1037
|
+
}
|
|
1038
|
+
if (lowerMethod.includes('namespaced')) {
|
|
1039
|
+
return 'namespaced';
|
|
1040
|
+
}
|
|
1041
|
+
return 'cluster';
|
|
1042
|
+
}
|
|
1043
|
+
// ============================================================================
|
|
1044
|
+
// Prometheus Library Methods (Dynamic Extraction from .d.ts files)
|
|
1045
|
+
// ============================================================================
|
|
1046
|
+
/**
|
|
1047
|
+
* Extract JSDoc comment text from a node using TypeScript AST
|
|
1048
|
+
*/
|
|
1049
|
+
function extractJSDocComment(node, _sourceFile) {
|
|
1050
|
+
const jsDocComments = ts.getJSDocCommentsAndTags(node);
|
|
1051
|
+
for (const comment of jsDocComments) {
|
|
1052
|
+
if (ts.isJSDoc(comment) && comment.comment) {
|
|
1053
|
+
if (typeof comment.comment === 'string') {
|
|
1054
|
+
return comment.comment;
|
|
1055
|
+
}
|
|
1056
|
+
// Handle JSDocComment array (multiple parts)
|
|
1057
|
+
if (Array.isArray(comment.comment)) {
|
|
1058
|
+
return comment.comment
|
|
1059
|
+
.map(part => typeof part === 'string' ? part : part.text)
|
|
1060
|
+
.join('')
|
|
1061
|
+
.trim();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return '';
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Extract parameter info from TypeScript function parameters
|
|
1069
|
+
*/
|
|
1070
|
+
function extractParameterInfo(params, sourceFile) {
|
|
1071
|
+
return params.map(param => {
|
|
1072
|
+
const name = param.name.getText(sourceFile);
|
|
1073
|
+
const type = param.type?.getText(sourceFile) || 'any';
|
|
1074
|
+
const optional = !!param.questionToken || !!param.initializer;
|
|
1075
|
+
return { name, type, optional };
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Determine category for a prometheus-query method based on its name
|
|
1080
|
+
*/
|
|
1081
|
+
function categorizePrometheusQueryMethod(methodName, _description) {
|
|
1082
|
+
const lowerName = methodName.toLowerCase();
|
|
1083
|
+
if (lowerName.includes('query') || lowerName === 'instantquery' || lowerName === 'rangequery') {
|
|
1084
|
+
return 'query';
|
|
1085
|
+
}
|
|
1086
|
+
if (lowerName.includes('alert') || lowerName.includes('rule')) {
|
|
1087
|
+
return 'alerts';
|
|
1088
|
+
}
|
|
1089
|
+
return 'metadata';
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Generate example code for a prometheus-query method
|
|
1093
|
+
*/
|
|
1094
|
+
function generatePrometheusQueryExample(methodName, params) {
|
|
1095
|
+
const requiredParams = params.filter(p => !p.optional);
|
|
1096
|
+
const paramExamples = [];
|
|
1097
|
+
for (const p of requiredParams) {
|
|
1098
|
+
switch (p.name) {
|
|
1099
|
+
case 'query':
|
|
1100
|
+
paramExamples.push("'up{job=\"prometheus\"}'");
|
|
1101
|
+
break;
|
|
1102
|
+
case 'time':
|
|
1103
|
+
case 'start':
|
|
1104
|
+
paramExamples.push('new Date(Date.now() - 3600000)');
|
|
1105
|
+
break;
|
|
1106
|
+
case 'end':
|
|
1107
|
+
paramExamples.push('new Date()');
|
|
1108
|
+
break;
|
|
1109
|
+
case 'step':
|
|
1110
|
+
paramExamples.push("'1m'");
|
|
1111
|
+
break;
|
|
1112
|
+
case 'matchs':
|
|
1113
|
+
case 'match':
|
|
1114
|
+
paramExamples.push("['{job=\"prometheus\"}']");
|
|
1115
|
+
break;
|
|
1116
|
+
case 'labelName':
|
|
1117
|
+
paramExamples.push("'job'");
|
|
1118
|
+
break;
|
|
1119
|
+
default:
|
|
1120
|
+
paramExamples.push(`/* ${p.name} */`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return `import { PrometheusDriver } from 'prometheus-query';
|
|
1124
|
+
|
|
1125
|
+
const prom = new PrometheusDriver({ endpoint: process.env.PROMETHEUS_URL || 'http://prometheus:9090' });
|
|
1126
|
+
const result = await prom.${methodName}(${paramExamples.join(', ')});
|
|
1127
|
+
console.log(result);`;
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Dynamically extract methods from prometheus-query library .d.ts files
|
|
1131
|
+
*/
|
|
1132
|
+
function extractPrometheusQueryMethods() {
|
|
1133
|
+
const methods = [];
|
|
1134
|
+
try {
|
|
1135
|
+
const driverPath = join(process.cwd(), 'node_modules', 'prometheus-query', 'dist', 'driver.d.ts');
|
|
1136
|
+
if (!existsSync(driverPath)) {
|
|
1137
|
+
logger.debug('prometheus-query driver.d.ts not found');
|
|
1138
|
+
return methods;
|
|
1139
|
+
}
|
|
1140
|
+
const sourceCode = readFileSync(driverPath, 'utf-8');
|
|
1141
|
+
const sourceFile = ts.createSourceFile(driverPath, sourceCode, ts.ScriptTarget.Latest, true);
|
|
1142
|
+
function visit(node) {
|
|
1143
|
+
if (ts.isClassDeclaration(node) && node.name?.text === 'PrometheusDriver') {
|
|
1144
|
+
for (const member of node.members) {
|
|
1145
|
+
if (ts.isMethodDeclaration(member) && member.name) {
|
|
1146
|
+
const methodName = member.name.getText(sourceFile);
|
|
1147
|
+
if (methodName.startsWith('_') || methodName === 'constructor' ||
|
|
1148
|
+
member.modifiers?.some(m => m.kind === ts.SyntaxKind.PrivateKeyword)) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
const description = extractJSDocComment(member, sourceFile) ||
|
|
1152
|
+
`${methodName.charAt(0).toUpperCase() + methodName.slice(1).replace(/([A-Z])/g, ' $1').trim()} from Prometheus API`;
|
|
1153
|
+
const params = extractParameterInfo(member.parameters, sourceFile);
|
|
1154
|
+
const returnType = member.type?.getText(sourceFile) || 'Promise<any>';
|
|
1155
|
+
const category = categorizePrometheusQueryMethod(methodName, description);
|
|
1156
|
+
const example = generatePrometheusQueryExample(methodName, params);
|
|
1157
|
+
methods.push({
|
|
1158
|
+
library: 'prometheus-query',
|
|
1159
|
+
className: 'PrometheusDriver',
|
|
1160
|
+
methodName,
|
|
1161
|
+
category,
|
|
1162
|
+
description,
|
|
1163
|
+
parameters: params,
|
|
1164
|
+
returnType,
|
|
1165
|
+
example,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
ts.forEachChild(node, visit);
|
|
1171
|
+
}
|
|
1172
|
+
visit(sourceFile);
|
|
1173
|
+
logger.debug(`Extracted ${methods.length} methods from prometheus-query`);
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
logger.debug(`Failed to extract prometheus-query methods: ${error instanceof Error ? error.message : String(error)}`);
|
|
1177
|
+
}
|
|
1178
|
+
return methods;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Get all Prometheus library methods (dynamically extracted from .d.ts files)
|
|
1182
|
+
*/
|
|
1183
|
+
function getAllPrometheusMethods() {
|
|
1184
|
+
const startTime = Date.now();
|
|
1185
|
+
const methods = extractPrometheusQueryMethods();
|
|
1186
|
+
const elapsed = Date.now() - startTime;
|
|
1187
|
+
logger.info(`Dynamically extracted ${methods.length} prometheus-query methods in ${elapsed}ms`);
|
|
1188
|
+
return methods;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Prometheus methods cache (populated at service initialization)
|
|
1192
|
+
*/
|
|
1193
|
+
let prometheusMethodsCache = null;
|
|
1194
|
+
/**
|
|
1195
|
+
* Get cached Prometheus methods
|
|
1196
|
+
*/
|
|
1197
|
+
function getPrometheusMethods() {
|
|
1198
|
+
if (!prometheusMethodsCache) {
|
|
1199
|
+
prometheusMethodsCache = getAllPrometheusMethods();
|
|
1200
|
+
}
|
|
1201
|
+
return prometheusMethodsCache;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Clear the prometheus methods cache (used during shutdown/reset)
|
|
1205
|
+
*/
|
|
1206
|
+
function clearPrometheusMethodsCache() {
|
|
1207
|
+
prometheusMethodsCache = null;
|
|
1208
|
+
}
|
|
1209
|
+
// ============================================================================
|
|
1210
|
+
// Script Parsing Functions
|
|
1211
|
+
// ============================================================================
|
|
1212
|
+
/**
|
|
1213
|
+
* Extract the first comment block from a TypeScript file using TypeScript AST.
|
|
1214
|
+
* Supports block comments and consecutive single-line comments.
|
|
1215
|
+
*/
|
|
1216
|
+
function extractFirstCommentBlock(filePath) {
|
|
1217
|
+
try {
|
|
1218
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1219
|
+
// Get leading comments from the start of the file using TypeScript's comment parser
|
|
1220
|
+
const leadingComments = ts.getLeadingCommentRanges(content, 0);
|
|
1221
|
+
if (!leadingComments || leadingComments.length === 0) {
|
|
1222
|
+
return '';
|
|
1223
|
+
}
|
|
1224
|
+
// Collect all consecutive comments at the start
|
|
1225
|
+
const commentTexts = [];
|
|
1226
|
+
for (const comment of leadingComments) {
|
|
1227
|
+
const commentText = content.slice(comment.pos, comment.end);
|
|
1228
|
+
if (comment.kind === ts.SyntaxKind.MultiLineCommentTrivia) {
|
|
1229
|
+
// Block comment - extract content between /* and */
|
|
1230
|
+
const inner = commentText.slice(2, -2); // Remove /* and */
|
|
1231
|
+
const lines = inner.split('\n');
|
|
1232
|
+
for (const line of lines) {
|
|
1233
|
+
// Remove leading asterisks and whitespace
|
|
1234
|
+
let cleaned = line.trim();
|
|
1235
|
+
if (cleaned.startsWith('*')) {
|
|
1236
|
+
cleaned = cleaned.slice(1).trim();
|
|
1237
|
+
}
|
|
1238
|
+
if (cleaned.length > 0) {
|
|
1239
|
+
commentTexts.push(cleaned);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
else if (comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) {
|
|
1244
|
+
// Single-line comment - remove leading //
|
|
1245
|
+
const cleaned = commentText.slice(2).trim();
|
|
1246
|
+
if (cleaned.length > 0) {
|
|
1247
|
+
commentTexts.push(cleaned);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return commentTexts.join(' ').trim();
|
|
1252
|
+
}
|
|
1253
|
+
catch (error) {
|
|
1254
|
+
logger.debug(`Failed to extract comment from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1255
|
+
return '';
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Extract likely resource types from a script filename.
|
|
1260
|
+
* Examples:
|
|
1261
|
+
* "get-pod-logs.ts" -> ["pod", "log", "logs"]
|
|
1262
|
+
* "list-nodes.ts" -> ["node", "nodes"]
|
|
1263
|
+
*/
|
|
1264
|
+
function extractResourceTypesFromFilename(filename) {
|
|
1265
|
+
// Remove extension
|
|
1266
|
+
const baseName = filename.replace(/\.ts$/, '');
|
|
1267
|
+
// Split by common separators and filter out action words
|
|
1268
|
+
const parts = baseName
|
|
1269
|
+
.split(/[-_]/)
|
|
1270
|
+
.filter(part => part.length > 0)
|
|
1271
|
+
.filter(part => !['get', 'list', 'create', 'delete', 'update', 'patch', 'watch'].includes(part.toLowerCase()));
|
|
1272
|
+
// Add singular/plural variants
|
|
1273
|
+
const resourceTypes = [];
|
|
1274
|
+
for (const part of parts) {
|
|
1275
|
+
resourceTypes.push(part.toLowerCase());
|
|
1276
|
+
// Add singular if plural
|
|
1277
|
+
if (part.endsWith('s') && part.length > 2) {
|
|
1278
|
+
resourceTypes.push(part.slice(0, -1).toLowerCase());
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return [...new Set(resourceTypes)];
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Extract K8s API signals from script content using TypeScript AST.
|
|
1285
|
+
* Extracts API class references and resource type references.
|
|
1286
|
+
*/
|
|
1287
|
+
function extractApiSignals(filePath) {
|
|
1288
|
+
try {
|
|
1289
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1290
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
1291
|
+
const apiClasses = new Set();
|
|
1292
|
+
const resourceTypes = new Set();
|
|
1293
|
+
// Known K8s API class names
|
|
1294
|
+
const knownApiClasses = new Set([
|
|
1295
|
+
'CoreV1Api', 'AppsV1Api', 'BatchV1Api', 'NetworkingV1Api',
|
|
1296
|
+
'RbacAuthorizationV1Api', 'StorageV1Api', 'CustomObjectsApi',
|
|
1297
|
+
'ApiextensionsV1Api', 'AutoscalingV1Api', 'PolicyV1Api',
|
|
1298
|
+
]);
|
|
1299
|
+
function visit(node) {
|
|
1300
|
+
// Find type references (V1Pod, V1Deployment, etc.)
|
|
1301
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
1302
|
+
const typeName = node.typeName.getText(sourceFile);
|
|
1303
|
+
// K8s types start with V followed by version number
|
|
1304
|
+
if (typeName.startsWith('V') && typeName.length > 2) {
|
|
1305
|
+
const secondChar = typeName.charAt(1);
|
|
1306
|
+
if (secondChar >= '0' && secondChar <= '9') {
|
|
1307
|
+
// Filter out Api and List types
|
|
1308
|
+
if (!typeName.includes('Api') && !typeName.includes('List') && typeName.length < 30) {
|
|
1309
|
+
resourceTypes.add(typeName);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
// Find identifier references to API classes
|
|
1315
|
+
if (ts.isIdentifier(node)) {
|
|
1316
|
+
const name = node.text;
|
|
1317
|
+
if (knownApiClasses.has(name)) {
|
|
1318
|
+
apiClasses.add(name);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
// Find property access like k8s.CoreV1Api
|
|
1322
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1323
|
+
const propName = node.name.text;
|
|
1324
|
+
if (knownApiClasses.has(propName)) {
|
|
1325
|
+
apiClasses.add(propName);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
ts.forEachChild(node, visit);
|
|
1329
|
+
}
|
|
1330
|
+
visit(sourceFile);
|
|
1331
|
+
return {
|
|
1332
|
+
apiClasses: [...apiClasses],
|
|
1333
|
+
resourceTypes: [...resourceTypes].slice(0, MAX_RESOURCE_TYPES_FROM_CONTENT),
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
logger.debug(`Failed to extract API signals from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1338
|
+
return { apiClasses: [], resourceTypes: [] };
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Parse a cached script file and extract searchable metadata.
|
|
1343
|
+
*/
|
|
1344
|
+
function parseScriptFile(filePath) {
|
|
1345
|
+
try {
|
|
1346
|
+
const filename = basename(filePath);
|
|
1347
|
+
const description = extractFirstCommentBlock(filePath);
|
|
1348
|
+
const filenameResourceTypes = extractResourceTypesFromFilename(filename);
|
|
1349
|
+
const { apiClasses, resourceTypes: contentResourceTypes } = extractApiSignals(filePath);
|
|
1350
|
+
// Combine resource types from filename and content
|
|
1351
|
+
const resourceTypes = [...new Set([...filenameResourceTypes, ...contentResourceTypes.map(t => t.toLowerCase())])];
|
|
1352
|
+
// Extract additional keywords from description
|
|
1353
|
+
const keywords = description
|
|
1354
|
+
.toLowerCase()
|
|
1355
|
+
.split(/\s+/)
|
|
1356
|
+
.filter(word => word.length > 2)
|
|
1357
|
+
.filter(word => !['the', 'and', 'for', 'from', 'with', 'this', 'that'].includes(word));
|
|
1358
|
+
return {
|
|
1359
|
+
filename,
|
|
1360
|
+
filePath,
|
|
1361
|
+
description: description || `Script: ${filename.replace(/\.ts$/, '')}`,
|
|
1362
|
+
resourceTypes,
|
|
1363
|
+
apiClasses,
|
|
1364
|
+
keywords,
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
logger.debug(`Failed to parse script ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Build an Orama document from a CachedScript
|
|
1374
|
+
*/
|
|
1375
|
+
function buildScriptDocument(script) {
|
|
1376
|
+
// Build search tokens from filename, description, and API signals
|
|
1377
|
+
const searchTokens = [
|
|
1378
|
+
script.filename.replace(/\.ts$/, '').replace(/[-_]/g, ' '),
|
|
1379
|
+
...script.resourceTypes,
|
|
1380
|
+
script.description,
|
|
1381
|
+
...script.apiClasses,
|
|
1382
|
+
...script.keywords,
|
|
1383
|
+
].join(' ');
|
|
1384
|
+
return {
|
|
1385
|
+
id: `script:${script.filename}`,
|
|
1386
|
+
documentType: 'script',
|
|
1387
|
+
resourceType: script.resourceTypes.join(' '),
|
|
1388
|
+
methodName: script.filename.replace(/\.ts$/, ''),
|
|
1389
|
+
description: script.description,
|
|
1390
|
+
searchTokens,
|
|
1391
|
+
action: 'script',
|
|
1392
|
+
scope: 'script',
|
|
1393
|
+
apiClass: script.apiClasses.length > 0 ? script.apiClasses[0] : 'CachedScript',
|
|
1394
|
+
filePath: script.filePath,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
// ============================================================================
|
|
1398
|
+
// End Script Parsing Functions
|
|
1399
|
+
// ============================================================================
|
|
45
1400
|
/**
|
|
46
1401
|
* Initialize scripts directory with node_modules symlink for package resolution
|
|
47
1402
|
*/
|
|
@@ -51,25 +1406,38 @@ function initializeScriptsDirectory(scriptsDir) {
|
|
|
51
1406
|
if (!existsSync(scriptsDir)) {
|
|
52
1407
|
mkdirSync(scriptsDir, { recursive: true });
|
|
53
1408
|
}
|
|
54
|
-
// Create symlink to node_modules
|
|
55
|
-
// When installed via npx
|
|
56
|
-
//
|
|
57
|
-
//
|
|
1409
|
+
// Create symlink to node_modules
|
|
1410
|
+
// When installed via npx: PACKAGE_ROOT = /path/npx/node_modules/@prodisco/k8s-mcp
|
|
1411
|
+
// -> dependencies are in: /path/npx/node_modules (go up 2 levels)
|
|
1412
|
+
// When running in dev: PACKAGE_ROOT = /path/to/project
|
|
1413
|
+
// -> dependencies are in: /path/to/project/node_modules
|
|
58
1414
|
const nodeModulesLink = join(scriptsDir, 'node_modules');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1415
|
+
// Detect if running from npx cache (path contains node_modules/@prodisco)
|
|
1416
|
+
const isNpxInstall = PACKAGE_ROOT.includes('node_modules/@prodisco') ||
|
|
1417
|
+
PACKAGE_ROOT.includes('node_modules\\@prodisco');
|
|
1418
|
+
const nodeModulesTarget = isNpxInstall
|
|
1419
|
+
? realpathSync(join(PACKAGE_ROOT, '../..')) // npx: go up from node_modules/@prodisco/k8s-mcp
|
|
1420
|
+
: realpathSync(join(PACKAGE_ROOT, 'node_modules')); // dev: use project's node_modules
|
|
1421
|
+
if (!existsSync(nodeModulesTarget)) {
|
|
1422
|
+
logger.warn(`node_modules target does not exist: ${nodeModulesTarget}`);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
// Always remove existing symlink and recreate to ensure it points to current location
|
|
1426
|
+
try {
|
|
1427
|
+
unlinkSync(nodeModulesLink);
|
|
1428
|
+
}
|
|
1429
|
+
catch {
|
|
1430
|
+
// Ignore - doesn't exist
|
|
1431
|
+
}
|
|
1432
|
+
try {
|
|
1433
|
+
symlinkSync(nodeModulesTarget, nodeModulesLink, 'dir');
|
|
1434
|
+
}
|
|
1435
|
+
catch (err) {
|
|
1436
|
+
logger.warn(`Could not create symlink to node_modules: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
1437
|
}
|
|
69
1438
|
}
|
|
70
1439
|
catch (err) {
|
|
71
|
-
|
|
72
|
-
console.error('Could not initialize scripts directory:', err);
|
|
1440
|
+
logger.warn(`Could not initialize scripts directory: ${err instanceof Error ? err.message : String(err)}`);
|
|
73
1441
|
}
|
|
74
1442
|
}
|
|
75
1443
|
/**
|
|
@@ -133,64 +1501,6 @@ function extractMethodTypeDefinitions(apiClass, methodName, resourceType) {
|
|
|
133
1501
|
}
|
|
134
1502
|
return result;
|
|
135
1503
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Extract all API methods from @kubernetes/client-node
|
|
138
|
-
*/
|
|
139
|
-
function extractKubernetesApiMethods() {
|
|
140
|
-
if (apiMethodsCache) {
|
|
141
|
-
return apiMethodsCache;
|
|
142
|
-
}
|
|
143
|
-
const methods = [];
|
|
144
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
145
|
-
const apiClasses = [
|
|
146
|
-
{ class: 'CoreV1Api', constructor: k8s.CoreV1Api, description: 'Core Kubernetes resources (Pods, Services, ConfigMaps, Secrets, Namespaces, Nodes, etc.)' },
|
|
147
|
-
{ class: 'AppsV1Api', constructor: k8s.AppsV1Api, description: 'Applications API (Deployments, StatefulSets, DaemonSets, ReplicaSets)' },
|
|
148
|
-
{ class: 'BatchV1Api', constructor: k8s.BatchV1Api, description: 'Batch operations (Jobs, CronJobs)' },
|
|
149
|
-
{ class: 'NetworkingV1Api', constructor: k8s.NetworkingV1Api, description: 'Networking resources (Ingresses, NetworkPolicies, IngressClasses)' },
|
|
150
|
-
{ class: 'RbacAuthorizationV1Api', constructor: k8s.RbacAuthorizationV1Api, description: 'RBAC (Roles, RoleBindings, ClusterRoles, ClusterRoleBindings, ServiceAccounts)' },
|
|
151
|
-
{ class: 'StorageV1Api', constructor: k8s.StorageV1Api, description: 'Storage resources (StorageClasses, PersistentVolumes, VolumeAttachments)' },
|
|
152
|
-
{ class: 'CustomObjectsApi', constructor: k8s.CustomObjectsApi, description: 'Custom Resource Definitions (CRDs) and custom resources' },
|
|
153
|
-
{ class: 'ApiextensionsV1Api', constructor: k8s.ApiextensionsV1Api, description: 'API extensions (CustomResourceDefinitions for discovering and managing CRDs)' },
|
|
154
|
-
{ class: 'AutoscalingV1Api', constructor: k8s.AutoscalingV1Api, description: 'Autoscaling resources (HorizontalPodAutoscalers)' },
|
|
155
|
-
{ class: 'PolicyV1Api', constructor: k8s.PolicyV1Api, description: 'Policy resources (PodDisruptionBudgets)' },
|
|
156
|
-
];
|
|
157
|
-
for (const { class: className, constructor: ApiClass, description: classDesc } of apiClasses) {
|
|
158
|
-
if (!ApiClass)
|
|
159
|
-
continue;
|
|
160
|
-
const proto = ApiClass.prototype;
|
|
161
|
-
const methodNames = Object.getOwnPropertyNames(proto);
|
|
162
|
-
for (const methodName of methodNames) {
|
|
163
|
-
if (methodName === 'constructor' || methodName.startsWith('_') ||
|
|
164
|
-
methodName === 'setDefaultAuthentication' || typeof proto[methodName] !== 'function') {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const resourceType = extractResourceType(methodName);
|
|
168
|
-
const description = generateDescriptionFromMethodName(methodName, className, classDesc);
|
|
169
|
-
const parameters = inferParameters(methodName, className);
|
|
170
|
-
const example = generateUsageExample(className, methodName, parameters);
|
|
171
|
-
const inputSchema = generateInputSchema(methodName, parameters);
|
|
172
|
-
const outputSchema = generateOutputSchema(methodName, resourceType);
|
|
173
|
-
const typeDefinitionFile = `node_modules/@kubernetes/client-node/dist/gen/apis/${className}.d.ts`;
|
|
174
|
-
const typeDefinitions = extractMethodTypeDefinitions(className, methodName, resourceType);
|
|
175
|
-
methods.push({
|
|
176
|
-
apiClass: className,
|
|
177
|
-
methodName,
|
|
178
|
-
resourceType,
|
|
179
|
-
description,
|
|
180
|
-
parameters,
|
|
181
|
-
returnType: 'Promise<any>',
|
|
182
|
-
example,
|
|
183
|
-
typeDefinitionFile,
|
|
184
|
-
inputSchema,
|
|
185
|
-
outputSchema,
|
|
186
|
-
typeDefinitions: Object.keys(typeDefinitions).length > 0 ? typeDefinitions : undefined,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
apiMethodsCache = methods;
|
|
191
|
-
console.error(`Indexed ${methods.length} Kubernetes API methods`);
|
|
192
|
-
return methods;
|
|
193
|
-
}
|
|
194
1504
|
function extractResourceType(methodName) {
|
|
195
1505
|
let resource = methodName
|
|
196
1506
|
.replace(/^(list|read|create|delete|patch|replace|connect|get|watch)/, '')
|
|
@@ -203,7 +1513,7 @@ function extractResourceType(methodName) {
|
|
|
203
1513
|
}
|
|
204
1514
|
return resource || 'Resource';
|
|
205
1515
|
}
|
|
206
|
-
function generateDescriptionFromMethodName(methodName,
|
|
1516
|
+
function generateDescriptionFromMethodName(methodName, classDesc) {
|
|
207
1517
|
const words = methodName.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
|
|
208
1518
|
const resourceMatch = methodName.match(/(?:list|read|create|delete|patch|replace)(?:Namespaced)?(.+?)(?:ForAllNamespaces)?$/);
|
|
209
1519
|
const resource = resourceMatch ? resourceMatch[1] : '';
|
|
@@ -257,7 +1567,7 @@ function inferParameters(methodName, apiClass) {
|
|
|
257
1567
|
}
|
|
258
1568
|
return parameters;
|
|
259
1569
|
}
|
|
260
|
-
function generateInputSchema(
|
|
1570
|
+
function generateInputSchema(parameters) {
|
|
261
1571
|
const properties = {};
|
|
262
1572
|
const required = [];
|
|
263
1573
|
for (const param of parameters) {
|
|
@@ -345,220 +1655,695 @@ function generateUsageExample(apiClass, methodName, parameters) {
|
|
|
345
1655
|
example += `\n}\n\n// Execute the function\nmain();`;
|
|
346
1656
|
return example;
|
|
347
1657
|
}
|
|
1658
|
+
// ============================================================================
|
|
1659
|
+
// Execute Functions for Each Mode
|
|
1660
|
+
// ============================================================================
|
|
348
1661
|
/**
|
|
349
|
-
*
|
|
1662
|
+
* Execute type definition lookup mode
|
|
350
1663
|
*/
|
|
351
|
-
function
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
1664
|
+
async function executeTypeMode(input) {
|
|
1665
|
+
const { types, depth = 1 } = input;
|
|
1666
|
+
if (!types || types.length === 0) {
|
|
1667
|
+
return {
|
|
1668
|
+
mode: 'types',
|
|
1669
|
+
summary: 'Error: types parameter is required when mode is "types"',
|
|
1670
|
+
types: {},
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
const basePath = process.cwd();
|
|
1674
|
+
const results = {};
|
|
1675
|
+
const typesToProcess = new Set(types);
|
|
1676
|
+
const processedTypes = new Set();
|
|
1677
|
+
let currentDepth = 0;
|
|
1678
|
+
while (typesToProcess.size > 0 && currentDepth < depth) {
|
|
1679
|
+
const currentBatch = Array.from(typesToProcess);
|
|
1680
|
+
typesToProcess.clear();
|
|
1681
|
+
for (const typePath of currentBatch) {
|
|
1682
|
+
if (processedTypes.has(typePath)) {
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
processedTypes.add(typePath);
|
|
1686
|
+
const parsedPath = parseTypePath(typePath);
|
|
1687
|
+
if (!parsedPath) {
|
|
1688
|
+
results[typePath] = {
|
|
1689
|
+
name: typePath,
|
|
1690
|
+
definition: `// Invalid type path: ${typePath}`,
|
|
1691
|
+
file: 'error',
|
|
1692
|
+
nestedTypes: [],
|
|
1693
|
+
};
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
const { baseType, path: propertyPath } = parsedPath;
|
|
1697
|
+
if (propertyPath.length > 0) {
|
|
1698
|
+
const cache = new Map();
|
|
1699
|
+
const subtypeInfo = getSubtypeInfo(baseType, propertyPath, basePath, cache);
|
|
1700
|
+
if (subtypeInfo) {
|
|
1701
|
+
const definition = formatTypeInfo(subtypeInfo.typeInfo);
|
|
1702
|
+
results[typePath] = {
|
|
1703
|
+
name: subtypeInfo.typeInfo.name,
|
|
1704
|
+
definition,
|
|
1705
|
+
file: findTypeDefinitionFile(subtypeInfo.originalType, basePath)?.replace(basePath, '.') || 'resolved',
|
|
1706
|
+
nestedTypes: [],
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
results[typePath] = {
|
|
1711
|
+
name: typePath,
|
|
1712
|
+
definition: `// Could not resolve property path: ${typePath}`,
|
|
1713
|
+
file: 'not found',
|
|
1714
|
+
nestedTypes: [],
|
|
1715
|
+
};
|
|
375
1716
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1717
|
+
}
|
|
1718
|
+
else {
|
|
1719
|
+
const filePath = findTypeDefinitionFile(baseType, basePath);
|
|
1720
|
+
if (filePath) {
|
|
1721
|
+
try {
|
|
1722
|
+
const extracted = extractTypeDefinitionWithTS(baseType, filePath);
|
|
1723
|
+
if (extracted) {
|
|
1724
|
+
const definition = formatTypeInfo(extracted.typeInfo);
|
|
1725
|
+
results[typePath] = {
|
|
1726
|
+
name: baseType,
|
|
1727
|
+
definition,
|
|
1728
|
+
file: filePath.replace(basePath, '.'),
|
|
1729
|
+
nestedTypes: extracted.nestedTypes,
|
|
1730
|
+
};
|
|
1731
|
+
if (currentDepth < depth - 1) {
|
|
1732
|
+
for (const nestedType of extracted.nestedTypes) {
|
|
1733
|
+
if (!processedTypes.has(nestedType)) {
|
|
1734
|
+
typesToProcess.add(nestedType);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
else {
|
|
1740
|
+
results[typePath] = {
|
|
1741
|
+
name: baseType,
|
|
1742
|
+
definition: `// Type ${baseType} not found in file ${filePath}`,
|
|
1743
|
+
file: filePath.replace(basePath, '.'),
|
|
1744
|
+
nestedTypes: [],
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
catch (error) {
|
|
1749
|
+
results[typePath] = {
|
|
1750
|
+
name: baseType,
|
|
1751
|
+
definition: `// Error extracting type ${baseType}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1752
|
+
file: filePath.replace(basePath, '.'),
|
|
1753
|
+
nestedTypes: [],
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
379
1756
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
1757
|
+
else {
|
|
1758
|
+
results[typePath] = {
|
|
1759
|
+
name: baseType,
|
|
1760
|
+
definition: `// Type ${baseType} not found in @kubernetes/client-node type definitions`,
|
|
1761
|
+
file: 'not found',
|
|
1762
|
+
nestedTypes: [],
|
|
1763
|
+
};
|
|
383
1764
|
}
|
|
384
1765
|
}
|
|
385
1766
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1767
|
+
currentDepth++;
|
|
1768
|
+
}
|
|
1769
|
+
const foundCount = Object.values(results).filter(r => r.file !== 'not found').length;
|
|
1770
|
+
const totalTypes = Object.keys(results).length;
|
|
1771
|
+
let summary = `Fetched ${foundCount} type definition(s)`;
|
|
1772
|
+
if (totalTypes > types.length) {
|
|
1773
|
+
summary += ` (${types.length} requested, ${totalTypes - types.length} nested)\n\n`;
|
|
1774
|
+
}
|
|
1775
|
+
else {
|
|
1776
|
+
summary += `\n\n`;
|
|
1777
|
+
}
|
|
1778
|
+
for (const typeName of types) {
|
|
1779
|
+
const typeInfo = results[typeName];
|
|
1780
|
+
if (typeInfo && typeInfo.file !== 'not found') {
|
|
1781
|
+
summary += `${typeName}: ${typeInfo.nestedTypes.length} nested type(s)\n`;
|
|
395
1782
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
1783
|
+
}
|
|
1784
|
+
return {
|
|
1785
|
+
mode: 'types',
|
|
1786
|
+
summary,
|
|
1787
|
+
types: results,
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Execute method search mode
|
|
1792
|
+
*/
|
|
1793
|
+
async function executeMethodMode(input) {
|
|
1794
|
+
const { resourceType, action, scope = 'all', exclude, limit = 10, offset = 0 } = input;
|
|
1795
|
+
if (!resourceType) {
|
|
1796
|
+
return {
|
|
1797
|
+
mode: 'methods',
|
|
1798
|
+
summary: 'Error: resourceType parameter is required when mode is "methods"',
|
|
1799
|
+
tools: [],
|
|
1800
|
+
totalMatches: 0,
|
|
1801
|
+
usage: '',
|
|
1802
|
+
paths: { scriptsDirectory: '' },
|
|
1803
|
+
cachedScripts: [],
|
|
1804
|
+
relevantScripts: [],
|
|
1805
|
+
pagination: { offset: 0, limit: 10, hasMore: false },
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
const { methodResults: oramaResults, scriptResults, totalMethodCount, facets, searchTime } = await searchToolsService.searchWithOrama(resourceType, action, scope, exclude, limit, offset);
|
|
1809
|
+
const allMethods = searchToolsService.getApiMethods();
|
|
1810
|
+
const methodMap = new Map(allMethods.map(m => [`${m.apiClass}.${m.methodName}`, m]));
|
|
1811
|
+
const results = oramaResults
|
|
1812
|
+
.map(doc => methodMap.get(doc.id))
|
|
1813
|
+
.filter((m) => m !== undefined);
|
|
1814
|
+
const scriptsDirectory = join(os.homedir(), '.prodisco', 'scripts', 'cache');
|
|
1815
|
+
initializeScriptsDirectory(scriptsDirectory);
|
|
1816
|
+
let cachedScripts = [];
|
|
1817
|
+
try {
|
|
1818
|
+
if (existsSync(scriptsDirectory)) {
|
|
1819
|
+
cachedScripts = readdirSync(scriptsDirectory)
|
|
1820
|
+
.filter(f => f.endsWith('.ts'))
|
|
1821
|
+
.sort();
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
catch {
|
|
1825
|
+
// Ignore errors
|
|
1826
|
+
}
|
|
1827
|
+
// Build relevant scripts array from search results
|
|
1828
|
+
const relevantScripts = scriptResults.map(doc => ({
|
|
1829
|
+
filename: doc.methodName + '.ts',
|
|
1830
|
+
filePath: doc.filePath,
|
|
1831
|
+
description: doc.description,
|
|
1832
|
+
apiClasses: doc.apiClass !== 'CachedScript' ? [doc.apiClass] : [],
|
|
1833
|
+
}));
|
|
1834
|
+
const hasMore = offset + results.length < totalMethodCount;
|
|
1835
|
+
let summary = `Found ${results.length} method(s) for resource "${resourceType}"`;
|
|
1836
|
+
if (action)
|
|
1837
|
+
summary += `, action "${action}"`;
|
|
1838
|
+
if (scope !== 'all')
|
|
1839
|
+
summary += `, scope "${scope}"`;
|
|
1840
|
+
if (exclude) {
|
|
1841
|
+
if (exclude.actions && exclude.actions.length > 0) {
|
|
1842
|
+
summary += `, excluding actions: [${exclude.actions.join(', ')}]`;
|
|
1843
|
+
}
|
|
1844
|
+
if (exclude.apiClasses && exclude.apiClasses.length > 0) {
|
|
1845
|
+
summary += `, excluding API classes: [${exclude.apiClasses.join(', ')}]`;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
summary += ` (search: ${searchTime.toFixed(2)}ms)`;
|
|
1849
|
+
if (offset > 0 || hasMore) {
|
|
1850
|
+
summary += ` | Page: ${Math.floor(offset / limit) + 1}, showing ${offset + 1}-${offset + results.length} of ${totalMethodCount}`;
|
|
1851
|
+
}
|
|
1852
|
+
summary += `\n\n`;
|
|
1853
|
+
// ========== RELEVANT CACHED SCRIPTS (shown FIRST) ==========
|
|
1854
|
+
if (relevantScripts.length > 0) {
|
|
1855
|
+
summary += `RELEVANT CACHED SCRIPTS (${relevantScripts.length} matching "${resourceType}"):\n`;
|
|
1856
|
+
relevantScripts.forEach((script, i) => {
|
|
1857
|
+
summary += ` ${i + 1}. ${script.filename}\n`;
|
|
1858
|
+
summary += ` ${script.description}\n`;
|
|
1859
|
+
if (script.apiClasses.length > 0) {
|
|
1860
|
+
summary += ` APIs: ${script.apiClasses.join(', ')}\n`;
|
|
405
1861
|
}
|
|
1862
|
+
summary += ` Path: ${script.filePath}\n`;
|
|
1863
|
+
summary += ` Run: npx tsx ${script.filePath}\n\n`;
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
// ========== FACETS ==========
|
|
1867
|
+
if (Object.keys(facets.apiClass).length > 0) {
|
|
1868
|
+
summary += `FACETS (refine your search):\n`;
|
|
1869
|
+
summary += ` API Classes: ${Object.entries(facets.apiClass).map(([k, v]) => `${k}(${v})`).join(', ')}\n`;
|
|
1870
|
+
summary += ` Actions: ${Object.entries(facets.action).map(([k, v]) => `${k}(${v})`).join(', ')}\n`;
|
|
1871
|
+
summary += ` Scopes: ${Object.entries(facets.scope).map(([k, v]) => `${k}(${v})`).join(', ')}\n\n`;
|
|
1872
|
+
}
|
|
1873
|
+
// ========== API METHODS ==========
|
|
1874
|
+
summary += `API METHODS:\n\n`;
|
|
1875
|
+
results.forEach((method, i) => {
|
|
1876
|
+
summary += `${i + 1}. ${method.apiClass}.${method.methodName}\n`;
|
|
1877
|
+
if (method.inputSchema.required.length > 0) {
|
|
1878
|
+
const params = method.inputSchema.required.map(r => `${r}: "${method.inputSchema.properties[r]?.type || 'string'}"`).join(', ');
|
|
1879
|
+
summary += ` method_args: { ${params} }\n`;
|
|
406
1880
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const hasForAllNamespaces = lowerMethod.includes('forallnamespaces');
|
|
410
|
-
if (scope === 'namespaced' && !hasNamespaced) {
|
|
411
|
-
return false;
|
|
1881
|
+
else {
|
|
1882
|
+
summary += ` method_args: {} (empty object - required)\n`;
|
|
412
1883
|
}
|
|
413
|
-
|
|
414
|
-
|
|
1884
|
+
const isList = method.methodName.startsWith('list');
|
|
1885
|
+
if (isList) {
|
|
1886
|
+
summary += ` return_values: response.items (array of ${method.resourceType})\n`;
|
|
415
1887
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const sorted = filtered.sort((a, b) => {
|
|
421
|
-
const aExact = a.resourceType.toLowerCase() === lowerResourceType;
|
|
422
|
-
const bExact = b.resourceType.toLowerCase() === lowerResourceType;
|
|
423
|
-
if (aExact && !bExact)
|
|
424
|
-
return -1;
|
|
425
|
-
if (!aExact && bExact)
|
|
426
|
-
return 1;
|
|
427
|
-
// Both exact or both partial: sort alphabetically
|
|
428
|
-
return a.methodName.localeCompare(b.methodName);
|
|
1888
|
+
else {
|
|
1889
|
+
summary += ` return_values: response (${method.resourceType} object)\n`;
|
|
1890
|
+
}
|
|
1891
|
+
summary += `\n`;
|
|
429
1892
|
});
|
|
430
|
-
|
|
1893
|
+
if (results.length === 0) {
|
|
1894
|
+
summary += `No methods found. Try:\n`;
|
|
1895
|
+
summary += `- Different resourceType (e.g., "Pod", "Deployment", "Service")\n`;
|
|
1896
|
+
summary += `- Omit action to see all available methods\n`;
|
|
1897
|
+
summary += `- Use scope: "all" to see both namespaced and cluster methods\n`;
|
|
1898
|
+
}
|
|
1899
|
+
summary += `Write scripts to: ${scriptsDirectory}/<name>.ts\n`;
|
|
1900
|
+
summary += `Run with: npx tsx ${scriptsDirectory}/<name>.ts\n`;
|
|
1901
|
+
summary += `For type definitions: use mode: "types" with types: ["V1Pod"]\n\n`;
|
|
1902
|
+
const usage = 'USAGE:\n' +
|
|
1903
|
+
'- All methods require object parameter: await api.method({ param: value })\n' +
|
|
1904
|
+
'- List operations return: response.items (array)\n' +
|
|
1905
|
+
'- Single resource operations return: response (object)\n' +
|
|
1906
|
+
`- Write scripts to: ${scriptsDirectory}/<yourscript>.ts\n` +
|
|
1907
|
+
`- Import: import * as k8s from '@kubernetes/client-node';\n` +
|
|
1908
|
+
`- Run: npx tsx ${scriptsDirectory}/<yourscript>.ts`;
|
|
1909
|
+
return {
|
|
1910
|
+
mode: 'methods',
|
|
1911
|
+
summary,
|
|
1912
|
+
tools: results,
|
|
1913
|
+
totalMatches: totalMethodCount,
|
|
1914
|
+
usage,
|
|
1915
|
+
paths: {
|
|
1916
|
+
scriptsDirectory,
|
|
1917
|
+
},
|
|
1918
|
+
cachedScripts,
|
|
1919
|
+
relevantScripts,
|
|
1920
|
+
facets,
|
|
1921
|
+
searchTime,
|
|
1922
|
+
pagination: {
|
|
1923
|
+
offset,
|
|
1924
|
+
limit,
|
|
1925
|
+
hasMore,
|
|
1926
|
+
},
|
|
1927
|
+
};
|
|
431
1928
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
1929
|
+
/**
|
|
1930
|
+
* Execute script search mode
|
|
1931
|
+
*/
|
|
1932
|
+
async function executeScriptMode(input) {
|
|
1933
|
+
const { searchTerm, limit = 10, offset = 0 } = input;
|
|
1934
|
+
const db = await searchToolsService.getOramaDb();
|
|
1935
|
+
const scriptsDirectory = join(os.homedir(), '.prodisco', 'scripts', 'cache');
|
|
1936
|
+
initializeScriptsDirectory(scriptsDirectory);
|
|
1937
|
+
let scripts = [];
|
|
1938
|
+
let totalMatches = 0;
|
|
1939
|
+
if (searchTerm) {
|
|
1940
|
+
// Search for scripts matching the term
|
|
1941
|
+
const searchParams = {
|
|
1942
|
+
term: searchTerm,
|
|
1943
|
+
properties: ['resourceType', 'methodName', 'description', 'searchTokens'],
|
|
1944
|
+
boost: {
|
|
1945
|
+
resourceType: 3,
|
|
1946
|
+
searchTokens: 2.5,
|
|
1947
|
+
methodName: 2,
|
|
1948
|
+
description: 1,
|
|
1949
|
+
},
|
|
1950
|
+
tolerance: 1,
|
|
1951
|
+
limit: 100, // Get all matches for filtering
|
|
1952
|
+
};
|
|
1953
|
+
const searchResult = await search(db, searchParams);
|
|
1954
|
+
// Filter to only scripts
|
|
1955
|
+
const scriptHits = searchResult.hits
|
|
1956
|
+
.filter(hit => hit.document.documentType === 'script')
|
|
1957
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
1958
|
+
totalMatches = scriptHits.length;
|
|
1959
|
+
// Filter out scripts that no longer exist on disk, and clean up stale index entries
|
|
1960
|
+
const validScriptHits = [];
|
|
1961
|
+
for (const hit of scriptHits) {
|
|
1962
|
+
if (existsSync(hit.document.filePath)) {
|
|
1963
|
+
validScriptHits.push(hit);
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
// Clean up stale index entry
|
|
1967
|
+
try {
|
|
1968
|
+
await remove(db, hit.document.id);
|
|
1969
|
+
logger.debug(`Orama: Removed stale script ${hit.document.methodName} from index`);
|
|
1970
|
+
}
|
|
1971
|
+
catch {
|
|
1972
|
+
// Ignore removal errors
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
totalMatches = validScriptHits.length;
|
|
1977
|
+
scripts = validScriptHits
|
|
1978
|
+
.slice(offset, offset + limit)
|
|
1979
|
+
.map(hit => ({
|
|
1980
|
+
filename: hit.document.methodName + '.ts',
|
|
1981
|
+
filePath: hit.document.filePath,
|
|
1982
|
+
description: hit.document.description,
|
|
1983
|
+
apiClasses: hit.document.apiClass !== 'CachedScript' ? [hit.document.apiClass] : [],
|
|
1984
|
+
}));
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
// List all scripts
|
|
468
1988
|
try {
|
|
469
1989
|
if (existsSync(scriptsDirectory)) {
|
|
470
|
-
|
|
1990
|
+
const allScripts = readdirSync(scriptsDirectory)
|
|
471
1991
|
.filter(f => f.endsWith('.ts'))
|
|
472
1992
|
.sort();
|
|
1993
|
+
totalMatches = allScripts.length;
|
|
1994
|
+
scripts = allScripts
|
|
1995
|
+
.slice(offset, offset + limit)
|
|
1996
|
+
.map(filename => {
|
|
1997
|
+
const filePath = join(scriptsDirectory, filename);
|
|
1998
|
+
const parsed = parseScriptFile(filePath);
|
|
1999
|
+
return {
|
|
2000
|
+
filename,
|
|
2001
|
+
filePath,
|
|
2002
|
+
description: parsed?.description || `Script: ${filename.replace(/\.ts$/, '')}`,
|
|
2003
|
+
apiClasses: parsed?.apiClasses || [],
|
|
2004
|
+
};
|
|
2005
|
+
});
|
|
473
2006
|
}
|
|
474
2007
|
}
|
|
475
2008
|
catch {
|
|
476
|
-
// Ignore errors
|
|
477
|
-
}
|
|
478
|
-
// Structured output - clear and unambiguous
|
|
479
|
-
let summary = `Found ${results.length} method(s) for resource "${resourceType}"`;
|
|
480
|
-
if (action)
|
|
481
|
-
summary += `, action "${action}"`;
|
|
482
|
-
if (scope !== 'all')
|
|
483
|
-
summary += `, scope "${scope}"`;
|
|
484
|
-
if (exclude) {
|
|
485
|
-
if (exclude.actions && exclude.actions.length > 0) {
|
|
486
|
-
summary += `, excluding actions: [${exclude.actions.join(', ')}]`;
|
|
487
|
-
}
|
|
488
|
-
if (exclude.apiClasses && exclude.apiClasses.length > 0) {
|
|
489
|
-
summary += `, excluding API classes: [${exclude.apiClasses.join(', ')}]`;
|
|
490
|
-
}
|
|
2009
|
+
// Ignore errors
|
|
491
2010
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// Method arguments
|
|
508
|
-
if (method.inputSchema.required.length > 0) {
|
|
509
|
-
const params = method.inputSchema.required.map(r => `${r}: "${method.inputSchema.properties[r]?.type || 'string'}"`).join(', ');
|
|
510
|
-
summary += ` method_args: { ${params} }\n`;
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
summary += ` method_args: {} (empty object - required)\n`;
|
|
514
|
-
}
|
|
515
|
-
// Return values
|
|
516
|
-
const isList = method.methodName.startsWith('list');
|
|
517
|
-
if (isList) {
|
|
518
|
-
summary += ` return_values: response.items (array of ${method.resourceType})\n`;
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
summary += ` return_values: response (${method.resourceType} object)\n`;
|
|
522
|
-
}
|
|
523
|
-
// Include inline type definitions if available (brief overview)
|
|
524
|
-
if (method.typeDefinitions && method.typeDefinitions.output) {
|
|
525
|
-
const lines = method.typeDefinitions.output.split('\n');
|
|
526
|
-
const typeName = lines[0]?.trim() || 'unknown';
|
|
527
|
-
// Extract key properties (just first 2-3)
|
|
528
|
-
const propertyLines = lines.slice(1, 4).filter(l => l.trim() && !l.includes('}'));
|
|
529
|
-
summary += ` return_types: ${typeName}\n`;
|
|
530
|
-
if (propertyLines.length > 0) {
|
|
531
|
-
summary += ` key properties: ${propertyLines.map(l => l.trim()).join(', ')}\n`;
|
|
532
|
-
}
|
|
533
|
-
summary += ` (use kubernetes.getTypeDefinition for complete type details)\n`;
|
|
2011
|
+
}
|
|
2012
|
+
const hasMore = offset + scripts.length < totalMatches;
|
|
2013
|
+
let summary = searchTerm
|
|
2014
|
+
? `CACHED SCRIPTS (${totalMatches} matching "${searchTerm}")`
|
|
2015
|
+
: `CACHED SCRIPTS (${totalMatches} total)`;
|
|
2016
|
+
if (offset > 0 || hasMore) {
|
|
2017
|
+
summary += ` | Page ${Math.floor(offset / limit) + 1}, showing ${offset + 1}-${offset + scripts.length} of ${totalMatches}`;
|
|
2018
|
+
}
|
|
2019
|
+
summary += `\n\n`;
|
|
2020
|
+
if (scripts.length > 0) {
|
|
2021
|
+
scripts.forEach((script, i) => {
|
|
2022
|
+
summary += `${i + 1}. ${script.filename}\n`;
|
|
2023
|
+
summary += ` ${script.description}\n`;
|
|
2024
|
+
if (script.apiClasses.length > 0) {
|
|
2025
|
+
summary += ` APIs: ${script.apiClasses.join(', ')}\n`;
|
|
534
2026
|
}
|
|
535
|
-
summary +=
|
|
2027
|
+
summary += ` Path: ${script.filePath}\n`;
|
|
2028
|
+
summary += ` Run: npx tsx ${script.filePath}\n\n`;
|
|
536
2029
|
});
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
summary +=
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
2030
|
+
}
|
|
2031
|
+
else {
|
|
2032
|
+
summary += `No scripts found.`;
|
|
2033
|
+
if (searchTerm) {
|
|
2034
|
+
summary += ` Try a different search term or omit searchTerm to list all scripts.\n`;
|
|
2035
|
+
}
|
|
2036
|
+
else {
|
|
2037
|
+
summary += ` Scripts directory: ${scriptsDirectory}\n`;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
summary += `\nScripts directory: ${scriptsDirectory}\n`;
|
|
2041
|
+
return {
|
|
2042
|
+
mode: 'scripts',
|
|
2043
|
+
summary,
|
|
2044
|
+
scripts,
|
|
2045
|
+
totalMatches,
|
|
2046
|
+
paths: {
|
|
2047
|
+
scriptsDirectory,
|
|
2048
|
+
},
|
|
2049
|
+
pagination: {
|
|
2050
|
+
offset,
|
|
2051
|
+
limit,
|
|
2052
|
+
hasMore,
|
|
2053
|
+
},
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Group metrics by semantic category for better discoverability
|
|
2058
|
+
*/
|
|
2059
|
+
function groupMetricsByCategory(metrics) {
|
|
2060
|
+
const categories = {
|
|
2061
|
+
'status & lifecycle': [],
|
|
2062
|
+
'cpu & compute': [],
|
|
2063
|
+
'memory': [],
|
|
2064
|
+
'network': [],
|
|
2065
|
+
'storage': [],
|
|
2066
|
+
'other': [],
|
|
2067
|
+
};
|
|
2068
|
+
for (const m of metrics) {
|
|
2069
|
+
const name = m.name.toLowerCase();
|
|
2070
|
+
if (name.includes('status') || name.includes('phase') || name.includes('ready') || name.includes('restart')) {
|
|
2071
|
+
categories['status & lifecycle'].push(m);
|
|
2072
|
+
}
|
|
2073
|
+
else if (name.includes('cpu') || name.includes('throttl')) {
|
|
2074
|
+
categories['cpu & compute'].push(m);
|
|
2075
|
+
}
|
|
2076
|
+
else if (name.includes('memory') || name.includes('mem_') || name.includes('_mem')) {
|
|
2077
|
+
categories['memory'].push(m);
|
|
2078
|
+
}
|
|
2079
|
+
else if (name.includes('network') || name.includes('receive') || name.includes('transmit') || name.includes('_rx_') || name.includes('_tx_')) {
|
|
2080
|
+
categories['network'].push(m);
|
|
2081
|
+
}
|
|
2082
|
+
else if (name.includes('storage') || name.includes('disk') || name.includes('volume') || name.includes('fs_')) {
|
|
2083
|
+
categories['storage'].push(m);
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
categories['other'].push(m);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
// Remove empty categories
|
|
2090
|
+
return Object.fromEntries(Object.entries(categories).filter(([, v]) => v.length > 0));
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Execute metrics mode - search for actual Prometheus metrics from the cluster
|
|
2094
|
+
*/
|
|
2095
|
+
async function executeMetricsMode(searchPattern, limit, offset) {
|
|
2096
|
+
const scriptsDirectory = join(os.homedir(), '.prodisco', 'scripts', 'cache');
|
|
2097
|
+
initializeScriptsDirectory(scriptsDirectory);
|
|
2098
|
+
const indexingStatus = searchToolsService.getMetricsIndexingStatus();
|
|
2099
|
+
if (indexingStatus === 'unavailable') {
|
|
552
2100
|
return {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
},
|
|
560
|
-
|
|
2101
|
+
mode: 'prometheus',
|
|
2102
|
+
category: 'metrics',
|
|
2103
|
+
summary: 'Prometheus metrics indexing unavailable. Ensure PROMETHEUS_URL is configured.',
|
|
2104
|
+
metrics: [],
|
|
2105
|
+
totalMatches: 0,
|
|
2106
|
+
indexingStatus,
|
|
2107
|
+
paths: { scriptsDirectory },
|
|
2108
|
+
pagination: { offset: 0, limit, hasMore: false },
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
const db = await searchToolsService.getOramaDb();
|
|
2112
|
+
// Search Orama for prometheus-metric documents
|
|
2113
|
+
const searchResults = await search(db, {
|
|
2114
|
+
term: searchPattern || '',
|
|
2115
|
+
properties: ['methodName', 'description', 'searchTokens'],
|
|
2116
|
+
limit: 10000, // Get all matching, we'll paginate manually
|
|
2117
|
+
});
|
|
2118
|
+
// Filter to only prometheus-metric documents
|
|
2119
|
+
const metricHits = searchResults.hits.filter(hit => hit.document.documentType === 'prometheus-metric');
|
|
2120
|
+
const totalMatches = metricHits.length;
|
|
2121
|
+
// Apply pagination
|
|
2122
|
+
const paginatedHits = metricHits.slice(offset, offset + limit);
|
|
2123
|
+
// Map to output format
|
|
2124
|
+
const metrics = paginatedHits.map(hit => ({
|
|
2125
|
+
name: hit.document.methodName,
|
|
2126
|
+
type: String(hit.document.metricType || 'unknown'),
|
|
2127
|
+
description: hit.document.description,
|
|
2128
|
+
}));
|
|
2129
|
+
// Build summary with semantic grouping
|
|
2130
|
+
let summary = `PROMETHEUS METRICS`;
|
|
2131
|
+
if (searchPattern)
|
|
2132
|
+
summary += ` matching "${searchPattern}"`;
|
|
2133
|
+
summary += `\n\nFound ${totalMatches} metric(s)`;
|
|
2134
|
+
if (indexingStatus === 'in_progress') {
|
|
2135
|
+
summary += ` (indexing in progress, results may be incomplete)`;
|
|
2136
|
+
}
|
|
2137
|
+
summary += `\n\n`;
|
|
2138
|
+
// Group metrics by category
|
|
2139
|
+
const grouped = groupMetricsByCategory(metrics);
|
|
2140
|
+
for (const [category, categoryMetrics] of Object.entries(grouped)) {
|
|
2141
|
+
summary += `${category.toUpperCase()}:\n`;
|
|
2142
|
+
for (const m of categoryMetrics) {
|
|
2143
|
+
summary += ` ${m.name} (${m.type})\n`;
|
|
2144
|
+
summary += ` ${m.description}\n\n`;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
// Add prominent usage hints
|
|
2148
|
+
summary += `${'='.repeat(60)}\n`;
|
|
2149
|
+
summary += `NEXT STEPS:\n\n`;
|
|
2150
|
+
summary += `1. GET LABELS for a metric (to see what you can filter on):\n`;
|
|
2151
|
+
summary += ` prom.labelNames(['{__name__="${metrics[0]?.name || 'metric_name'}"}'])\n`;
|
|
2152
|
+
summary += ` → Returns: ["namespace", "pod", "phase", ...]\n\n`;
|
|
2153
|
+
summary += `2. QUERY a metric:\n`;
|
|
2154
|
+
summary += ` prom.instantQuery('${metrics[0]?.name || 'metric_name'}{namespace="default"}')\n`;
|
|
2155
|
+
summary += `${'='.repeat(60)}\n`;
|
|
2156
|
+
return {
|
|
2157
|
+
mode: 'prometheus',
|
|
2158
|
+
category: 'metrics',
|
|
2159
|
+
summary,
|
|
2160
|
+
metrics,
|
|
2161
|
+
totalMatches,
|
|
2162
|
+
indexingStatus,
|
|
2163
|
+
paths: { scriptsDirectory },
|
|
2164
|
+
pagination: {
|
|
2165
|
+
offset,
|
|
2166
|
+
limit,
|
|
2167
|
+
hasMore: offset + metrics.length < totalMatches,
|
|
2168
|
+
},
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Execute prometheus mode - search for prometheus-query library methods
|
|
2173
|
+
*/
|
|
2174
|
+
async function executePrometheusMode(input) {
|
|
2175
|
+
const { category = 'all', methodPattern, limit = 10, offset = 0 } = input;
|
|
2176
|
+
// Handle metrics category - search indexed cluster metrics
|
|
2177
|
+
if (category === 'metrics') {
|
|
2178
|
+
return executeMetricsMode(methodPattern, limit, offset);
|
|
2179
|
+
}
|
|
2180
|
+
const scriptsDirectory = join(os.homedir(), '.prodisco', 'scripts', 'cache');
|
|
2181
|
+
initializeScriptsDirectory(scriptsDirectory);
|
|
2182
|
+
// Get all prometheus methods
|
|
2183
|
+
let methods = getPrometheusMethods();
|
|
2184
|
+
// Filter by category
|
|
2185
|
+
if (category !== 'all') {
|
|
2186
|
+
methods = methods.filter(m => m.category === category);
|
|
2187
|
+
}
|
|
2188
|
+
// Filter by method pattern
|
|
2189
|
+
if (methodPattern) {
|
|
2190
|
+
const pattern = methodPattern.toLowerCase();
|
|
2191
|
+
methods = methods.filter(m => m.methodName.toLowerCase().includes(pattern) ||
|
|
2192
|
+
m.description.toLowerCase().includes(pattern));
|
|
2193
|
+
}
|
|
2194
|
+
const totalMatches = methods.length;
|
|
2195
|
+
// Apply pagination
|
|
2196
|
+
const paginatedMethods = methods.slice(offset, offset + limit);
|
|
2197
|
+
const hasMore = offset + paginatedMethods.length < totalMatches;
|
|
2198
|
+
// Build facets
|
|
2199
|
+
const facets = {
|
|
2200
|
+
library: {},
|
|
2201
|
+
category: {},
|
|
2202
|
+
};
|
|
2203
|
+
for (const m of methods) {
|
|
2204
|
+
facets.library[m.library] = (facets.library[m.library] || 0) + 1;
|
|
2205
|
+
facets.category[m.category] = (facets.category[m.category] || 0) + 1;
|
|
2206
|
+
}
|
|
2207
|
+
// Library info
|
|
2208
|
+
const libraries = {
|
|
2209
|
+
'prometheus-query': { installed: true, version: '^3.3.2' },
|
|
2210
|
+
};
|
|
2211
|
+
// Check if PROMETHEUS_URL is configured
|
|
2212
|
+
const prometheusUrl = process.env.PROMETHEUS_URL;
|
|
2213
|
+
// Build summary
|
|
2214
|
+
let summary = `PROMETHEUS METHODS`;
|
|
2215
|
+
if (category !== 'all')
|
|
2216
|
+
summary += ` (category: ${category})`;
|
|
2217
|
+
if (methodPattern)
|
|
2218
|
+
summary += ` (pattern: "${methodPattern}")`;
|
|
2219
|
+
summary += `\n\nFound ${totalMatches} method(s)`;
|
|
2220
|
+
if (offset > 0 || hasMore) {
|
|
2221
|
+
summary += ` | Page ${Math.floor(offset / limit) + 1}, showing ${offset + 1}-${offset + paginatedMethods.length} of ${totalMatches}`;
|
|
2222
|
+
}
|
|
2223
|
+
summary += `\n\n`;
|
|
2224
|
+
// Show PROMETHEUS_URL status
|
|
2225
|
+
if (!prometheusUrl) {
|
|
2226
|
+
summary += `⚠️ PROMETHEUS_URL not configured - prometheus-query methods require this environment variable\n`;
|
|
2227
|
+
summary += ` Set via: PROMETHEUS_URL="http://prometheus:9090"\n\n`;
|
|
2228
|
+
}
|
|
2229
|
+
else {
|
|
2230
|
+
summary += `✓ PROMETHEUS_URL: ${prometheusUrl}\n\n`;
|
|
2231
|
+
}
|
|
2232
|
+
// Show facets
|
|
2233
|
+
if (Object.keys(facets.category).length > 0) {
|
|
2234
|
+
summary += `FACETS:\n`;
|
|
2235
|
+
summary += ` Categories: ${Object.entries(facets.category).map(([k, v]) => `${k}(${v})`).join(', ')}\n\n`;
|
|
2236
|
+
}
|
|
2237
|
+
// Show methods
|
|
2238
|
+
summary += `METHODS:\n\n`;
|
|
2239
|
+
paginatedMethods.forEach((method, i) => {
|
|
2240
|
+
const className = method.className ? `${method.className}.` : '';
|
|
2241
|
+
summary += `${i + 1}. ${method.library}: ${className}${method.methodName}\n`;
|
|
2242
|
+
summary += ` Category: ${method.category}\n`;
|
|
2243
|
+
summary += ` ${method.description}\n`;
|
|
2244
|
+
if (method.parameters.length > 0) {
|
|
2245
|
+
const params = method.parameters.map(p => `${p.name}${p.optional ? '?' : ''}: ${p.type}`).join(', ');
|
|
2246
|
+
summary += ` Params: (${params})\n`;
|
|
2247
|
+
}
|
|
2248
|
+
summary += ` Returns: ${method.returnType}\n\n`;
|
|
2249
|
+
});
|
|
2250
|
+
if (paginatedMethods.length === 0) {
|
|
2251
|
+
summary += `No methods found. Try:\n`;
|
|
2252
|
+
summary += `- Different category filter\n`;
|
|
2253
|
+
summary += `- Different methodPattern\n`;
|
|
2254
|
+
}
|
|
2255
|
+
summary += `\nWrite scripts to: ${scriptsDirectory}/<name>.ts\n`;
|
|
2256
|
+
summary += `Run with: npx tsx ${scriptsDirectory}/<name>.ts\n`;
|
|
2257
|
+
// Add tip about metrics category
|
|
2258
|
+
if (prometheusUrl) {
|
|
2259
|
+
summary += `\n💡 TIP: Use category: "metrics" to discover actual metrics from your cluster.\n`;
|
|
2260
|
+
summary += ` Example: { mode: "prometheus", category: "metrics", methodPattern: "pod" }\n`;
|
|
2261
|
+
}
|
|
2262
|
+
const usage = 'USAGE:\n' +
|
|
2263
|
+
'- import { PrometheusDriver } from \'prometheus-query\';\n' +
|
|
2264
|
+
`- Write scripts to: ${scriptsDirectory}/<yourscript>.ts\n` +
|
|
2265
|
+
`- Run: npx tsx ${scriptsDirectory}/<yourscript>.ts`;
|
|
2266
|
+
// If PROMETHEUS_URL is not set, return error result
|
|
2267
|
+
if (!prometheusUrl) {
|
|
2268
|
+
return {
|
|
2269
|
+
mode: 'prometheus',
|
|
2270
|
+
error: 'PROMETHEUS_URL_NOT_CONFIGURED',
|
|
2271
|
+
message: 'The PROMETHEUS_URL environment variable is not set. prometheus-query methods require this to connect to a Prometheus server.',
|
|
2272
|
+
example: 'PROMETHEUS_URL="http://prometheus:9090" npx tsx script.ts',
|
|
2273
|
+
methods: paginatedMethods,
|
|
2274
|
+
totalMatches,
|
|
2275
|
+
libraries,
|
|
2276
|
+
paths: { scriptsDirectory },
|
|
2277
|
+
facets,
|
|
2278
|
+
pagination: { offset, limit, hasMore },
|
|
561
2279
|
};
|
|
2280
|
+
}
|
|
2281
|
+
return {
|
|
2282
|
+
mode: 'prometheus',
|
|
2283
|
+
summary,
|
|
2284
|
+
methods: paginatedMethods,
|
|
2285
|
+
totalMatches,
|
|
2286
|
+
libraries,
|
|
2287
|
+
usage,
|
|
2288
|
+
paths: { scriptsDirectory },
|
|
2289
|
+
facets,
|
|
2290
|
+
pagination: { offset, limit, hasMore },
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
// ============================================================================
|
|
2294
|
+
// Warmup Export
|
|
2295
|
+
// ============================================================================
|
|
2296
|
+
/**
|
|
2297
|
+
* Pre-warm the Orama search index during server startup.
|
|
2298
|
+
* This avoids the indexing delay on the first search request.
|
|
2299
|
+
*/
|
|
2300
|
+
export async function warmupSearchIndex() {
|
|
2301
|
+
await searchToolsService.initialize();
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Shutdown the search tools service. Call this during graceful shutdown.
|
|
2305
|
+
*/
|
|
2306
|
+
export async function shutdownSearchIndex() {
|
|
2307
|
+
await searchToolsService.shutdown();
|
|
2308
|
+
}
|
|
2309
|
+
// ============================================================================
|
|
2310
|
+
// Main Tool Export
|
|
2311
|
+
// ============================================================================
|
|
2312
|
+
export const searchToolsTool = {
|
|
2313
|
+
name: 'kubernetes.searchTools',
|
|
2314
|
+
description: 'Find Kubernetes API methods, get type definitions, or search cached scripts. ' +
|
|
2315
|
+
'MODES: ' +
|
|
2316
|
+
'• methods (default): Search for API methods by resource type. Also shows relevant cached scripts first. ' +
|
|
2317
|
+
'Params: resourceType (required), action, scope, exclude, limit, offset. ' +
|
|
2318
|
+
'Example: { resourceType: "Pod", action: "list" } ' +
|
|
2319
|
+
'• types: Get TypeScript type definitions with path navigation. ' +
|
|
2320
|
+
'Params: types (required), depth. ' +
|
|
2321
|
+
'Example: { mode: "types", types: ["V1Pod", "V1Deployment.spec.template.spec"] } ' +
|
|
2322
|
+
'• scripts: Search or list cached scripts. ' +
|
|
2323
|
+
'Params: searchTerm (optional), limit, offset. ' +
|
|
2324
|
+
'Example: { mode: "scripts", searchTerm: "pod" } ' +
|
|
2325
|
+
'• prometheus: Search Prometheus API methods or discover cluster metrics. ' +
|
|
2326
|
+
'Params: category (query/metadata/alerts/metrics), methodPattern (optional), limit, offset. ' +
|
|
2327
|
+
'Example: { mode: "prometheus", category: "query" } ' +
|
|
2328
|
+
'Use category: "metrics" with methodPattern to discover metrics (e.g., { mode: "prometheus", category: "metrics", methodPattern: "gpu" }). ' +
|
|
2329
|
+
'Actions: list, read, create, delete, patch, replace, connect, get, watch. ' +
|
|
2330
|
+
'Scopes: namespaced, cluster, all. ' +
|
|
2331
|
+
'Docs: https://github.com/harche/ProDisco/blob/main/docs/search-tools.md',
|
|
2332
|
+
schema: SearchToolsInputSchema,
|
|
2333
|
+
async execute(input) {
|
|
2334
|
+
const { mode = 'methods' } = input;
|
|
2335
|
+
if (mode === 'types') {
|
|
2336
|
+
return executeTypeMode(input);
|
|
2337
|
+
}
|
|
2338
|
+
else if (mode === 'scripts') {
|
|
2339
|
+
return executeScriptMode(input);
|
|
2340
|
+
}
|
|
2341
|
+
else if (mode === 'prometheus') {
|
|
2342
|
+
return executePrometheusMode(input);
|
|
2343
|
+
}
|
|
2344
|
+
else {
|
|
2345
|
+
return executeMethodMode(input);
|
|
2346
|
+
}
|
|
562
2347
|
},
|
|
563
2348
|
};
|
|
564
2349
|
//# sourceMappingURL=searchTools.js.map
|