driftdetect-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,976 @@
1
+ /**
2
+ * Drift MCP Server Implementation
3
+ *
4
+ * Provides structured access to drift functionality for AI agents.
5
+ */
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
+ import { PatternStore, ManifestStore, } from 'driftdetect-core';
9
+ import { PackManager } from './packs.js';
10
+ import { FeedbackManager } from './feedback.js';
11
+ const PATTERN_CATEGORIES = [
12
+ 'structural', 'components', 'styling', 'api', 'auth', 'errors',
13
+ 'data-access', 'testing', 'logging', 'security', 'config',
14
+ 'types', 'performance', 'accessibility', 'documentation',
15
+ ];
16
+ const TOOLS = [
17
+ {
18
+ name: 'drift_status',
19
+ description: 'Get overall codebase pattern health and statistics. Use this first to understand what patterns drift has learned.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {},
23
+ required: [],
24
+ },
25
+ },
26
+ {
27
+ name: 'drift_patterns',
28
+ description: 'Get patterns for specific categories. Returns learned patterns with confidence scores and locations.',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ categories: {
33
+ type: 'array',
34
+ items: { type: 'string' },
35
+ description: `Categories to query. Valid: ${PATTERN_CATEGORIES.join(', ')}`,
36
+ },
37
+ minConfidence: {
38
+ type: 'number',
39
+ description: 'Minimum confidence score (0.0-1.0)',
40
+ },
41
+ },
42
+ required: [],
43
+ },
44
+ },
45
+ {
46
+ name: 'drift_files',
47
+ description: 'Get patterns found in a specific file or file pattern (glob supported).',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ path: {
52
+ type: 'string',
53
+ description: 'File path or glob pattern (e.g., "src/api/*.ts")',
54
+ },
55
+ category: {
56
+ type: 'string',
57
+ description: 'Filter by category',
58
+ },
59
+ },
60
+ required: ['path'],
61
+ },
62
+ },
63
+ {
64
+ name: 'drift_where',
65
+ description: 'Find where a pattern is used across the codebase.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ pattern: {
70
+ type: 'string',
71
+ description: 'Pattern name or ID to search for',
72
+ },
73
+ category: {
74
+ type: 'string',
75
+ description: 'Filter by category',
76
+ },
77
+ },
78
+ required: ['pattern'],
79
+ },
80
+ },
81
+ {
82
+ name: 'drift_export',
83
+ description: 'Export patterns in AI-optimized format. Use this to get full context for code generation.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ categories: {
88
+ type: 'array',
89
+ items: { type: 'string' },
90
+ description: 'Categories to export (defaults to all)',
91
+ },
92
+ format: {
93
+ type: 'string',
94
+ enum: ['ai-context', 'json', 'summary'],
95
+ description: 'Export format (default: ai-context)',
96
+ },
97
+ compact: {
98
+ type: 'boolean',
99
+ description: 'Compact output with fewer details',
100
+ },
101
+ },
102
+ required: [],
103
+ },
104
+ },
105
+ {
106
+ name: 'drift_contracts',
107
+ description: 'Get frontend/backend API contract status. Shows mismatches between API calls and endpoints.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ status: {
112
+ type: 'string',
113
+ enum: ['all', 'verified', 'mismatch', 'discovered'],
114
+ description: 'Filter by contract status',
115
+ },
116
+ },
117
+ required: [],
118
+ },
119
+ },
120
+ {
121
+ name: 'drift_examples',
122
+ description: 'Get actual code examples for patterns. Returns real code snippets from the codebase that demonstrate how patterns are implemented. Use this to understand HOW to implement patterns. Automatically filters out documentation, config files, and deprecated code.',
123
+ inputSchema: {
124
+ type: 'object',
125
+ properties: {
126
+ categories: {
127
+ type: 'array',
128
+ items: { type: 'string' },
129
+ description: `Categories to get examples for. Valid: ${PATTERN_CATEGORIES.join(', ')}`,
130
+ },
131
+ pattern: {
132
+ type: 'string',
133
+ description: 'Specific pattern name or ID to get examples for',
134
+ },
135
+ maxExamples: {
136
+ type: 'number',
137
+ description: 'Maximum examples per pattern (default: 3)',
138
+ },
139
+ contextLines: {
140
+ type: 'number',
141
+ description: 'Lines of context around each match (default: 10)',
142
+ },
143
+ includeDeprecated: {
144
+ type: 'boolean',
145
+ description: 'Include deprecated/legacy code examples (default: false)',
146
+ },
147
+ },
148
+ required: [],
149
+ },
150
+ },
151
+ {
152
+ name: 'drift_pack',
153
+ description: 'Get a pre-defined pattern pack for common development tasks. Packs are cached and auto-invalidate when patterns change. Also supports learning new packs from usage patterns.',
154
+ inputSchema: {
155
+ type: 'object',
156
+ properties: {
157
+ action: {
158
+ type: 'string',
159
+ enum: ['get', 'list', 'suggest', 'create', 'delete', 'infer'],
160
+ description: 'Action to perform (default: "get"). "suggest" shows packs based on usage, "infer" suggests from file structure, "create" saves a custom pack, "delete" removes a custom pack',
161
+ },
162
+ name: {
163
+ type: 'string',
164
+ description: 'Pack name for get/create/delete actions',
165
+ },
166
+ refresh: {
167
+ type: 'boolean',
168
+ description: 'Force regenerate the pack even if cached (default: false)',
169
+ },
170
+ list: {
171
+ type: 'boolean',
172
+ description: '[DEPRECATED: use action="list"] List all available packs',
173
+ },
174
+ // For create action
175
+ description: {
176
+ type: 'string',
177
+ description: 'Pack description (for create action)',
178
+ },
179
+ categories: {
180
+ type: 'array',
181
+ items: { type: 'string' },
182
+ description: 'Categories to include in pack (for create action)',
183
+ },
184
+ patterns: {
185
+ type: 'array',
186
+ items: { type: 'string' },
187
+ description: 'Pattern name filters (for create action)',
188
+ },
189
+ },
190
+ required: [],
191
+ },
192
+ },
193
+ {
194
+ name: 'drift_feedback',
195
+ description: 'Provide feedback on pattern examples to improve future suggestions. Good feedback helps drift learn which files produce useful examples.',
196
+ inputSchema: {
197
+ type: 'object',
198
+ properties: {
199
+ action: {
200
+ type: 'string',
201
+ enum: ['rate', 'stats', 'clear'],
202
+ description: 'Action: "rate" to rate an example, "stats" to see feedback statistics, "clear" to reset all feedback',
203
+ },
204
+ patternId: {
205
+ type: 'string',
206
+ description: 'Pattern ID (for rate action)',
207
+ },
208
+ patternName: {
209
+ type: 'string',
210
+ description: 'Pattern name (for rate action)',
211
+ },
212
+ category: {
213
+ type: 'string',
214
+ description: 'Pattern category (for rate action)',
215
+ },
216
+ file: {
217
+ type: 'string',
218
+ description: 'File path of the example (for rate action)',
219
+ },
220
+ line: {
221
+ type: 'number',
222
+ description: 'Line number of the example (for rate action)',
223
+ },
224
+ rating: {
225
+ type: 'string',
226
+ enum: ['good', 'bad', 'irrelevant'],
227
+ description: 'Rating: "good" = useful example, "bad" = wrong/misleading, "irrelevant" = not related to pattern',
228
+ },
229
+ reason: {
230
+ type: 'string',
231
+ description: 'Optional reason for the rating',
232
+ },
233
+ },
234
+ required: [],
235
+ },
236
+ },
237
+ ];
238
+ export function createDriftMCPServer(config) {
239
+ const server = new Server({ name: 'drift', version: '0.1.0' }, { capabilities: { tools: {} } });
240
+ const patternStore = new PatternStore({ rootDir: config.projectRoot });
241
+ const manifestStore = new ManifestStore(config.projectRoot);
242
+ const packManager = new PackManager(config.projectRoot, patternStore);
243
+ const feedbackManager = new FeedbackManager(config.projectRoot);
244
+ // List available tools
245
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
246
+ tools: TOOLS,
247
+ }));
248
+ // Handle tool calls
249
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
250
+ const { name, arguments: args } = request.params;
251
+ try {
252
+ switch (name) {
253
+ case 'drift_status':
254
+ return await handleStatus(patternStore);
255
+ case 'drift_patterns':
256
+ return await handlePatterns(patternStore, args);
257
+ case 'drift_files':
258
+ return await handleFiles(manifestStore, args);
259
+ case 'drift_where':
260
+ return await handleWhere(patternStore, args);
261
+ case 'drift_export':
262
+ return await handleExport(patternStore, args);
263
+ case 'drift_contracts':
264
+ return await handleContracts(config.projectRoot, args);
265
+ case 'drift_examples':
266
+ return await handleExamples(config.projectRoot, patternStore, packManager, feedbackManager, args);
267
+ case 'drift_pack':
268
+ return await handlePack(packManager, args);
269
+ case 'drift_feedback':
270
+ return await handleFeedback(feedbackManager, args);
271
+ default:
272
+ return {
273
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
274
+ isError: true,
275
+ };
276
+ }
277
+ }
278
+ catch (error) {
279
+ return {
280
+ content: [{
281
+ type: 'text',
282
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
283
+ }],
284
+ isError: true,
285
+ };
286
+ }
287
+ });
288
+ return server;
289
+ }
290
+ async function handleStatus(store) {
291
+ await store.initialize();
292
+ const stats = store.getStats();
293
+ const result = {
294
+ totalPatterns: stats.totalPatterns,
295
+ byCategory: stats.byCategory,
296
+ byConfidence: stats.byConfidenceLevel,
297
+ byStatus: stats.byStatus,
298
+ totalLocations: stats.totalLocations,
299
+ totalOutliers: stats.totalOutliers,
300
+ categories: PATTERN_CATEGORIES,
301
+ };
302
+ return {
303
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
304
+ };
305
+ }
306
+ async function handlePatterns(store, args) {
307
+ await store.initialize();
308
+ let patterns = store.getAll();
309
+ // Filter by categories
310
+ if (args.categories && args.categories.length > 0) {
311
+ const cats = new Set(args.categories);
312
+ patterns = patterns.filter(p => cats.has(p.category));
313
+ }
314
+ // Filter by confidence
315
+ if (args.minConfidence !== undefined) {
316
+ patterns = patterns.filter(p => p.confidence.score >= args.minConfidence);
317
+ }
318
+ // Format for AI consumption
319
+ const result = patterns.map(p => ({
320
+ id: p.id,
321
+ name: p.name,
322
+ category: p.category,
323
+ subcategory: p.subcategory,
324
+ description: p.description,
325
+ confidence: p.confidence.score,
326
+ confidenceLevel: p.confidence.level,
327
+ locationCount: p.locations.length,
328
+ outlierCount: p.outliers.length,
329
+ exampleLocations: p.locations.slice(0, 3).map(l => ({
330
+ file: l.file,
331
+ line: l.line,
332
+ })),
333
+ }));
334
+ return {
335
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
336
+ };
337
+ }
338
+ async function handleFiles(store, args) {
339
+ await store.load();
340
+ const query = {
341
+ path: args.path,
342
+ };
343
+ if (args.category) {
344
+ query.category = args.category;
345
+ }
346
+ const result = store.queryFile(query);
347
+ if (!result) {
348
+ return {
349
+ content: [{ type: 'text', text: `No patterns found in "${args.path}"` }],
350
+ };
351
+ }
352
+ return {
353
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
354
+ };
355
+ }
356
+ async function handleWhere(store, args) {
357
+ await store.initialize();
358
+ const searchTerm = args.pattern.toLowerCase();
359
+ let patterns = store.getAll().filter(p => p.id.toLowerCase().includes(searchTerm) ||
360
+ p.name.toLowerCase().includes(searchTerm));
361
+ if (args.category) {
362
+ patterns = patterns.filter(p => p.category === args.category);
363
+ }
364
+ const result = patterns.map(p => ({
365
+ id: p.id,
366
+ name: p.name,
367
+ category: p.category,
368
+ locations: p.locations.map(l => ({
369
+ file: l.file,
370
+ line: l.line,
371
+ column: l.column,
372
+ })),
373
+ }));
374
+ return {
375
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
376
+ };
377
+ }
378
+ async function handleExport(store, args) {
379
+ await store.initialize();
380
+ let patterns = store.getAll();
381
+ // Filter by categories
382
+ if (args.categories && args.categories.length > 0) {
383
+ const cats = new Set(args.categories);
384
+ patterns = patterns.filter(p => cats.has(p.category));
385
+ }
386
+ const format = args.format ?? 'ai-context';
387
+ if (format === 'ai-context') {
388
+ // Optimized format for LLM consumption
389
+ const grouped = new Map();
390
+ for (const p of patterns) {
391
+ if (!grouped.has(p.category)) {
392
+ grouped.set(p.category, []);
393
+ }
394
+ grouped.get(p.category).push(p);
395
+ }
396
+ let output = '# Codebase Patterns\n\n';
397
+ output += `Total: ${patterns.length} patterns across ${grouped.size} categories\n\n`;
398
+ for (const [category, categoryPatterns] of grouped) {
399
+ output += `## ${category.toUpperCase()}\n\n`;
400
+ for (const p of categoryPatterns) {
401
+ output += `### ${p.name}\n`;
402
+ output += `- Confidence: ${(p.confidence.score * 100).toFixed(0)}%\n`;
403
+ output += `- Found in ${p.locations.length} locations\n`;
404
+ if (p.description) {
405
+ output += `- ${p.description}\n`;
406
+ }
407
+ if (!args.compact && p.locations.length > 0) {
408
+ output += `- Examples: ${p.locations.slice(0, 2).map(l => l.file).join(', ')}\n`;
409
+ }
410
+ output += '\n';
411
+ }
412
+ }
413
+ return {
414
+ content: [{ type: 'text', text: output }],
415
+ };
416
+ }
417
+ if (format === 'summary') {
418
+ const stats = store.getStats();
419
+ return {
420
+ content: [{
421
+ type: 'text',
422
+ text: JSON.stringify({
423
+ totalPatterns: stats.totalPatterns,
424
+ byCategory: stats.byCategory,
425
+ byConfidence: stats.byConfidenceLevel,
426
+ }, null, 2),
427
+ }],
428
+ };
429
+ }
430
+ // JSON format
431
+ return {
432
+ content: [{ type: 'text', text: JSON.stringify(patterns, null, 2) }],
433
+ };
434
+ }
435
+ async function handleContracts(projectRoot, args) {
436
+ const fs = await import('node:fs/promises');
437
+ const path = await import('node:path');
438
+ const contractsDir = path.join(projectRoot, '.drift', 'contracts');
439
+ try {
440
+ const dirs = ['discovered', 'verified', 'mismatch'];
441
+ const contracts = [];
442
+ for (const dir of dirs) {
443
+ if (args.status && args.status !== 'all' && args.status !== dir) {
444
+ continue;
445
+ }
446
+ const dirPath = path.join(contractsDir, dir);
447
+ try {
448
+ const files = await fs.readdir(dirPath);
449
+ for (const file of files) {
450
+ if (file.endsWith('.json') && !file.startsWith('.')) {
451
+ const content = await fs.readFile(path.join(dirPath, file), 'utf-8');
452
+ const data = JSON.parse(content);
453
+ contracts.push({ status: dir, contracts: data.contracts ?? [] });
454
+ }
455
+ }
456
+ }
457
+ catch {
458
+ // Directory doesn't exist
459
+ }
460
+ }
461
+ return {
462
+ content: [{ type: 'text', text: JSON.stringify(contracts, null, 2) }],
463
+ };
464
+ }
465
+ catch (error) {
466
+ return {
467
+ content: [{
468
+ type: 'text',
469
+ text: `No contracts found. Run \`drift scan\` first.`,
470
+ }],
471
+ };
472
+ }
473
+ }
474
+ async function handleExamples(projectRoot, store, packManager, feedbackManager, args) {
475
+ const fs = await import('node:fs/promises');
476
+ const path = await import('node:path');
477
+ // Require at least one filter to avoid processing all 800+ patterns
478
+ if ((!args.categories || args.categories.length === 0) && !args.pattern) {
479
+ return {
480
+ content: [{
481
+ type: 'text',
482
+ text: 'Error: Please specify at least one filter:\n' +
483
+ '- categories: ["auth", "security", "api"] etc.\n' +
484
+ '- pattern: "middleware", "token", "rate-limit" etc.\n\n' +
485
+ 'Valid categories: structural, components, styling, api, auth, errors, ' +
486
+ 'data-access, testing, logging, security, config, types, performance, ' +
487
+ 'accessibility, documentation',
488
+ }],
489
+ isError: true,
490
+ };
491
+ }
492
+ await store.initialize();
493
+ await packManager.initialize();
494
+ await feedbackManager.initialize();
495
+ // Track usage for pack learning
496
+ if (args.categories && args.categories.length > 0) {
497
+ await packManager.trackUsage({
498
+ categories: args.categories,
499
+ patterns: args.pattern ? [args.pattern] : undefined,
500
+ timestamp: new Date().toISOString(),
501
+ context: 'code_generation',
502
+ });
503
+ }
504
+ const maxExamples = args.maxExamples ?? 3;
505
+ const contextLines = args.contextLines ?? 10;
506
+ const includeDeprecated = args.includeDeprecated ?? false;
507
+ let patterns = store.getAll();
508
+ // Filter by categories
509
+ if (args.categories && args.categories.length > 0) {
510
+ const cats = new Set(args.categories);
511
+ patterns = patterns.filter(p => cats.has(p.category));
512
+ }
513
+ // Filter by pattern name/id
514
+ if (args.pattern) {
515
+ const searchTerm = args.pattern.toLowerCase();
516
+ patterns = patterns.filter(p => p.id.toLowerCase().includes(searchTerm) ||
517
+ p.name.toLowerCase().includes(searchTerm) ||
518
+ p.subcategory.toLowerCase().includes(searchTerm));
519
+ }
520
+ // Deduplicate patterns by subcategory to get unique pattern types
521
+ const uniquePatterns = new Map();
522
+ for (const p of patterns) {
523
+ const key = `${p.category}/${p.subcategory}`;
524
+ if (!uniquePatterns.has(key) || p.locations.length > uniquePatterns.get(key).locations.length) {
525
+ uniquePatterns.set(key, p);
526
+ }
527
+ }
528
+ // Limit to 20 unique patterns max to avoid timeout
529
+ const limitedPatterns = Array.from(uniquePatterns.entries()).slice(0, 20);
530
+ // File filtering patterns (same as packs.ts)
531
+ const EXAMPLE_EXCLUDE_PATTERNS = [
532
+ /README/i, /CHANGELOG/i, /CONTRIBUTING/i, /LICENSE/i, /\.md$/i,
533
+ /\.github\//, /\.gitlab\//, /\.ya?ml$/i, /\.toml$/i,
534
+ /Dockerfile/i, /docker-compose/i,
535
+ /package\.json$/i, /package-lock\.json$/i, /pnpm-lock\.yaml$/i,
536
+ /requirements\.txt$/i, /pyproject\.toml$/i,
537
+ /\.env/i, /dist\//, /build\//, /node_modules\//,
538
+ ];
539
+ const DEPRECATION_MARKERS = [
540
+ /DEPRECATED/i, /LEGACY/i, /@deprecated/i,
541
+ /TODO:\s*remove/i, /REMOVAL:\s*planned/i,
542
+ /backward.?compat/i, /will be removed/i,
543
+ ];
544
+ const shouldExcludeFile = (filePath) => {
545
+ // Check feedback-based exclusion first
546
+ if (feedbackManager.shouldExcludeFile(filePath)) {
547
+ return true;
548
+ }
549
+ return EXAMPLE_EXCLUDE_PATTERNS.some(pattern => pattern.test(filePath));
550
+ };
551
+ const scoreLocation = (filePath) => {
552
+ let score = 1.0;
553
+ // Apply feedback-based scoring
554
+ score *= feedbackManager.getFileScore(filePath);
555
+ // Apply heuristic scoring
556
+ if (/\.md$/i.test(filePath))
557
+ score *= 0.1;
558
+ if (/README/i.test(filePath))
559
+ score *= 0.1;
560
+ if (/\.ya?ml$/i.test(filePath))
561
+ score *= 0.2;
562
+ if (/\.json$/i.test(filePath))
563
+ score *= 0.3;
564
+ if (/\.(ts|tsx|js|jsx|py|rb|go|rs|java)$/i.test(filePath))
565
+ score *= 1.5;
566
+ if (/\/src\//i.test(filePath))
567
+ score *= 1.3;
568
+ if (/\.(test|spec)\./i.test(filePath))
569
+ score *= 0.7;
570
+ return score;
571
+ };
572
+ // Read actual code snippets
573
+ const fileCache = new Map();
574
+ const fileContentCache = new Map();
575
+ let excludedCount = 0;
576
+ let deprecatedCount = 0;
577
+ async function getFileLines(filePath) {
578
+ if (fileCache.has(filePath)) {
579
+ return fileCache.get(filePath);
580
+ }
581
+ try {
582
+ const fullPath = path.join(projectRoot, filePath);
583
+ const content = await fs.readFile(fullPath, 'utf-8');
584
+ const lines = content.split('\n');
585
+ fileCache.set(filePath, lines);
586
+ fileContentCache.set(filePath, content);
587
+ return lines;
588
+ }
589
+ catch {
590
+ return [];
591
+ }
592
+ }
593
+ function extractSnippet(lines, startLine, endLine) {
594
+ const start = Math.max(0, startLine - contextLines - 1);
595
+ const end = Math.min(lines.length, (endLine ?? startLine) + contextLines);
596
+ return lines.slice(start, end).join('\n');
597
+ }
598
+ function isDeprecatedContent(content) {
599
+ const header = content.slice(0, 500);
600
+ return DEPRECATION_MARKERS.some(pattern => pattern.test(header));
601
+ }
602
+ const results = [];
603
+ for (const [, pattern] of limitedPatterns) {
604
+ const examples = [];
605
+ // Sort locations by quality score and filter excluded files
606
+ const scoredLocations = pattern.locations
607
+ .map(loc => ({ loc, score: scoreLocation(loc.file) }))
608
+ .filter(({ loc }) => !shouldExcludeFile(loc.file))
609
+ .sort((a, b) => b.score - a.score);
610
+ excludedCount += pattern.locations.length - scoredLocations.length;
611
+ // Get unique files to avoid duplicate examples from same file
612
+ const seenFiles = new Set();
613
+ for (const { loc } of scoredLocations) {
614
+ if (seenFiles.has(loc.file))
615
+ continue;
616
+ if (examples.length >= maxExamples)
617
+ break;
618
+ const lines = await getFileLines(loc.file);
619
+ if (lines.length === 0)
620
+ continue;
621
+ // Check for deprecation markers
622
+ const content = fileContentCache.get(loc.file) || '';
623
+ if (!includeDeprecated && isDeprecatedContent(content)) {
624
+ deprecatedCount++;
625
+ continue;
626
+ }
627
+ const snippet = extractSnippet(lines, loc.line, loc.endLine);
628
+ if (snippet.trim()) {
629
+ examples.push({
630
+ file: loc.file,
631
+ line: loc.line,
632
+ code: snippet,
633
+ });
634
+ seenFiles.add(loc.file);
635
+ }
636
+ }
637
+ if (examples.length > 0) {
638
+ results.push({
639
+ category: pattern.category,
640
+ subcategory: pattern.subcategory,
641
+ patternName: pattern.name,
642
+ description: pattern.description,
643
+ confidence: pattern.confidence.score,
644
+ examples,
645
+ });
646
+ }
647
+ }
648
+ // Format output for AI consumption
649
+ let output = '# Code Pattern Examples\n\n';
650
+ output += `Found ${results.length} unique patterns with code examples.\n`;
651
+ if (excludedCount > 0 || deprecatedCount > 0) {
652
+ output += `*(${excludedCount} non-source files excluded`;
653
+ if (deprecatedCount > 0) {
654
+ output += `, ${deprecatedCount} deprecated files skipped`;
655
+ }
656
+ output += `)*\n`;
657
+ }
658
+ output += '\n';
659
+ // Group by category
660
+ const grouped = new Map();
661
+ for (const r of results) {
662
+ if (!grouped.has(r.category)) {
663
+ grouped.set(r.category, []);
664
+ }
665
+ grouped.get(r.category).push(r);
666
+ }
667
+ for (const [category, categoryResults] of grouped) {
668
+ output += `## ${category.toUpperCase()}\n\n`;
669
+ for (const r of categoryResults) {
670
+ output += `### ${r.subcategory}\n`;
671
+ output += `**${r.patternName}** (${(r.confidence * 100).toFixed(0)}% confidence)\n`;
672
+ if (r.description) {
673
+ output += `${r.description}\n`;
674
+ }
675
+ output += '\n';
676
+ for (const ex of r.examples) {
677
+ output += `**${ex.file}:${ex.line}**\n`;
678
+ output += '```\n';
679
+ output += ex.code;
680
+ output += '\n```\n\n';
681
+ }
682
+ }
683
+ }
684
+ return {
685
+ content: [{ type: 'text', text: output }],
686
+ };
687
+ }
688
+ async function handlePack(packManager, args) {
689
+ await packManager.initialize();
690
+ // Determine action (support legacy 'list' boolean)
691
+ const action = args.action ?? (args.list ? 'list' : (args.name ? 'get' : 'list'));
692
+ switch (action) {
693
+ case 'list': {
694
+ const packs = packManager.getAllPacks();
695
+ let output = '# Available Pattern Packs\n\n';
696
+ output += 'Use `drift_pack` with a pack name to get pre-computed pattern context.\n\n';
697
+ output += '## Actions\n';
698
+ output += '- `action="get"` - Get a pack by name (default)\n';
699
+ output += '- `action="list"` - List all packs\n';
700
+ output += '- `action="suggest"` - Suggest packs based on your usage patterns\n';
701
+ output += '- `action="infer"` - Suggest packs from file structure analysis\n';
702
+ output += '- `action="create"` - Create a custom pack\n';
703
+ output += '- `action="delete"` - Delete a custom pack\n\n';
704
+ for (const pack of packs) {
705
+ output += `## ${pack.name}\n`;
706
+ output += `${pack.description}\n`;
707
+ output += `- Categories: ${pack.categories.join(', ')}\n`;
708
+ if (pack.patterns) {
709
+ output += `- Pattern filters: ${pack.patterns.join(', ')}\n`;
710
+ }
711
+ output += '\n';
712
+ }
713
+ return {
714
+ content: [{ type: 'text', text: output }],
715
+ };
716
+ }
717
+ case 'suggest': {
718
+ const suggestions = await packManager.suggestPacks();
719
+ if (suggestions.length === 0) {
720
+ return {
721
+ content: [{
722
+ type: 'text',
723
+ text: '# No Pack Suggestions Yet\n\n' +
724
+ 'Pack suggestions are based on your usage patterns. Keep using `drift_examples` and `drift_pack` ' +
725
+ 'with different category combinations, and suggestions will appear here.\n\n' +
726
+ 'Alternatively, use `action="infer"` to get suggestions based on file structure analysis.',
727
+ }],
728
+ };
729
+ }
730
+ let output = '# Suggested Packs\n\n';
731
+ output += 'Based on your usage patterns, these category combinations might be useful as packs:\n\n';
732
+ for (const s of suggestions) {
733
+ output += `## ${s.name}\n`;
734
+ output += `${s.description}\n`;
735
+ output += `- Categories: ${s.categories.join(', ')}\n`;
736
+ if (s.patterns && s.patterns.length > 0) {
737
+ output += `- Patterns: ${s.patterns.join(', ')}\n`;
738
+ }
739
+ output += `- Used ${s.usageCount} times (last: ${s.lastUsed})\n\n`;
740
+ output += `To create this pack:\n`;
741
+ output += '```\n';
742
+ output += `drift_pack action="create" name="${s.name}" description="${s.description}" categories=${JSON.stringify(s.categories)}\n`;
743
+ output += '```\n\n';
744
+ }
745
+ return {
746
+ content: [{ type: 'text', text: output }],
747
+ };
748
+ }
749
+ case 'infer': {
750
+ const suggestions = await packManager.inferPacksFromStructure();
751
+ if (suggestions.length === 0) {
752
+ return {
753
+ content: [{
754
+ type: 'text',
755
+ text: '# No Inferred Packs\n\n' +
756
+ 'Could not find significant pattern co-occurrences in the codebase. ' +
757
+ 'This usually means patterns are well-separated by file, or the codebase needs more scanning.',
758
+ }],
759
+ };
760
+ }
761
+ let output = '# Inferred Packs from File Structure\n\n';
762
+ output += 'These category combinations frequently appear together in the same files:\n\n';
763
+ for (const s of suggestions) {
764
+ output += `## ${s.name}\n`;
765
+ output += `${s.description}\n`;
766
+ output += `- Categories: ${s.categories.join(', ')}\n`;
767
+ output += `- Found in ${s.usageCount} files\n\n`;
768
+ output += `To create this pack:\n`;
769
+ output += '```\n';
770
+ output += `drift_pack action="create" name="${s.name}" description="${s.description}" categories=${JSON.stringify(s.categories)}\n`;
771
+ output += '```\n\n';
772
+ }
773
+ return {
774
+ content: [{ type: 'text', text: output }],
775
+ };
776
+ }
777
+ case 'create': {
778
+ if (!args.name) {
779
+ return {
780
+ content: [{
781
+ type: 'text',
782
+ text: 'Error: Pack name is required for create action.\n\n' +
783
+ 'Example: drift_pack action="create" name="my_pack" description="My custom pack" categories=["api", "auth"]',
784
+ }],
785
+ isError: true,
786
+ };
787
+ }
788
+ if (!args.categories || args.categories.length === 0) {
789
+ return {
790
+ content: [{
791
+ type: 'text',
792
+ text: 'Error: At least one category is required for create action.\n\n' +
793
+ `Valid categories: ${PATTERN_CATEGORIES.join(', ')}`,
794
+ }],
795
+ isError: true,
796
+ };
797
+ }
798
+ const packDef = {
799
+ name: args.name,
800
+ description: args.description ?? `Custom pack: ${args.name}`,
801
+ categories: args.categories,
802
+ patterns: args.patterns,
803
+ };
804
+ await packManager.createCustomPack(packDef);
805
+ return {
806
+ content: [{
807
+ type: 'text',
808
+ text: `# Pack Created: ${args.name}\n\n` +
809
+ `Successfully created custom pack "${args.name}".\n\n` +
810
+ `- Categories: ${args.categories.join(', ')}\n` +
811
+ (args.patterns ? `- Patterns: ${args.patterns.join(', ')}\n` : '') +
812
+ `\nUse \`drift_pack name="${args.name}"\` to get the pack content.`,
813
+ }],
814
+ };
815
+ }
816
+ case 'delete': {
817
+ if (!args.name) {
818
+ return {
819
+ content: [{
820
+ type: 'text',
821
+ text: 'Error: Pack name is required for delete action.',
822
+ }],
823
+ isError: true,
824
+ };
825
+ }
826
+ const deleted = await packManager.deleteCustomPack(args.name);
827
+ if (!deleted) {
828
+ return {
829
+ content: [{
830
+ type: 'text',
831
+ text: `Error: Pack "${args.name}" not found or is a built-in pack (cannot delete built-in packs).`,
832
+ }],
833
+ isError: true,
834
+ };
835
+ }
836
+ return {
837
+ content: [{
838
+ type: 'text',
839
+ text: `Successfully deleted custom pack "${args.name}".`,
840
+ }],
841
+ };
842
+ }
843
+ case 'get':
844
+ default: {
845
+ if (!args.name) {
846
+ // Fall back to list if no name provided
847
+ return handlePack(packManager, { action: 'list' });
848
+ }
849
+ try {
850
+ const result = await packManager.getPackContent(args.name, {
851
+ refresh: args.refresh ?? false,
852
+ });
853
+ // Track usage for learning
854
+ const pack = packManager.getPack(args.name);
855
+ if (pack) {
856
+ await packManager.trackUsage({
857
+ categories: pack.categories,
858
+ patterns: pack.patterns,
859
+ timestamp: new Date().toISOString(),
860
+ context: 'code_generation',
861
+ });
862
+ }
863
+ let header = '';
864
+ if (result.fromCache) {
865
+ header = `<!-- Served from cache (generated: ${result.generatedAt}) -->\n\n`;
866
+ }
867
+ else if (result.staleReason) {
868
+ header = `<!-- Regenerated: ${result.staleReason} -->\n\n`;
869
+ }
870
+ else {
871
+ header = `<!-- Freshly generated -->\n\n`;
872
+ }
873
+ return {
874
+ content: [{ type: 'text', text: header + result.content }],
875
+ };
876
+ }
877
+ catch (error) {
878
+ return {
879
+ content: [{
880
+ type: 'text',
881
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
882
+ }],
883
+ isError: true,
884
+ };
885
+ }
886
+ }
887
+ }
888
+ }
889
+ async function handleFeedback(feedbackManager, args) {
890
+ await feedbackManager.initialize();
891
+ const action = args.action ?? 'stats';
892
+ switch (action) {
893
+ case 'rate': {
894
+ if (!args.file || !args.rating) {
895
+ return {
896
+ content: [{
897
+ type: 'text',
898
+ text: 'Error: "file" and "rating" are required for rate action.\n\n' +
899
+ 'Example: drift_feedback action="rate" file="api/routes/auth.py" line=42 ' +
900
+ 'rating="good" patternName="token-handling" category="auth"',
901
+ }],
902
+ isError: true,
903
+ };
904
+ }
905
+ await feedbackManager.recordFeedback({
906
+ patternId: args.patternId ?? 'unknown',
907
+ patternName: args.patternName ?? 'unknown',
908
+ category: args.category ?? 'unknown',
909
+ file: args.file,
910
+ line: args.line ?? 0,
911
+ rating: args.rating,
912
+ reason: args.reason,
913
+ });
914
+ const emoji = args.rating === 'good' ? '👍' : args.rating === 'bad' ? '👎' : '🤷';
915
+ return {
916
+ content: [{
917
+ type: 'text',
918
+ text: `${emoji} Feedback recorded for ${args.file}:${args.line ?? 0}\n\n` +
919
+ `Rating: ${args.rating}\n` +
920
+ (args.reason ? `Reason: ${args.reason}\n` : '') +
921
+ `\nThis feedback will improve future example suggestions.`,
922
+ }],
923
+ };
924
+ }
925
+ case 'stats': {
926
+ const stats = await feedbackManager.getStats();
927
+ let output = '# Example Feedback Statistics\n\n';
928
+ output += `Total feedback: ${stats.totalFeedback}\n`;
929
+ output += `- Good examples: ${stats.goodExamples}\n`;
930
+ output += `- Bad examples: ${stats.badExamples}\n`;
931
+ output += `- Irrelevant: ${stats.irrelevantExamples}\n\n`;
932
+ if (stats.topGoodPatterns.length > 0) {
933
+ output += '## Top Patterns with Good Examples\n';
934
+ for (const p of stats.topGoodPatterns) {
935
+ output += `- ${p.pattern}: ${p.count} good\n`;
936
+ }
937
+ output += '\n';
938
+ }
939
+ if (stats.topBadPatterns.length > 0) {
940
+ output += '## Patterns Needing Improvement\n';
941
+ for (const p of stats.topBadPatterns) {
942
+ output += `- ${p.pattern}: ${p.count} bad\n`;
943
+ }
944
+ output += '\n';
945
+ }
946
+ if (stats.topBadFiles.length > 0) {
947
+ output += '## Files Producing Poor Examples\n';
948
+ output += 'These files are being deprioritized in example selection:\n';
949
+ for (const f of stats.topBadFiles) {
950
+ output += `- ${f.file}: ${f.count} negative ratings\n`;
951
+ }
952
+ }
953
+ return {
954
+ content: [{ type: 'text', text: output }],
955
+ };
956
+ }
957
+ case 'clear': {
958
+ await feedbackManager.clearFeedback();
959
+ return {
960
+ content: [{
961
+ type: 'text',
962
+ text: '✅ All feedback has been cleared. Example scoring reset to defaults.',
963
+ }],
964
+ };
965
+ }
966
+ default:
967
+ return {
968
+ content: [{
969
+ type: 'text',
970
+ text: `Unknown action: ${action}. Valid actions: rate, stats, clear`,
971
+ }],
972
+ isError: true,
973
+ };
974
+ }
975
+ }
976
+ //# sourceMappingURL=server.js.map