From 86a8cc1cdb5e7e7971e879cda35df85701e3fbfd Mon Sep 17 00:00:00 2001 From: prosdev Date: Tue, 31 Mar 2026 00:21:55 -0700 Subject: [PATCH 1/5] refactor(core): extract pure pattern analyzers for testability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract import style, error handling, and type coverage analysis from private class methods into exported pure functions. Class methods now delegate to these, keeping all existing behavior identical. Adds 13 unit tests for the pure extractors — no file I/O needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../pattern-analysis-service.test.ts | 106 ++++++++++- .../src/services/pattern-analysis-service.ts | 170 +++++++----------- 2 files changed, 169 insertions(+), 107 deletions(-) diff --git a/packages/core/src/services/__tests__/pattern-analysis-service.test.ts b/packages/core/src/services/__tests__/pattern-analysis-service.test.ts index a2717fc..660fd7f 100644 --- a/packages/core/src/services/__tests__/pattern-analysis-service.test.ts +++ b/packages/core/src/services/__tests__/pattern-analysis-service.test.ts @@ -8,7 +8,111 @@ import * as fs from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { PatternAnalysisService } from '../pattern-analysis-service'; +import { + extractErrorHandlingFromContent, + extractImportStyleFromContent, + extractTypeCoverageFromSignatures, + PatternAnalysisService, +} from '../pattern-analysis-service'; + +// ======================================================================== +// Pure Pattern Extractors (no I/O) +// ======================================================================== + +describe('Pure Pattern Extractors', () => { + describe('extractImportStyleFromContent', () => { + it('should detect ESM imports', () => { + const content = 'import { foo } from "./bar";\nimport * as baz from "baz";'; + const result = extractImportStyleFromContent(content); + expect(result).toEqual({ style: 'esm', importCount: 2 }); + }); + + it('should detect CJS requires', () => { + const content = 'const foo = require("bar");\nconst baz = require("baz");'; + const result = extractImportStyleFromContent(content); + expect(result).toEqual({ style: 'cjs', importCount: 2 }); + }); + + it('should detect mixed imports', () => { + const content = 'import { foo } from "./bar";\nconst baz = require("baz");'; + const result = extractImportStyleFromContent(content); + expect(result).toEqual({ style: 'mixed', importCount: 2 }); + }); + + it('should return unknown for no imports', () => { + const content = 'const x = 1;'; + const result = extractImportStyleFromContent(content); + expect(result).toEqual({ style: 'unknown', importCount: 0 }); + }); + }); + + describe('extractErrorHandlingFromContent', () => { + it('should detect throw style', () => { + const content = 'throw new Error("oops");\nthrow new TypeError("bad");'; + const result = extractErrorHandlingFromContent(content); + expect(result.style).toBe('throw'); + }); + + it('should detect result style', () => { + const content = 'function foo(): Result { return { ok: true, value: "x" }; }'; + const result = extractErrorHandlingFromContent(content); + expect(result.style).toBe('result'); + }); + + it('should detect mixed style', () => { + const content = 'throw new Error("oops");\nfunction foo(): Result {}'; + const result = extractErrorHandlingFromContent(content); + expect(result.style).toBe('mixed'); + }); + + it('should return unknown for no error handling', () => { + const content = 'const x = 1;'; + const result = extractErrorHandlingFromContent(content); + expect(result.style).toBe('unknown'); + }); + }); + + describe('extractTypeCoverageFromSignatures', () => { + it('should detect full coverage', () => { + const signatures = ['function foo(x: string): number', 'function bar(y: boolean): void']; + const result = extractTypeCoverageFromSignatures(signatures); + expect(result.coverage).toBe('full'); + expect(result.annotatedCount).toBe(2); + expect(result.totalCount).toBe(2); + }); + + it('should detect partial coverage', () => { + const signatures = ['function foo(x: string): number', 'function bar(y)']; + const result = extractTypeCoverageFromSignatures(signatures); + expect(result.coverage).toBe('partial'); + }); + + it('should detect minimal coverage', () => { + const signatures = ['function foo(x)', 'function bar(y)', 'function baz(z: string): number']; + const result = extractTypeCoverageFromSignatures(signatures); + expect(result.coverage).toBe('minimal'); + expect(result.annotatedCount).toBe(1); + expect(result.totalCount).toBe(3); + }); + + it('should return none for no signatures', () => { + const result = extractTypeCoverageFromSignatures([]); + expect(result.coverage).toBe('none'); + expect(result.annotatedCount).toBe(0); + expect(result.totalCount).toBe(0); + }); + + it('should return none when no signatures have types', () => { + const signatures = ['function foo(x)', 'function bar(y)']; + const result = extractTypeCoverageFromSignatures(signatures); + expect(result.coverage).toBe('none'); + }); + }); +}); + +// ======================================================================== +// PatternAnalysisService (integration — uses file I/O) +// ======================================================================== describe('PatternAnalysisService', () => { let tempDir: string; diff --git a/packages/core/src/services/pattern-analysis-service.ts b/packages/core/src/services/pattern-analysis-service.ts index 6b3d9c7..58023f5 100644 --- a/packages/core/src/services/pattern-analysis-service.ts +++ b/packages/core/src/services/pattern-analysis-service.ts @@ -43,6 +43,64 @@ export type { TypeAnnotationPattern, } from './pattern-analysis-types'; +// ======================================================================== +// Pure Pattern Extractors — no I/O, fully testable +// ======================================================================== + +/** + * Extract import style from raw file content. + */ +export function extractImportStyleFromContent(content: string): ImportStylePattern { + const esmImports = content.match(/^import\s/gm) || []; + const cjsImports = content.match(/require\s*\(/g) || []; + const hasESM = esmImports.length > 0; + const hasCJS = cjsImports.length > 0; + + if (!hasESM && !hasCJS) return { style: 'unknown', importCount: 0 }; + + const importCount = esmImports.length + cjsImports.length; + const style: ImportStylePattern['style'] = hasESM && hasCJS ? 'mixed' : hasESM ? 'esm' : 'cjs'; + return { style, importCount }; +} + +/** + * Extract error handling pattern from raw file content. + */ +export function extractErrorHandlingFromContent(content: string): ErrorHandlingPattern { + const counts = { + throw: [...content.matchAll(/throw\s+new\s+\w*Error/g)].length, + result: [...content.matchAll(/Result<|{\s*ok:\s*(true|false)/g)].length, + errorReturn: [...content.matchAll(/\)\s*:\s*\([^)]*,\s*error\)/g)].length, + }; + const total = counts.throw + counts.result + counts.errorReturn; + if (total === 0) return { style: 'unknown', examples: [] }; + + const max = Math.max(counts.throw, counts.result, counts.errorReturn); + const hasMultiple = Object.values(counts).filter((c) => c > 0).length > 1; + let style: ErrorHandlingPattern['style'] = 'unknown'; + if (hasMultiple) style = 'mixed'; + else if (counts.throw === max) style = 'throw'; + else if (counts.result === max) style = 'result'; + else if (counts.errorReturn === max) style = 'error-return'; + return { style, examples: [] }; +} + +/** + * Extract type coverage from function/method signatures. + */ +export function extractTypeCoverageFromSignatures(signatures: string[]): TypeAnnotationPattern { + if (signatures.length === 0) return { coverage: 'none', annotatedCount: 0, totalCount: 0 }; + + const annotated = signatures.filter((sig) => /(\)|=>)\s*:\s*\w+/.test(sig)); + const ratio = annotated.length / signatures.length; + let coverage: TypeAnnotationPattern['coverage']; + if (ratio >= 0.9) coverage = 'full'; + else if (ratio >= 0.5) coverage = 'partial'; + else if (ratio > 0) coverage = 'minimal'; + else coverage = 'none'; + return { coverage, annotatedCount: annotated.length, totalCount: signatures.length }; +} + /** * Pattern Analysis Service * @@ -196,126 +254,26 @@ export class PatternAnalysisService { filePath: string, _documents: Document[] ): Promise { - // Always analyze raw content for maximum reliability - // Scanner extraction can be incomplete for test files or unusual syntax const fullPath = path.join(this.config.repositoryPath, filePath); const content = await fs.readFile(fullPath, 'utf-8'); - - return this.analyzeImportsFromContent(content); - } - - /** - * Analyze imports from raw file content (fallback method) - */ - private analyzeImportsFromContent(content: string): ImportStylePattern { - // Count actual imports (not exports) - const esmImports = content.match(/^import\s/gm) || []; - const cjsImports = content.match(/require\s*\(/g) || []; - - const hasESM = esmImports.length > 0; - const hasCJS = cjsImports.length > 0; - - if (!hasESM && !hasCJS) { - return { style: 'unknown', importCount: 0 }; - } - - const importCount = esmImports.length + cjsImports.length; - - let style: ImportStylePattern['style']; - if (hasESM && hasCJS) { - style = 'mixed'; - } else if (hasESM) { - style = 'esm'; - } else { - style = 'cjs'; - } - - return { style, importCount }; + return extractImportStyleFromContent(content); } /** * Analyze error handling patterns in file content - * - * Detects: throw, Result, error returns (Go style) */ private analyzeErrorHandling(content: string): ErrorHandlingPattern { - const patterns = { - throw: /throw\s+new\s+\w*Error/g, - result: /Result<|{\s*ok:\s*(true|false)/g, - errorReturn: /\)\s*:\s*\([^)]*,\s*error\)/g, // Go: (val, error) - }; - - const matches = { - throw: [...content.matchAll(patterns.throw)], - result: [...content.matchAll(patterns.result)], - errorReturn: [...content.matchAll(patterns.errorReturn)], - }; - - const counts = { - throw: matches.throw.length, - result: matches.result.length, - errorReturn: matches.errorReturn.length, - }; - - // Determine primary style - const total = counts.throw + counts.result + counts.errorReturn; - if (total === 0) { - return { style: 'unknown', examples: [] }; - } - - const max = Math.max(counts.throw, counts.result, counts.errorReturn); - const hasMultiple = Object.values(counts).filter((c) => c > 0).length > 1; - - let style: ErrorHandlingPattern['style'] = 'unknown'; - if (hasMultiple) { - style = 'mixed'; - } else if (counts.throw === max) { - style = 'throw'; - } else if (counts.result === max) { - style = 'result'; - } else if (counts.errorReturn === max) { - style = 'error-return'; - } - - return { style, examples: [] }; + return extractErrorHandlingFromContent(content); } /** * Analyze type annotation coverage from documents - * - * Checks function/method signatures for explicit types. */ private analyzeTypes(documents: Document[]): TypeAnnotationPattern { - const functions = documents.filter((d) => d.type === 'function' || d.type === 'method'); - - if (functions.length === 0) { - return { coverage: 'none', annotatedCount: 0, totalCount: 0 }; - } - - // Check if signatures have explicit return types (contains ': ' after params) - const annotated = functions.filter((d) => { - const sig = d.metadata.signature || ''; - // Look for ': Type' pattern after closing paren or arrow - return /(\)|=>)\s*:\s*\w+/.test(sig); - }); - - const coverage = annotated.length / functions.length; - let coverageLevel: TypeAnnotationPattern['coverage']; - if (coverage >= 0.9) { - coverageLevel = 'full'; - } else if (coverage >= 0.5) { - coverageLevel = 'partial'; - } else if (coverage > 0) { - coverageLevel = 'minimal'; - } else { - coverageLevel = 'none'; - } - - return { - coverage: coverageLevel, - annotatedCount: annotated.length, - totalCount: functions.length, - }; + const signatures = documents + .filter((d) => d.type === 'function' || d.type === 'method') + .map((d) => d.metadata.signature || ''); + return extractTypeCoverageFromSignatures(signatures); } // ======================================================================== From 37c86b70f988035785ead590fe7a54f1cca44133 Mon Sep 17 00:00:00 2001 From: prosdev Date: Tue, 31 Mar 2026 00:36:23 -0700 Subject: [PATCH 2/5] feat(core,mcp): use Antfly index for pattern analysis (10-30x faster) Add getDocsByFilePath to VectorStorage and analyzeFileFromIndex to PatternAnalysisService. When vectorStorage is available, comparePatterns reads signatures from the Antfly index instead of re-scanning with ts-morph. Falls back to scanner for tests/offline. Wire VectorStorage through InspectAdapter so dev_patterns uses the fast path in production. Add scratchpad tracking the 5k doc cap as a known limitation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/scratchpad.md | 22 +++ .../pattern-analysis-service.test.ts | 149 +++++++++++++++++- .../src/services/pattern-analysis-service.ts | 108 +++++++++---- .../src/services/pattern-analysis-types.ts | 1 + packages/core/src/vector/index.ts | 32 ++++ packages/mcp-server/bin/dev-agent-mcp.ts | 1 + .../src/adapters/built-in/inspect-adapter.ts | 3 + 7 files changed, 283 insertions(+), 33 deletions(-) create mode 100644 .claude/scratchpad.md diff --git a/.claude/scratchpad.md b/.claude/scratchpad.md new file mode 100644 index 0000000..8bbea4b --- /dev/null +++ b/.claude/scratchpad.md @@ -0,0 +1,22 @@ +# Scratchpad + +## Known Limitations + +- **`getDocsByFilePath` fetches all docs client-side (capped at 5k).** Uses `getAll(limit: 5000)` + exact path filter. Fine for single repos (dev-agent has ~2,200 docs). Won't scale to monorepos with 50k+ files. Future fix: server-side path filter in Antfly SDK. + +## Open Questions + +- Can Antfly SDK support server-side path filtering? Would eliminate the 5k doc cap in `getDocsByFilePath`. Worth raising with Antfly team after MCP Phase 1 ships. + +## Future Work + +- Antfly SDK: server-side path filter for `getDocsByFilePath` (eliminates 5k cap) +- `dev_patterns format: "json"` for token-efficient agent output (MCP Phase 1, Part 1.4) +- ast-grep as optional dep for pattern analysis (MCP Phase 1, Part 1.5) +- PageRank for `dev_map` hot paths (MCP Phase 1, Part 1.6) +- E2E tests in CI — blocked on Antfly memory requirements vs GitHub runner limits (7GB) + +## Notes + +- Both pattern analysis paths (index vs scan) must use the same pure extractors from 1.1 to avoid drift. Test this explicitly. +- Log which path is used (index vs scanner) at debug level so we can verify the fast path fires in production. diff --git a/packages/core/src/services/__tests__/pattern-analysis-service.test.ts b/packages/core/src/services/__tests__/pattern-analysis-service.test.ts index 660fd7f..6092f77 100644 --- a/packages/core/src/services/__tests__/pattern-analysis-service.test.ts +++ b/packages/core/src/services/__tests__/pattern-analysis-service.test.ts @@ -7,7 +7,7 @@ import * as fs from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { extractErrorHandlingFromContent, extractImportStyleFromContent, @@ -110,6 +110,153 @@ describe('Pure Pattern Extractors', () => { }); }); +// ======================================================================== +// analyzeFileFromIndex (index-based, no ts-morph) +// ======================================================================== + +describe('analyzeFileFromIndex', () => { + let tempDir: string; + let service: PatternAnalysisService; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'analyze-from-index-')); + service = new PatternAnalysisService({ repositoryPath: tempDir }); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('should extract patterns from indexed metadata', async () => { + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile( + path.join(tempDir, 'src/test.ts'), + 'import { foo } from "./bar";\n\nexport function test(): string {\n throw new Error("oops");\n return "hello";\n}\n' + ); + + const indexedDocs = [ + { + id: 'src/test.ts:test:3', + score: 0.9, + metadata: { + path: 'src/test.ts', + type: 'function', + name: 'test', + signature: 'function test(): string', + exported: true, + }, + }, + ]; + + const result = await service.analyzeFileFromIndex('src/test.ts', indexedDocs); + expect(result.importStyle.style).toBe('esm'); + expect(result.typeAnnotations.coverage).toBe('full'); + expect(result.fileSize.lines).toBe(7); + expect(result.errorHandling.style).toBe('throw'); + }); + + it('should handle files with no indexed docs', async () => { + await fs.writeFile(path.join(tempDir, 'empty.ts'), 'const x = 1;\n'); + const result = await service.analyzeFileFromIndex('empty.ts', []); + expect(result.typeAnnotations.coverage).toBe('none'); + expect(result.typeAnnotations.totalCount).toBe(0); + }); + + it('should handle deleted files gracefully (ENOENT)', async () => { + const result = await service.analyzeFileFromIndex('nonexistent.ts', []); + expect(result.fileSize.lines).toBe(0); + expect(result.fileSize.bytes).toBe(0); + expect(result.importStyle.style).toBe('unknown'); + expect(result.errorHandling.style).toBe('unknown'); + }); +}); + +// ======================================================================== +// comparePatterns with vectorStorage (fast path) +// ======================================================================== + +describe('comparePatterns with vectorStorage', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'compare-index-')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('should use index path when vectorStorage is provided', async () => { + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile( + path.join(tempDir, 'src/target.ts'), + 'import { x } from "./y";\nexport function target(): string { throw new Error("oops"); }\n' + ); + await fs.writeFile( + path.join(tempDir, 'src/similar.ts'), + 'import { a } from "./b";\nexport function similar(): number { throw new Error("bad"); }\n' + ); + + const mockGetDocsByFilePath = vi.fn().mockResolvedValue( + new Map([ + [ + 'src/target.ts', + [ + { + id: 'src/target.ts:target:2', + score: 0.9, + metadata: { + path: 'src/target.ts', + type: 'function', + signature: 'function target(): string', + }, + }, + ], + ], + [ + 'src/similar.ts', + [ + { + id: 'src/similar.ts:similar:2', + score: 0.9, + metadata: { + path: 'src/similar.ts', + type: 'function', + signature: 'function similar(): number', + }, + }, + ], + ], + ]) + ); + + const mockVectorStorage = { getDocsByFilePath: mockGetDocsByFilePath } as any; + const serviceWithIndex = new PatternAnalysisService({ + repositoryPath: tempDir, + vectorStorage: mockVectorStorage, + }); + + const result = await serviceWithIndex.comparePatterns('src/target.ts', ['src/similar.ts']); + expect(mockGetDocsByFilePath).toHaveBeenCalledWith(['src/target.ts', 'src/similar.ts']); + expect(result.importStyle.yourFile).toBe('esm'); + expect(result.typeAnnotations.yourFile).toBe('full'); + }); + + it('should handle empty results from index gracefully', async () => { + await fs.writeFile(path.join(tempDir, 'solo.ts'), 'const x = 1;\n'); + + const mockGetDocsByFilePath = vi.fn().mockResolvedValue(new Map()); + const mockVectorStorage = { getDocsByFilePath: mockGetDocsByFilePath } as any; + const serviceWithIndex = new PatternAnalysisService({ + repositoryPath: tempDir, + vectorStorage: mockVectorStorage, + }); + + const result = await serviceWithIndex.comparePatterns('solo.ts', []); + expect(result.fileSize.yourFile).toBeGreaterThan(0); + }); +}); + // ======================================================================== // PatternAnalysisService (integration — uses file I/O) // ======================================================================== diff --git a/packages/core/src/services/pattern-analysis-service.ts b/packages/core/src/services/pattern-analysis-service.ts index 58023f5..e48f7a1 100644 --- a/packages/core/src/services/pattern-analysis-service.ts +++ b/packages/core/src/services/pattern-analysis-service.ts @@ -10,6 +10,7 @@ import * as path from 'node:path'; import { scanRepository } from '../scanner'; import type { Document } from '../scanner/types'; import { findTestFile, isTestFile } from '../utils/test-utils'; +import type { SearchResult } from '../vector/types'; import type { ErrorHandlingComparison, ErrorHandlingPattern, @@ -129,45 +130,88 @@ export class PatternAnalysisService { } /** - * Compare patterns between target file and similar files + * Analyze file patterns using indexed metadata (fast — no ts-morph). * - * OPTIMIZED: Batch scans all files in one pass to avoid repeated ts-morph initialization + * Reads signatures from the Antfly index, content from disk (for line count + * and error handling regex). Falls back gracefully on ENOENT (deleted file). + */ + async analyzeFileFromIndex(filePath: string, indexedDocs: SearchResult[]): Promise { + const fullPath = path.join(this.config.repositoryPath, filePath); + + let content = ''; + let bytes = 0; + let lines = 0; + try { + const [fileContent, stat] = await Promise.all([ + fs.readFile(fullPath, 'utf-8'), + fs.stat(fullPath), + ]); + content = fileContent; + bytes = stat.size; + lines = content.split('\n').length; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + // File deleted between index and analysis — return empty patterns + } + + const testing = await this.analyzeTesting(filePath); + const signatures = indexedDocs + .filter((d) => d.metadata.type === 'function' || d.metadata.type === 'method') + .map((d) => (d.metadata.signature as string) || '') + .filter(Boolean); + + return { + fileSize: { lines, bytes }, + testing, + importStyle: extractImportStyleFromContent(content), + errorHandling: extractErrorHandlingFromContent(content), + typeAnnotations: extractTypeCoverageFromSignatures(signatures), + }; + } + + /** + * Compare patterns between target file and similar files * - * @param targetFile - Target file to analyze - * @param similarFiles - Array of similar file paths - * @returns Pattern comparison results + * Uses Antfly index when vectorStorage is available (fast path, ~100ms). + * Falls back to ts-morph scanning when not (tests, offline). */ async comparePatterns(targetFile: string, similarFiles: string[]): Promise { - // OPTIMIZATION: Batch scan all files at once (5-10x faster than individual scans) const allFiles = [targetFile, ...similarFiles]; - const batchResult = await scanRepository({ - repoRoot: this.config.repositoryPath, - include: allFiles, - }); - - // Group documents by file for fast lookup - const docsByFile = new Map(); - for (const doc of batchResult.documents) { - const file = doc.metadata.file; - if (!docsByFile.has(file)) { - docsByFile.set(file, []); - } - const docs = docsByFile.get(file); - if (docs) { - docs.push(doc); + let targetPatterns: FilePatterns; + let similarPatterns: FilePatterns[]; + + if (this.config.vectorStorage) { + // FAST PATH: read from Antfly index + // Fast path: index-based analysis (~100ms vs 1-3s) + const docsByFile = await this.config.vectorStorage.getDocsByFilePath(allFiles); + + targetPatterns = await this.analyzeFileFromIndex( + targetFile, + docsByFile.get(targetFile) || [] + ); + similarPatterns = await Promise.all( + similarFiles.map((f) => this.analyzeFileFromIndex(f, docsByFile.get(f) || [])) + ); + } else { + // FALLBACK: scan files with ts-morph + // Fallback: ts-morph scan (for tests/offline) + const batchResult = await scanRepository({ + repoRoot: this.config.repositoryPath, + include: allFiles, + }); + + const docsByFile = new Map(); + for (const doc of batchResult.documents) { + const file = doc.metadata.file; + if (!docsByFile.has(file)) docsByFile.set(file, []); + docsByFile.get(file)!.push(doc); } - } - // Analyze target file with cached documents - const targetPatterns = await this.analyzeFileWithDocs( - targetFile, - docsByFile.get(targetFile) || [] - ); - - // Analyze similar files in parallel with cached documents - const similarPatterns = await Promise.all( - similarFiles.map((f) => this.analyzeFileWithDocs(f, docsByFile.get(f) || [])) - ); + targetPatterns = await this.analyzeFileWithDocs(targetFile, docsByFile.get(targetFile) || []); + similarPatterns = await Promise.all( + similarFiles.map((f) => this.analyzeFileWithDocs(f, docsByFile.get(f) || [])) + ); + } return { fileSize: this.compareFileSize( diff --git a/packages/core/src/services/pattern-analysis-types.ts b/packages/core/src/services/pattern-analysis-types.ts index 52099b8..e035275 100644 --- a/packages/core/src/services/pattern-analysis-types.ts +++ b/packages/core/src/services/pattern-analysis-types.ts @@ -122,4 +122,5 @@ export interface PatternComparison { */ export interface PatternAnalysisConfig { repositoryPath: string; + vectorStorage?: import('../vector/index.js').VectorStorage; } diff --git a/packages/core/src/vector/index.ts b/packages/core/src/vector/index.ts index 2f70f6a..4108b12 100644 --- a/packages/core/src/vector/index.ts +++ b/packages/core/src/vector/index.ts @@ -110,6 +110,38 @@ export class VectorStorage { return this.store.getAll(options); } + /** + * Get indexed documents grouped by file path. + * + * Uses getAll with a capped limit + client-side exact path filter. + * More reliable than BM25 search which tokenizes paths unpredictably. + * + * Note: Fetches up to 5,000 docs client-side. Fine for single repos, + * won't scale to monorepos with 50k+ files. See .claude/scratchpad.md. + */ + async getDocsByFilePath(filePaths: string[]): Promise> { + this.assertReady(); + const DOC_LIMIT = 5000; + const pathSet = new Set(filePaths); + const allDocs = await this.getAll({ limit: DOC_LIMIT }); + + if (allDocs.length >= DOC_LIMIT) { + console.error( + `[dev-agent] Warning: getDocsByFilePath hit ${DOC_LIMIT} doc limit. Some files may be missing.` + ); + } + + const byFile = new Map(); + for (const doc of allDocs) { + const docPath = doc.metadata.path as string; + if (pathSet.has(docPath)) { + if (!byFile.has(docPath)) byFile.set(docPath, []); + byFile.get(docPath)!.push(doc); + } + } + return byFile; + } + /** * Get a document by ID */ diff --git a/packages/mcp-server/bin/dev-agent-mcp.ts b/packages/mcp-server/bin/dev-agent-mcp.ts index 67b47aa..bdb97f9 100644 --- a/packages/mcp-server/bin/dev-agent-mcp.ts +++ b/packages/mcp-server/bin/dev-agent-mcp.ts @@ -268,6 +268,7 @@ async function main() { const inspectAdapter = new InspectAdapter({ repositoryPath, searchService, + vectorStorage: indexer.getVectorStorage(), defaultLimit: 10, defaultThreshold: 0.7, defaultFormat: 'compact', diff --git a/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts b/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts index ff30236..1ade9a1 100644 --- a/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts @@ -9,6 +9,7 @@ import { PatternAnalysisService, type PatternComparison, type SearchService, + type VectorStorage, } from '@prosdevlab/dev-agent-core'; import { InspectArgsSchema } from '../../schemas/index.js'; import { ToolAdapter } from '../tool-adapter.js'; @@ -18,6 +19,7 @@ import { validateArgs } from '../validation.js'; export interface InspectAdapterConfig { repositoryPath: string; searchService: SearchService; + vectorStorage?: VectorStorage; defaultLimit?: number; defaultThreshold?: number; defaultFormat?: 'compact' | 'verbose'; @@ -49,6 +51,7 @@ export class InspectAdapter extends ToolAdapter { this.searchService = config.searchService; this.patternService = new PatternAnalysisService({ repositoryPath: config.repositoryPath, + vectorStorage: config.vectorStorage, }); this.defaultLimit = config.defaultLimit ?? 10; this.defaultThreshold = config.defaultThreshold ?? 0.7; From 90cb2bdb4eabe60be650226af7c2c1d264de51ac Mon Sep 17 00:00:00 2001 From: prosdev Date: Tue, 31 Mar 2026 00:42:16 -0700 Subject: [PATCH 3/5] refactor(core,mcp): remove dead code and stale GitHub references - Rewrite analyzeFileWithDocs to use pure extractors directly - Delete 3 now-redundant private wrapper methods - Remove githubStatePath, checkGitHubIndex, githubIndex from health adapter - Clean up health adapter tests (remove 5 GitHub-specific tests) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/services/pattern-analysis-service.ts | 70 +++----------- .../adapters/__tests__/health-adapter.test.ts | 96 ------------------- .../src/adapters/built-in/health-adapter.ts | 69 +------------ 3 files changed, 18 insertions(+), 217 deletions(-) diff --git a/packages/core/src/services/pattern-analysis-service.ts b/packages/core/src/services/pattern-analysis-service.ts index e48f7a1..e85b6d4 100644 --- a/packages/core/src/services/pattern-analysis-service.ts +++ b/packages/core/src/services/pattern-analysis-service.ts @@ -238,46 +238,37 @@ export class PatternAnalysisService { } /** - * Analyze file patterns using pre-scanned documents (faster) - * - * @param filePath - Relative path from repository root - * @param documents - Pre-scanned documents for this file - * @returns Pattern analysis results + * Analyze file patterns using pre-scanned documents (fallback path). */ private async analyzeFileWithDocs( filePath: string, documents: Document[] ): Promise { - // Step 1: Get file stats and content const fullPath = path.join(this.config.repositoryPath, filePath); - const [stat, content] = await Promise.all([fs.stat(fullPath), fs.readFile(fullPath, 'utf-8')]); + const [content, stat, testing] = await Promise.all([ + fs.readFile(fullPath, 'utf-8'), + fs.stat(fullPath), + this.analyzeTesting(filePath), + ]); - const lines = content.split('\n').length; + const signatures = documents + .filter((d) => d.type === 'function' || d.type === 'method') + .map((d) => d.metadata.signature || '') + .filter(Boolean); - // Step 2: Extract all patterns (using cached documents) return { - fileSize: { - lines, - bytes: stat.size, - }, - testing: await this.analyzeTesting(filePath), - importStyle: await this.analyzeImportsFromFile(filePath, documents), - errorHandling: this.analyzeErrorHandling(content), - typeAnnotations: this.analyzeTypes(documents), + fileSize: { lines: content.split('\n').length, bytes: stat.size }, + testing, + importStyle: extractImportStyleFromContent(content), + errorHandling: extractErrorHandlingFromContent(content), + typeAnnotations: extractTypeCoverageFromSignatures(signatures), }; } - // ======================================================================== - // Pattern Extractors (MVP: 5 core patterns) - // ======================================================================== - /** * Analyze test coverage for a file - * - * Checks for co-located test files (*.test.*, *.spec.*) */ private async analyzeTesting(filePath: string): Promise { - // Skip if already a test file if (isTestFile(filePath)) { return { hasTest: false }; } @@ -289,37 +280,6 @@ export class PatternAnalysisService { }; } - /** - * Analyze import style from documents - * - * Always uses content analysis for reliability (scanner may not extract imports from all files). - */ - private async analyzeImportsFromFile( - filePath: string, - _documents: Document[] - ): Promise { - const fullPath = path.join(this.config.repositoryPath, filePath); - const content = await fs.readFile(fullPath, 'utf-8'); - return extractImportStyleFromContent(content); - } - - /** - * Analyze error handling patterns in file content - */ - private analyzeErrorHandling(content: string): ErrorHandlingPattern { - return extractErrorHandlingFromContent(content); - } - - /** - * Analyze type annotation coverage from documents - */ - private analyzeTypes(documents: Document[]): TypeAnnotationPattern { - const signatures = documents - .filter((d) => d.type === 'function' || d.type === 'method') - .map((d) => d.metadata.signature || ''); - return extractTypeCoverageFromSignatures(signatures); - } - // ======================================================================== // Pattern Comparisons // ======================================================================== diff --git a/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts b/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts index 78de9f0..b166fb6 100644 --- a/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts @@ -9,7 +9,6 @@ describe('HealthAdapter', () => { let testDir: string; let vectorStorePath: string; let repositoryPath: string; - let githubStatePath: string; let adapter: HealthAdapter; let context: AdapterContext; let execContext: ToolExecutionContext; @@ -19,8 +18,6 @@ describe('HealthAdapter', () => { testDir = path.join(os.tmpdir(), `health-adapter-test-${Date.now()}`); vectorStorePath = path.join(testDir, 'vectors'); repositoryPath = path.join(testDir, 'repo'); - githubStatePath = path.join(testDir, 'github-state.json'); - await fs.mkdir(testDir, { recursive: true }); await fs.mkdir(vectorStorePath, { recursive: true }); await fs.mkdir(repositoryPath, { recursive: true }); @@ -28,7 +25,6 @@ describe('HealthAdapter', () => { adapter = new HealthAdapter({ repositoryPath, vectorStorePath, - githubStatePath, }); context = { @@ -75,16 +71,6 @@ describe('HealthAdapter', () => { // Setup: Create git repository await fs.mkdir(path.join(repositoryPath, '.git')); - // Setup: Create GitHub index - await fs.writeFile( - githubStatePath, - JSON.stringify({ - version: '1.0.0', - lastIndexed: new Date().toISOString(), - items: [{ id: 1 }, { id: 2 }], - }) - ); - const result = await adapter.execute({}, execContext); expect(result.success).toBe(true); @@ -95,13 +81,11 @@ describe('HealthAdapter', () => { expect(result.data).toContain('HEALTHY'); expect(result.data).toContain('Vector Storage'); expect(result.data).toContain('Repository'); - expect(result.data).toContain('Github Index'); }); it('should report degraded when components have warnings', async () => { // Vector storage is empty (warning) // Repository exists but no .git (warning) - // No GitHub index const result = await adapter.execute({}, execContext); @@ -188,67 +172,6 @@ describe('HealthAdapter', () => { }); }); - describe('GitHub Index Check', () => { - it('should pass when index is recent', async () => { - await fs.writeFile( - githubStatePath, - JSON.stringify({ - version: '1.0.0', - lastIndexed: new Date().toISOString(), - items: [{ id: 1 }, { id: 2 }, { id: 3 }], - }) - ); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Github Index'); - expect(result.data).toContain('3 items'); - }); - - it('should warn when index is stale (>24 hours)', async () => { - const yesterday = new Date(Date.now() - 25 * 60 * 60 * 1000); - await fs.writeFile( - githubStatePath, - JSON.stringify({ - version: '1.0.0', - lastIndexed: yesterday.toISOString(), - items: [{ id: 1 }], - }) - ); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Github Index'); - expect(result.data).toContain('old'); - }); - - it('should warn when index file does not exist', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('not accessible'); - }); - - it('should not check GitHub index when not configured', async () => { - const adapterWithoutGithub = new HealthAdapter({ - repositoryPath, - vectorStorePath, - }); - - await adapterWithoutGithub.initialize(context); - - const result = await adapterWithoutGithub.execute({}, execContext); - - expect(result.success).toBe(true); - // Github Index section should not appear when not configured - expect(result.data).not.toContain('Github Index'); - - await adapterWithoutGithub.shutdown(); - }); - }); - describe('Output Formatting', () => { it('should format uptime correctly', async () => { // Wait a moment to accumulate uptime @@ -309,16 +232,6 @@ describe('HealthAdapter', () => { await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); await fs.mkdir(path.join(repositoryPath, '.git')); - // Add GitHub index to make it fully healthy - await fs.writeFile( - githubStatePath, - JSON.stringify({ - version: '1.0.0', - lastIndexed: new Date().toISOString(), - items: [{ id: 1 }], - }) - ); - const healthy = await adapter.healthCheck(); expect(healthy).toBe(true); @@ -334,15 +247,6 @@ describe('HealthAdapter', () => { }); describe('Error Handling', () => { - it('should handle invalid GitHub state JSON', async () => { - await fs.writeFile(githubStatePath, 'invalid json'); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('⚠️'); - }); - it('should handle permission errors gracefully', async () => { // This test is platform-dependent, so we'll skip it if we can't set permissions if (process.platform !== 'win32') { diff --git a/packages/mcp-server/src/adapters/built-in/health-adapter.ts b/packages/mcp-server/src/adapters/built-in/health-adapter.ts index bc03bc1..6909857 100644 --- a/packages/mcp-server/src/adapters/built-in/health-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/health-adapter.ts @@ -13,7 +13,6 @@ import { validateArgs } from '../validation.js'; export interface HealthCheckConfig { repositoryPath: string; vectorStorePath: string; - githubStatePath?: string; } export interface HealthStatus { @@ -21,7 +20,6 @@ export interface HealthStatus { checks: { vectorStorage: CheckResult; repository: CheckResult; - githubIndex?: CheckResult; }; timestamp: string; uptime: number; // milliseconds @@ -61,7 +59,7 @@ export class HealthAdapter extends ToolAdapter { return { name: 'dev_health', description: - 'Check the health status of the dev-agent MCP server and its dependencies (vector storage, repository, GitHub index)', + 'Check the health status of the dev-agent MCP server and its dependencies (Antfly vector storage, repository access)', inputSchema: { type: 'object', properties: { @@ -114,20 +112,12 @@ export class HealthAdapter extends ToolAdapter { } private async performHealthChecks(verbose: boolean): Promise { - const [vectorStorage, repository, githubIndex] = await Promise.all([ + const [vectorStorage, repository] = await Promise.all([ this.checkVectorStorage(verbose), this.checkRepository(verbose), - this.config.githubStatePath ? this.checkGitHubIndex(verbose) : Promise.resolve(undefined), ]); - const checks: HealthStatus['checks'] = { - vectorStorage, - repository, - }; - - if (githubIndex) { - checks.githubIndex = githubIndex; - } + const checks: HealthStatus['checks'] = { vectorStorage, repository }; return { status: this.getOverallStatus({ checks } as HealthStatus), @@ -211,59 +201,6 @@ export class HealthAdapter extends ToolAdapter { } } - private async checkGitHubIndex(verbose: boolean): Promise { - if (!this.config.githubStatePath) { - return { - status: 'warn', - message: 'GitHub index not configured', - }; - } - - try { - const content = await fs.readFile(this.config.githubStatePath, 'utf-8'); - const state = JSON.parse(content); - - const lastIndexed = state.lastIndexed ? new Date(state.lastIndexed) : null; - const itemCount = state.items?.length ?? 0; - - if (!lastIndexed) { - return { - status: 'warn', - message: 'GitHub index exists but has no lastIndexed timestamp', - details: verbose ? { path: this.config.githubStatePath } : undefined, - }; - } - - const ageMs = Date.now() - lastIndexed.getTime(); - const ageHours = Math.floor(ageMs / (1000 * 60 * 60)); - - // Warn if index is older than 24 hours - if (ageHours > 24) { - return { - status: 'warn', - message: `GitHub index is ${ageHours}h old (${itemCount} items) - consider re-indexing`, - details: verbose - ? { path: this.config.githubStatePath, lastIndexed: lastIndexed.toISOString() } - : undefined, - }; - } - - return { - status: 'pass', - message: `GitHub index operational (${itemCount} items, indexed ${ageHours}h ago)`, - details: verbose - ? { path: this.config.githubStatePath, lastIndexed: lastIndexed.toISOString() } - : undefined, - }; - } catch (error) { - return { - status: 'warn', - message: `GitHub index not accessible: ${error instanceof Error ? error.message : String(error)}`, - details: verbose ? { path: this.config.githubStatePath } : undefined, - }; - } - } - private getOverallStatus(health: HealthStatus): 'healthy' | 'degraded' | 'unhealthy' { const checks = Object.values(health.checks).filter( (check): check is CheckResult => check !== undefined From 2c3ff960b602b9fb6af3f59d52821dbb5ed41ad7 Mon Sep 17 00:00:00 2001 From: prosdev Date: Tue, 31 Mar 2026 00:51:59 -0700 Subject: [PATCH 4/5] =?UTF-8?q?feat(mcp):=20agent=20usability=20overhaul?= =?UTF-8?q?=20=E2=80=94=20merge=20health,=20rename=20params,=20add=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge dev_health into dev_status (section="health") — 6 tools → 5 - Delete HealthAdapter, its tests, and schema - Rename dev_patterns `query` → `filePath` to prevent LLM misuse - Add `format: "json"` to dev_patterns for token-efficient agent workflows - Add negative guidance to dev_patterns description (NOT for search/refs) - Add `suggestion` field to all adapter error responses for agent recovery - Update regression test to expect 5 adapters Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/commands/mcp.ts | 17 +- packages/mcp-server/bin/dev-agent-mcp.ts | 17 +- .../adapters/__tests__/health-adapter.test.ts | 273 ----------------- .../__tests__/inspect-adapter.test.ts | 44 +-- .../__tests__/mcp-tools-regression.test.ts | 30 +- .../src/adapters/built-in/health-adapter.ts | 280 ------------------ .../mcp-server/src/adapters/built-in/index.ts | 1 - .../src/adapters/built-in/inspect-adapter.ts | 74 +++-- .../src/adapters/built-in/map-adapter.ts | 2 +- .../src/adapters/built-in/refs-adapter.ts | 4 +- .../src/adapters/built-in/search-adapter.ts | 3 +- .../src/adapters/built-in/status-adapter.ts | 2 +- .../src/schemas/__tests__/schemas.test.ts | 54 ++-- packages/mcp-server/src/schemas/index.ts | 39 +-- 14 files changed, 118 insertions(+), 722 deletions(-) delete mode 100644 packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts delete mode 100644 packages/mcp-server/src/adapters/built-in/health-adapter.ts diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts index b3553e4..3480c96 100644 --- a/packages/cli/src/commands/mcp.ts +++ b/packages/cli/src/commands/mcp.ts @@ -13,7 +13,6 @@ import { SearchService, } from '@prosdevlab/dev-agent-core'; import { - HealthAdapter, InspectAdapter, MapAdapter, MCPServer, @@ -121,11 +120,6 @@ Available Tools (6): defaultFormat: 'compact', }); - const healthAdapter = new HealthAdapter({ - repositoryPath, - vectorStorePath: vectors, - }); - const refsAdapter = new RefsAdapter({ searchService, defaultLimit: 20, @@ -138,7 +132,7 @@ Available Tools (6): defaultTokenBudget: 2000, }); - // Create MCP server with 6 adapters + // Create MCP server with 5 adapters (health merged into status) const server = new MCPServer({ serverInfo: { name: 'dev-agent', @@ -149,14 +143,7 @@ Available Tools (6): logLevel: logLevel as 'debug' | 'info' | 'warn' | 'error', }, transport: options.transport === 'stdio' ? 'stdio' : undefined, - adapters: [ - searchAdapter, - statusAdapter, - inspectAdapter, - healthAdapter, - refsAdapter, - mapAdapter, - ], + adapters: [searchAdapter, statusAdapter, inspectAdapter, refsAdapter, mapAdapter], }); // Handle graceful shutdown diff --git a/packages/mcp-server/bin/dev-agent-mcp.ts b/packages/mcp-server/bin/dev-agent-mcp.ts index bdb97f9..a252e7a 100644 --- a/packages/mcp-server/bin/dev-agent-mcp.ts +++ b/packages/mcp-server/bin/dev-agent-mcp.ts @@ -13,7 +13,6 @@ import { saveMetadata, } from '@prosdevlab/dev-agent-core'; import { - HealthAdapter, InspectAdapter, MapAdapter, RefsAdapter, @@ -274,11 +273,6 @@ async function main() { defaultFormat: 'compact', }); - const healthAdapter = new HealthAdapter({ - repositoryPath, - vectorStorePath: filePaths.vectors, - }); - const refsAdapter = new RefsAdapter({ searchService, defaultLimit: 20, @@ -291,7 +285,7 @@ async function main() { defaultTokenBudget: 2000, }); - // Create MCP server with 6 adapters + // Create MCP server with 5 adapters (health merged into status) const server = new MCPServer({ serverInfo: { name: 'dev-agent', @@ -302,14 +296,7 @@ async function main() { logLevel, }, transport: 'stdio', - adapters: [ - searchAdapter, - statusAdapter, - inspectAdapter, - healthAdapter, - refsAdapter, - mapAdapter, - ], + adapters: [searchAdapter, statusAdapter, inspectAdapter, refsAdapter, mapAdapter], }); // Start server diff --git a/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts b/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts deleted file mode 100644 index b166fb6..0000000 --- a/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import * as fs from 'node:fs/promises'; -import * as os from 'node:os'; -import * as path from 'node:path'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { HealthAdapter } from '../built-in/health-adapter'; -import type { AdapterContext, ToolExecutionContext } from '../types'; - -describe('HealthAdapter', () => { - let testDir: string; - let vectorStorePath: string; - let repositoryPath: string; - let adapter: HealthAdapter; - let context: AdapterContext; - let execContext: ToolExecutionContext; - - beforeEach(async () => { - // Create temporary directories for testing - testDir = path.join(os.tmpdir(), `health-adapter-test-${Date.now()}`); - vectorStorePath = path.join(testDir, 'vectors'); - repositoryPath = path.join(testDir, 'repo'); - await fs.mkdir(testDir, { recursive: true }); - await fs.mkdir(vectorStorePath, { recursive: true }); - await fs.mkdir(repositoryPath, { recursive: true }); - - adapter = new HealthAdapter({ - repositoryPath, - vectorStorePath, - }); - - context = { - logger: { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - config: { - repositoryPath, - }, - }; - - execContext = { - logger: context.logger, - config: context.config, - }; - - await adapter.initialize(context); - }); - - afterEach(async () => { - await adapter.shutdown(); - await fs.rm(testDir, { recursive: true, force: true }); - }); - - describe('Tool Definition', () => { - it('should provide valid tool definition', () => { - const definition = adapter.getToolDefinition(); - - expect(definition.name).toBe('dev_health'); - expect(definition.description).toContain('health status'); - expect(definition.inputSchema.type).toBe('object'); - expect(definition.inputSchema.properties).toHaveProperty('verbose'); - }); - }); - - describe('Health Checks', () => { - it('should report healthy when all components are operational', async () => { - // Setup: Create vector storage with some files - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test data'); - - // Setup: Create git repository - await fs.mkdir(path.join(repositoryPath, '.git')); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // Check formatted string output - expect(result.data).toContain('✅'); - expect(result.data).toContain('HEALTHY'); - expect(result.data).toContain('Vector Storage'); - expect(result.data).toContain('Repository'); - }); - - it('should report degraded when components have warnings', async () => { - // Vector storage is empty (warning) - // Repository exists but no .git (warning) - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('⚠️'); - expect(result.data).toContain('DEGRADED'); - }); - - it('should report unhealthy when components fail', async () => { - // Delete vector storage to cause failure - await fs.rm(vectorStorePath, { recursive: true }); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('❌'); - expect(result.data).toContain('UNHEALTHY'); - }); - }); - - describe('Vector Storage Check', () => { - it('should pass when vector storage has data', async () => { - await fs.writeFile(path.join(vectorStorePath, 'index.db'), 'data'); - await fs.writeFile(path.join(vectorStorePath, 'vectors.db'), 'data'); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Vector Storage'); - expect(result.data).toContain('2 files'); - }); - - it('should warn when vector storage is empty', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('empty'); - }); - - it('should fail when vector storage does not exist', async () => { - await fs.rm(vectorStorePath, { recursive: true }); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('not accessible'); - }); - - it('should include details in verbose mode', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - - const result = await adapter.execute({ verbose: true }, execContext); - expect(result.success).toBe(true); - // Verbose mode includes more details in the formatted output - expect(result.data).toContain('Vector Storage'); - }); - }); - - describe('Repository Check', () => { - it('should pass when repository is a git repo', async () => { - await fs.mkdir(path.join(repositoryPath, '.git')); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Repository'); - expect(result.data).toContain('Git repository'); - }); - - it('should warn when repository exists but is not a git repo', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('not a Git repository'); - }); - - it('should fail when repository does not exist', async () => { - await fs.rm(repositoryPath, { recursive: true }); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('not accessible'); - }); - }); - - describe('Output Formatting', () => { - it('should format uptime correctly', async () => { - // Wait a moment to accumulate uptime - await new Promise((resolve) => setTimeout(resolve, 10)); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Uptime:'); - }); - - it('should include timestamp', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Timestamp:'); - }); - - it('should format component names nicely', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Vector Storage'); - expect(result.data).toContain('Repository'); - }); - - it('should use appropriate emojis', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - await fs.mkdir(path.join(repositoryPath, '.git')); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('✅'); - }); - - it('should include details in verbose mode', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - - const result = await adapter.execute({ verbose: true }, execContext); - expect(result.success).toBe(true); - - expect(result.data).toContain('Details:'); - }); - - it('should not include details in non-verbose mode', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - - const result = await adapter.execute({ verbose: false }, execContext); - - expect(result.success).toBe(true); - expect(result.data).not.toContain('Details:'); - }); - }); - - describe('Adapter Health Check Method', () => { - it('should return true when healthy', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - await fs.mkdir(path.join(repositoryPath, '.git')); - - const healthy = await adapter.healthCheck(); - - expect(healthy).toBe(true); - }); - - it('should return false when unhealthy', async () => { - await fs.rm(vectorStorePath, { recursive: true }); - - const healthy = await adapter.healthCheck(); - - expect(healthy).toBe(false); - }); - }); - - describe('Error Handling', () => { - it('should handle permission errors gracefully', async () => { - // This test is platform-dependent, so we'll skip it if we can't set permissions - if (process.platform !== 'win32') { - await fs.chmod(vectorStorePath, 0o000); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // Restore permissions for cleanup - await fs.chmod(vectorStorePath, 0o755); - } - }); - }); - - describe('Metadata', () => { - it('should include correct metadata', () => { - expect(adapter.metadata.name).toBe('health-adapter'); - expect(adapter.metadata.version).toBe('1.0.0'); - expect(adapter.metadata.description).toContain('health'); - }); - }); -}); diff --git a/packages/mcp-server/src/adapters/__tests__/inspect-adapter.test.ts b/packages/mcp-server/src/adapters/__tests__/inspect-adapter.test.ts index ec52d20..5d8e4df 100644 --- a/packages/mcp-server/src/adapters/__tests__/inspect-adapter.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/inspect-adapter.test.ts @@ -54,15 +54,21 @@ describe('InspectAdapter', () => { expect(definition.name).toBe('dev_patterns'); expect(definition.description).toContain('pattern'); expect(definition.description).toContain('similar'); - expect(definition.inputSchema.required).toContain('query'); - expect(definition.inputSchema.required).not.toContain('action'); + expect(definition.inputSchema.required).toContain('filePath'); }); - it('should have file path description in query field', () => { + it('should have file path description in filePath field', () => { const definition = adapter.getToolDefinition(); - const queryProp = (definition.inputSchema.properties as any)?.query; + const filePathProp = (definition.inputSchema.properties as any)?.filePath; - expect(queryProp.description.toLowerCase()).toContain('file path'); + expect(filePathProp.description.toLowerCase()).toContain('file path'); + }); + + it('should include negative guidance in description', () => { + const definition = adapter.getToolDefinition(); + + expect(definition.description).toContain('NOT for finding code'); + expect(definition.description).toContain('dev_search'); }); it('should not have an output schema (returns plain markdown)', () => { @@ -74,10 +80,10 @@ describe('InspectAdapter', () => { }); describe('Input Validation', () => { - it('should reject empty query', async () => { + it('should reject empty filePath', async () => { const result = await adapter.execute( { - query: '', + filePath: '', }, mockContext ); @@ -89,7 +95,7 @@ describe('InspectAdapter', () => { it('should reject invalid limit', async () => { const result = await adapter.execute( { - query: 'src/test.ts', + filePath: 'src/test.ts', limit: -1, }, mockContext @@ -111,7 +117,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', limit: 10, format: 'compact', }, @@ -146,7 +152,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); @@ -181,7 +187,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); @@ -202,7 +208,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'README.md', + filePath: 'README.md', }, mockContext ); @@ -246,7 +252,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', limit: 5, }, mockContext @@ -276,7 +282,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', format: 'compact', }, mockContext @@ -303,7 +309,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', format: 'verbose', }, mockContext @@ -322,7 +328,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); @@ -336,7 +342,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'missing-file.ts', + filePath: 'missing-file.ts', }, mockContext ); @@ -350,7 +356,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); @@ -375,7 +381,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); diff --git a/packages/mcp-server/src/adapters/__tests__/mcp-tools-regression.test.ts b/packages/mcp-server/src/adapters/__tests__/mcp-tools-regression.test.ts index e832d66..b41691f 100644 --- a/packages/mcp-server/src/adapters/__tests__/mcp-tools-regression.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/mcp-tools-regression.test.ts @@ -1,8 +1,8 @@ /** - * MCP Tools Regression — verifies exactly 6 built-in adapters survive Phase 2 cleanup. + * MCP Tools Regression — verifies exactly 5 built-in adapters. * - * This test catches accidental re-introduction of removed adapters (History, GitHub, - * Plan, Explore) and ensures no adapters are silently dropped. + * Health was merged into Status (dev_status section="health"). + * This test catches accidental re-introduction of removed adapters. */ import { describe, expect, it } from 'vitest'; @@ -10,23 +10,19 @@ import * as builtIn from '../built-in/index.js'; const adapterNames = Object.keys(builtIn).filter((k) => k.endsWith('Adapter')); -describe('MCP tools regression (post Phase 2)', () => { - it('barrel exports exactly 6 adapter classes', () => { - expect(adapterNames).toHaveLength(6); +describe('MCP tools regression', () => { + it('barrel exports exactly 5 adapter classes', () => { + expect(adapterNames).toHaveLength(5); }); - it.each([ - 'HealthAdapter', - 'InspectAdapter', - 'MapAdapter', - 'RefsAdapter', - 'SearchAdapter', - 'StatusAdapter', - ])('exports %s', (name) => { - expect(adapterNames).toContain(name); - }); + it.each(['InspectAdapter', 'MapAdapter', 'RefsAdapter', 'SearchAdapter', 'StatusAdapter'])( + 'exports %s', + (name) => { + expect(adapterNames).toContain(name); + } + ); - it.each(['HistoryAdapter', 'GitHubAdapter', 'PlanAdapter', 'ExploreAdapter'])( + it.each(['HealthAdapter', 'HistoryAdapter', 'GitHubAdapter', 'PlanAdapter', 'ExploreAdapter'])( 'does NOT export removed %s', (name) => { expect(adapterNames).not.toContain(name); diff --git a/packages/mcp-server/src/adapters/built-in/health-adapter.ts b/packages/mcp-server/src/adapters/built-in/health-adapter.ts deleted file mode 100644 index 6909857..0000000 --- a/packages/mcp-server/src/adapters/built-in/health-adapter.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Health Check Adapter - * - * Provides health and readiness checks for the MCP server and its dependencies. - */ - -import * as fs from 'node:fs/promises'; -import { HealthArgsSchema } from '../../schemas/index.js'; -import { ToolAdapter } from '../tool-adapter'; -import type { AdapterContext, ToolDefinition, ToolExecutionContext, ToolResult } from '../types'; -import { validateArgs } from '../validation.js'; - -export interface HealthCheckConfig { - repositoryPath: string; - vectorStorePath: string; -} - -export interface HealthStatus { - status: 'healthy' | 'degraded' | 'unhealthy'; - checks: { - vectorStorage: CheckResult; - repository: CheckResult; - }; - timestamp: string; - uptime: number; // milliseconds -} - -interface CheckResult { - status: 'pass' | 'warn' | 'fail'; - message: string; - details?: Record; -} - -export class HealthAdapter extends ToolAdapter { - readonly metadata = { - name: 'health-adapter', - version: '1.0.0', - description: 'Provides health and readiness checks for the MCP server', - }; - - private config: HealthCheckConfig; - private startTime: number; - - constructor(config: HealthCheckConfig) { - super(); - this.config = config; - this.startTime = Date.now(); - } - - async initialize(context: AdapterContext): Promise { - this.initializeBase(context); - } - - async shutdown(): Promise { - // No cleanup needed - } - - getToolDefinition(): ToolDefinition { - return { - name: 'dev_health', - description: - 'Check the health status of the dev-agent MCP server and its dependencies (Antfly vector storage, repository access)', - inputSchema: { - type: 'object', - properties: { - verbose: { - type: 'boolean', - description: 'Include detailed diagnostic information', - default: false, - }, - }, - }, - }; - } - - async execute(args: Record, context: ToolExecutionContext): Promise { - // Validate args with Zod (no type assertions!) - const validation = validateArgs(HealthArgsSchema, args); - if (!validation.success) { - return validation.error; - } - - const { verbose } = validation.data; - - try { - const health = await this.performHealthChecks(verbose); - - const status = this.getOverallStatus(health); - const emoji = status === 'healthy' ? '✅' : status === 'degraded' ? '⚠️' : '❌'; - - const content = this.formatHealthReport(health, verbose); - - // Return formatted health report (MCP will wrap in content blocks) - return { - success: true, - data: `${emoji} **MCP Server Health: ${status.toUpperCase()}**\n\n${content}`, - }; - } catch (error) { - context.logger.error('Health check failed', { - error: error instanceof Error ? error.message : String(error), - }); - - return { - success: false, - error: { - code: 'HEALTH_CHECK_ERROR', - message: error instanceof Error ? error.message : 'Health check failed', - recoverable: true, - }, - }; - } - } - - private async performHealthChecks(verbose: boolean): Promise { - const [vectorStorage, repository] = await Promise.all([ - this.checkVectorStorage(verbose), - this.checkRepository(verbose), - ]); - - const checks: HealthStatus['checks'] = { vectorStorage, repository }; - - return { - status: this.getOverallStatus({ checks } as HealthStatus), - checks, - timestamp: new Date().toISOString(), - uptime: Date.now() - this.startTime, - }; - } - - private async checkVectorStorage(verbose: boolean): Promise { - try { - const stats = await fs.stat(this.config.vectorStorePath); - - if (!stats.isDirectory()) { - return { - status: 'fail', - message: 'Vector storage path is not a directory', - }; - } - - // Check if vector storage has data - const files = await fs.readdir(this.config.vectorStorePath); - const hasData = files.length > 0; - - if (!hasData) { - return { - status: 'warn', - message: 'Vector storage is empty (repository may not be indexed)', - details: verbose ? { path: this.config.vectorStorePath } : undefined, - }; - } - - return { - status: 'pass', - message: `Vector storage operational (${files.length} files)`, - details: verbose - ? { path: this.config.vectorStorePath, fileCount: files.length } - : undefined, - }; - } catch (error) { - return { - status: 'fail', - message: `Vector storage not accessible: ${error instanceof Error ? error.message : String(error)}`, - details: verbose ? { path: this.config.vectorStorePath } : undefined, - }; - } - } - - private async checkRepository(verbose: boolean): Promise { - try { - const stats = await fs.stat(this.config.repositoryPath); - - if (!stats.isDirectory()) { - return { - status: 'fail', - message: 'Repository path is not a directory', - }; - } - - // Check if it's a git repository - try { - await fs.stat(`${this.config.repositoryPath}/.git`); - return { - status: 'pass', - message: 'Repository accessible and is a Git repository', - details: verbose ? { path: this.config.repositoryPath } : undefined, - }; - } catch { - return { - status: 'warn', - message: 'Repository accessible but not a Git repository', - details: verbose ? { path: this.config.repositoryPath } : undefined, - }; - } - } catch (error) { - return { - status: 'fail', - message: `Repository not accessible: ${error instanceof Error ? error.message : String(error)}`, - details: verbose ? { path: this.config.repositoryPath } : undefined, - }; - } - } - - private getOverallStatus(health: HealthStatus): 'healthy' | 'degraded' | 'unhealthy' { - const checks = Object.values(health.checks).filter( - (check): check is CheckResult => check !== undefined - ); - - const hasFail = checks.some((check) => check.status === 'fail'); - const hasWarn = checks.some((check) => check.status === 'warn'); - - if (hasFail) { - return 'unhealthy'; - } - if (hasWarn) { - return 'degraded'; - } - return 'healthy'; - } - - private formatHealthReport(health: HealthStatus, verbose: boolean): string { - const lines: string[] = []; - - // Uptime - const uptimeMs = health.uptime; - const uptimeStr = this.formatUptime(uptimeMs); - lines.push(`**Uptime:** ${uptimeStr}`); - lines.push(`**Timestamp:** ${new Date(health.timestamp).toLocaleString()}`); - lines.push(''); - - // Checks - lines.push('**Component Status:**'); - lines.push(''); - - for (const [name, check] of Object.entries(health.checks)) { - if (!check) continue; - - const statusEmoji = check.status === 'pass' ? '✅' : check.status === 'warn' ? '⚠️' : '❌'; - const componentName = this.formatComponentName(name); - - lines.push(`${statusEmoji} **${componentName}:** ${check.message}`); - - if (verbose && check.details) { - lines.push(` *Details:* ${JSON.stringify(check.details)}`); - } - } - - return lines.join('\n'); - } - - private formatComponentName(name: string): string { - return name - .replace(/([A-Z])/g, ' $1') - .replace(/^./, (str) => str.toUpperCase()) - .trim(); - } - - private formatUptime(ms: number): string { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) { - return `${days}d ${hours % 24}h ${minutes % 60}m`; - } - if (hours > 0) { - return `${hours}h ${minutes % 60}m`; - } - if (minutes > 0) { - return `${minutes}m ${seconds % 60}s`; - } - return `${seconds}s`; - } - - async healthCheck(): Promise { - const health = await this.performHealthChecks(false); - return health.status === 'healthy'; - } -} diff --git a/packages/mcp-server/src/adapters/built-in/index.ts b/packages/mcp-server/src/adapters/built-in/index.ts index 2f8f7a1..d9378e3 100644 --- a/packages/mcp-server/src/adapters/built-in/index.ts +++ b/packages/mcp-server/src/adapters/built-in/index.ts @@ -3,7 +3,6 @@ * Production-ready adapters included with the MCP server */ -export { HealthAdapter, type HealthCheckConfig } from './health-adapter.js'; export { InspectAdapter, type InspectAdapterConfig } from './inspect-adapter.js'; export { MapAdapter, type MapAdapterConfig } from './map-adapter.js'; export { RefsAdapter, type RefsAdapterConfig } from './refs-adapter.js'; diff --git a/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts b/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts index 1ade9a1..f33553d 100644 --- a/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts @@ -75,13 +75,14 @@ export class InspectAdapter extends ToolAdapter { return { name: 'dev_patterns', description: - 'Analyze coding patterns in a file against the codebase. Compares import style, ' + - 'error handling, type coverage, test coverage, and file size against similar files. ' + - 'Use for code reviews and consistency checks.', + 'Analyze coding patterns in a file against similar code in the codebase. ' + + 'Compares import style, error handling, type coverage, test coverage, and file size. ' + + 'Use for code reviews and consistency checks. ' + + 'NOT for finding code (use dev_search) or tracing calls (use dev_refs).', inputSchema: { type: 'object', properties: { - query: { + filePath: { type: 'string', description: 'File path to analyze (e.g., "src/auth/middleware.ts")', }, @@ -94,13 +95,13 @@ export class InspectAdapter extends ToolAdapter { }, format: { type: 'string', - enum: ['compact', 'verbose'], + enum: ['compact', 'verbose', 'json'], description: - 'Output format: "compact" for summaries (default), "verbose" for full details', + 'Output format: "compact" (default), "verbose" for details, "json" for structured data', default: this.defaultFormat, }, }, - required: ['query'], + required: ['filePath'], }, }; } @@ -112,37 +113,51 @@ export class InspectAdapter extends ToolAdapter { return validation.error; } - const { query, limit, format } = validation.data; + const { filePath, limit, format } = validation.data; try { context.logger.debug('Executing pattern analysis', { - query, + filePath, limit, format, }); // Perform comprehensive file inspection - const { content, similarFilesCount, patternsAnalyzed } = await this.inspectFile( - query, - limit, - 0, // No threshold — let the pattern service decide relevance - format - ); + const { content, similarFilesCount, patternsAnalyzed, patternComparison } = + await this.inspectFile(filePath, limit, 0, format); context.logger.info('File inspection completed', { - query, + filePath, similarFilesCount, patternsAnalyzed, contentLength: content.length, }); + // JSON format: return structured data for token-efficient agent workflows + if (format === 'json' && patternComparison) { + const jsonData = JSON.stringify(patternComparison, null, 2); + return { + success: true, + data: jsonData, + metadata: { + tokens: jsonData.length / 4, + duration_ms: 0, + timestamp: new Date().toISOString(), + cached: false, + similar_files_count: similarFilesCount, + patterns_analyzed: patternsAnalyzed, + format: 'json', + }, + }; + } + // Return markdown content (MCP will wrap in content blocks) return { success: true, data: content, metadata: { - tokens: content.length / 4, // Rough estimate - duration_ms: 0, // Calculated by MCP server + tokens: content.length / 4, + duration_ms: 0, timestamp: new Date().toISOString(), cached: false, similar_files_count: similarFilesCount, @@ -159,7 +174,7 @@ export class InspectAdapter extends ToolAdapter { success: false, error: { code: 'FILE_NOT_FOUND', - message: `File not found: ${query}`, + message: `File not found: ${filePath}`, suggestion: 'Check the file path and ensure it exists in the repository.', }, }; @@ -182,6 +197,7 @@ export class InspectAdapter extends ToolAdapter { error: { code: 'INSPECTION_ERROR', message: error instanceof Error ? error.message : 'Unknown inspection error', + suggestion: 'Check the file path is valid and the repository is indexed.', }, }; } @@ -198,7 +214,12 @@ export class InspectAdapter extends ToolAdapter { limit: number, threshold: number, format: string - ): Promise<{ content: string; similarFilesCount: number; patternsAnalyzed: number }> { + ): Promise<{ + content: string; + similarFilesCount: number; + patternsAnalyzed: number; + patternComparison?: PatternComparison; + }> { // Step 1: Find similar files (request slightly more to account for extension filtering) const similarResults = await this.searchService.findSimilar(filePath, { limit: limit + 5, // Small buffer for extension filtering @@ -232,16 +253,19 @@ export class InspectAdapter extends ToolAdapter { const similarFilePaths = filteredResults.map((r) => r.metadata.path as string); const patternComparison = await this.patternService.comparePatterns(filePath, similarFilePaths); - // Step 3: Generate comprehensive inspection report + // Step 3: Generate inspection report (skip formatting for JSON) const content = - format === 'verbose' - ? await this.formatInspectionVerbose(filePath, filteredResults, patternComparison) - : await this.formatInspectionCompact(filePath, filteredResults, patternComparison); + format === 'json' + ? '' // JSON handled by execute() + : format === 'verbose' + ? await this.formatInspectionVerbose(filePath, filteredResults, patternComparison) + : await this.formatInspectionCompact(filePath, filteredResults, patternComparison); return { content, similarFilesCount: filteredResults.length, - patternsAnalyzed: 5, // Currently analyzing 5 pattern categories + patternsAnalyzed: 5, + patternComparison, }; } diff --git a/packages/mcp-server/src/adapters/built-in/map-adapter.ts b/packages/mcp-server/src/adapters/built-in/map-adapter.ts index ff2d035..3e6e4ae 100644 --- a/packages/mcp-server/src/adapters/built-in/map-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/map-adapter.ts @@ -226,7 +226,7 @@ export class MapAdapter extends ToolAdapter { error: { code: 'MAP_FAILED', message: error instanceof Error ? error.message : 'Unknown error', - details: error, + suggestion: 'Run "dev index" first. Try a lower depth if the map is too large.', }, }; } diff --git a/packages/mcp-server/src/adapters/built-in/refs-adapter.ts b/packages/mcp-server/src/adapters/built-in/refs-adapter.ts index 80f558a..aeb0cfe 100644 --- a/packages/mcp-server/src/adapters/built-in/refs-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/refs-adapter.ts @@ -130,6 +130,8 @@ export class RefsAdapter extends ToolAdapter { error: { code: 'NOT_FOUND', message: `Could not find function or method named "${name}"`, + suggestion: + 'Verify the function name exists with dev_search first. Names are case-sensitive.', }, }; } @@ -193,7 +195,7 @@ export class RefsAdapter extends ToolAdapter { error: { code: 'REFS_FAILED', message: error instanceof Error ? error.message : 'Unknown error', - details: error, + suggestion: 'Try dev_search to find the correct function name first.', }, }; } diff --git a/packages/mcp-server/src/adapters/built-in/search-adapter.ts b/packages/mcp-server/src/adapters/built-in/search-adapter.ts index 705236e..ae3e883 100644 --- a/packages/mcp-server/src/adapters/built-in/search-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/search-adapter.ts @@ -216,7 +216,8 @@ export class SearchAdapter extends ToolAdapter { error: { code: 'SEARCH_FAILED', message: error instanceof Error ? error.message : 'Unknown error', - details: error, + suggestion: + 'Run "dev index" to index the repository. Try a different query if no results.', }, }; } diff --git a/packages/mcp-server/src/adapters/built-in/status-adapter.ts b/packages/mcp-server/src/adapters/built-in/status-adapter.ts index 951630e..2095274 100644 --- a/packages/mcp-server/src/adapters/built-in/status-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/status-adapter.ts @@ -140,7 +140,7 @@ export class StatusAdapter extends ToolAdapter { error: { code: 'STATUS_FAILED', message: error instanceof Error ? error.message : 'Unknown error', - details: error, + suggestion: 'Run "dev setup" to ensure Antfly is running, then "dev index" to index.', }, }; } diff --git a/packages/mcp-server/src/schemas/__tests__/schemas.test.ts b/packages/mcp-server/src/schemas/__tests__/schemas.test.ts index 2b787e5..211564f 100644 --- a/packages/mcp-server/src/schemas/__tests__/schemas.test.ts +++ b/packages/mcp-server/src/schemas/__tests__/schemas.test.ts @@ -7,7 +7,6 @@ import { describe, expect, it } from 'vitest'; import { - HealthArgsSchema, InspectArgsSchema, MapArgsSchema, RefsArgsSchema, @@ -18,7 +17,7 @@ import { describe('InspectArgsSchema', () => { it('should validate valid input', () => { const result = InspectArgsSchema.safeParse({ - query: 'src/auth/token.ts', + filePath: 'src/auth/token.ts', }); expect(result.success).toBe(true); @@ -29,7 +28,7 @@ describe('InspectArgsSchema', () => { it('should apply defaults', () => { const result = InspectArgsSchema.safeParse({ - query: 'test', + filePath: 'test.ts', }); expect(result.success).toBe(true); @@ -41,9 +40,21 @@ describe('InspectArgsSchema', () => { } }); - it('should reject empty query', () => { + it('should accept json format', () => { + const result = InspectArgsSchema.safeParse({ + filePath: 'test.ts', + format: 'json', + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.format).toBe('json'); + } + }); + + it('should reject empty filePath', () => { const result = InspectArgsSchema.safeParse({ - query: '', + filePath: '', }); expect(result.success).toBe(false); @@ -51,7 +62,7 @@ describe('InspectArgsSchema', () => { it('should reject out-of-range limit', () => { const result = InspectArgsSchema.safeParse({ - query: 'test', + filePath: 'test.ts', limit: 200, }); @@ -60,7 +71,7 @@ describe('InspectArgsSchema', () => { it('should reject unknown properties', () => { const result = InspectArgsSchema.safeParse({ - query: 'test', + filePath: 'test.ts', unknownProp: 'value', }); @@ -193,32 +204,3 @@ describe('StatusArgsSchema', () => { } }); }); - -describe('HealthArgsSchema', () => { - it('should validate with default', () => { - const result = HealthArgsSchema.safeParse({}); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.verbose).toBe(false); - } - }); - - it('should validate verbose flag', () => { - const result = HealthArgsSchema.safeParse({ verbose: true }); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.verbose).toBe(true); - } - }); - - it('should reject unknown properties', () => { - const result = HealthArgsSchema.safeParse({ - verbose: true, - unknownProp: 'value', - }); - - expect(result.success).toBe(false); - }); -}); diff --git a/packages/mcp-server/src/schemas/index.ts b/packages/mcp-server/src/schemas/index.ts index 8b8ce24..936bf84 100644 --- a/packages/mcp-server/src/schemas/index.ts +++ b/packages/mcp-server/src/schemas/index.ts @@ -30,9 +30,9 @@ export const BaseQuerySchema = z.object({ export const InspectArgsSchema = z .object({ - query: z.string().min(1, 'Query must be a non-empty string (file path)'), + filePath: z.string().min(1, 'filePath must be a non-empty file path'), limit: z.number().int().min(1).max(50).default(10), - format: FormatSchema.default('compact'), + format: z.enum(['compact', 'verbose', 'json']).default('compact'), }) .strict(); // Reject unknown properties @@ -109,18 +109,6 @@ export const StatusOutputSchema = z.object({ export type StatusOutput = z.infer; -// ============================================================================ -// Health Adapter -// ============================================================================ - -export const HealthArgsSchema = z - .object({ - verbose: z.boolean().default(false), - }) - .strict(); - -export type HealthArgs = z.infer; - // ============================================================================ // Output Schemas (Runtime validation for adapter responses) // ============================================================================ @@ -136,29 +124,6 @@ export const SearchOutputSchema = z.object({ export type SearchOutput = z.infer; -/** - * Health check result schema - */ -export const HealthCheckResultSchema = z.object({ - status: z.enum(['pass', 'warn', 'fail']), - message: z.string(), - details: z.any().optional(), // Allow any type for details -}); - -export const HealthOutputSchema = z.object({ - status: z.enum(['healthy', 'degraded', 'unhealthy']), - uptime: z.number(), - timestamp: z.string(), - checks: z.object({ - vectorStorage: HealthCheckResultSchema, - repository: HealthCheckResultSchema, - githubIndex: HealthCheckResultSchema.optional(), - }), - formattedReport: z.string(), -}); - -export type HealthOutput = z.infer; - /** * Map output schema */ From d72a7e15e1756be7b7560769a5e3444dcb3e80a1 Mon Sep 17 00:00:00 2001 From: prosdev Date: Tue, 31 Mar 2026 01:02:01 -0700 Subject: [PATCH 5/5] =?UTF-8?q?docs:=20update=20all=20references=20for=205?= =?UTF-8?q?-tool=20MCP=20(dev=5Fhealth=20=E2=86=92=20dev=5Fstatus)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweep all docs for stale dev_health references and 6-tool counts: - CLAUDE.md, README.md, PLAN.md, TROUBLESHOOTING.md, examples/README.md - Website: homepage, tools index, dev-health (→ redirect), dev-patterns (query→filePath, add json format), troubleshooting - CLI help text and CURSOR_SETUP.md - packages/dev-agent/README.md - Add changeset for v0.10.4, release notes, update latest-version.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/mcp-phase1-tools-improvement.md | 13 +++ CLAUDE.md | 9 +- PLAN.md | 2 +- README.md | 15 +-- TROUBLESHOOTING.md | 26 ++--- examples/README.md | 14 +-- packages/cli/src/commands/mcp.ts | 8 +- packages/dev-agent/README.md | 9 +- packages/mcp-server/CURSOR_SETUP.md | 15 ++- website/content/docs/tools/dev-health.mdx | 95 +----------------- website/content/docs/tools/dev-patterns.mdx | 102 ++++++-------------- website/content/docs/tools/index.mdx | 10 +- website/content/docs/troubleshooting.mdx | 8 +- website/content/index.mdx | 7 +- website/content/latest-version.ts | 10 +- website/content/updates/index.mdx | 16 +++ 16 files changed, 123 insertions(+), 236 deletions(-) create mode 100644 .changeset/mcp-phase1-tools-improvement.md diff --git a/.changeset/mcp-phase1-tools-improvement.md b/.changeset/mcp-phase1-tools-improvement.md new file mode 100644 index 0000000..640fe5e --- /dev/null +++ b/.changeset/mcp-phase1-tools-improvement.md @@ -0,0 +1,13 @@ +--- +'@prosdevlab/dev-agent': patch +--- + +MCP tools improvement: faster pattern analysis, merged health into status, agent usability + +- `dev_patterns` is 10-30x faster — reads from Antfly index instead of re-scanning with ts-morph +- `dev_health` merged into `dev_status` (use `section="health"`) — 6 tools reduced to 5 +- `dev_patterns` parameter renamed from `query` to `filePath` to prevent LLM misuse +- New `format: "json"` option on `dev_patterns` for token-efficient agent workflows +- All tools now return `suggestion` field on errors for agent recovery guidance +- Removed stale GitHub code from health adapter +- Extracted pure pattern analyzers for testability diff --git a/CLAUDE.md b/CLAUDE.md index 5f95ba8..c97e886 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ Everything runs on your machine. No data leaves. packages/ core/ # Scanner (ts-morph, tree-sitter), vector storage (Antfly), services cli/ # Commander.js CLI — dev index, dev mcp install, etc. - mcp-server/ # MCP server with 6 built-in adapters + mcp-server/ # MCP server with 5 built-in adapters subagents/ # Coordinator, explorer, planner, PR agents integrations/ # Claude Code, VS Code, Cursor logger/ # @prosdevlab/kero — centralized logging @@ -139,16 +139,15 @@ See `.claude/da-plans/README.md` for status and format details. --- -## MCP tools (6 adapters) +## MCP tools (5 adapters) | Tool | Purpose | |------|---------| | `dev_search` | Hybrid code search — BM25 + vector + RRF (use FIRST for conceptual queries) | | `dev_refs` | Find callers/callees of functions | | `dev_map` | Codebase structure with change frequency | -| `dev_patterns` | File pattern analysis (similar code, error handling, types) | -| `dev_status` | Repository indexing status + Antfly stats + watcher status | -| `dev_health` | Server health checks (Antfly connectivity) | +| `dev_patterns` | File pattern analysis (similar code, error handling, types). Takes `filePath`, not `query`. | +| `dev_status` | Repository indexing status + Antfly stats + health checks (`section="health"`) | --- diff --git a/PLAN.md b/PLAN.md index 7620575..e1bb8b3 100644 --- a/PLAN.md +++ b/PLAN.md @@ -47,7 +47,7 @@ Dev-agent provides semantic code search, codebase intelligence, and GitHub integ | `dev_patterns` - File analysis | ✅ Done | MCP adapter | | `dev_plan` - Issue planning | ✅ Done | MCP adapter | | `dev_gh` - GitHub search | ✅ Done | MCP adapter | -| `dev_health` - Health checks | ✅ Done | MCP adapter | +| `dev_health` - Health checks | ✅ Merged into `dev_status` | MCP adapter | | Cursor integration | ✅ Done | CLI command | | Claude Code integration | ✅ Done | CLI command | | Rate limiting (token bucket) | ✅ Done | MCP server | diff --git a/README.md b/README.md index 833ef96..b701945 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,13 @@ ## What it does -dev-agent indexes your codebase and provides 6 MCP tools to AI assistants. Instead of grepping through files, they can ask conceptual questions like "where do we handle authentication?" +dev-agent indexes your codebase and provides 5 MCP tools to AI assistants. Instead of grepping through files, they can ask conceptual questions like "where do we handle authentication?" - `dev_search` — Hybrid search (BM25 + vector + RRF) — returns code snippets, not just paths - `dev_refs` — Find callers/callees of any function - `dev_map` — Codebase structure with hot paths (most referenced files) - `dev_patterns` — Compare coding patterns against similar files -- `dev_status` — Repository indexing status and health -- `dev_health` — Server health checks +- `dev_status` — Repository indexing status, health checks, and Antfly stats ## Quick Start @@ -110,13 +109,9 @@ Compare a file's coding patterns (imports, error handling, type coverage, testin Analyze patterns in src/auth/middleware.ts ``` -### `dev_status` — Repository Status +### `dev_status` — Repository Status & Health -Indexing status, document counts, Antfly stats, and file watcher state. - -### `dev_health` — Health Checks - -Server health, Antfly connectivity, and repository access. +Indexing status, document counts, Antfly stats, file watcher state, and health checks (`section="health"`). ## Supported Languages @@ -155,7 +150,7 @@ pnpm lint packages/ core/ # Scanner, vector storage, indexer, services cli/ # Commander.js CLI - mcp-server/ # MCP server with 6 tool adapters + mcp-server/ # MCP server with 5 tool adapters subagents/ # Explorer, planner, PR agents integrations/ # Claude Code, VS Code, Cursor logger/ # @prosdevlab/kero centralized logging diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 04a068d..d765dbf 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -252,7 +252,7 @@ killall dev 2. **Check health:** ``` - Use dev_health tool to see rate limit status + Use dev_status section="health" tool to see rate limit status ``` 3. **If persistent:** @@ -446,7 +446,7 @@ Failed to fetch issues: spawnSync /bin/sh ENOBUFS 3. **Monitor health:** ``` - dev_health --verbose + dev_status section="health" format="verbose" ``` --- @@ -466,7 +466,7 @@ Failed to fetch issues: spawnSync /bin/sh ENOBUFS **Diagnosis:** ``` -Use dev_health tool to check component status +Use dev_status section="health" tool to check component status ``` **Common causes:** @@ -505,7 +505,7 @@ Check the tool's input schema: 3. Check server health: ``` - dev_health + dev_status section="health" ``` --- @@ -573,9 +573,9 @@ claude mcp add --env LOG_LEVEL=debug dev-agent -- dev mcp start ### Check component health ``` -Use dev_health tool with verbose flag: +Use dev_status section="health" tool with verbose flag: -dev_health --verbose +dev_status section="health" format="verbose" Shows: - Vector storage status @@ -596,7 +596,7 @@ dev mcp start --verbose # In another terminal, send test message echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | dev mcp start -# Should list all 6 tools: dev_search, dev_refs, dev_map, dev_patterns, dev_status, dev_health +# Should list all 5 tools: dev_search, dev_refs, dev_map, dev_patterns, dev_status ``` ### Inspect storage @@ -666,7 +666,7 @@ If you encounter a bug: 2. **Get diagnostics:** ``` - Use dev_health --verbose + Use dev_status section="health" format="verbose" ``` 3. **Enable debug logs:** @@ -720,7 +720,7 @@ dev index dev_status # Verify health -dev_health +dev_status section="health" ``` ### Debugging search quality @@ -855,7 +855,7 @@ dev index ## Health Check Guide -The `dev_health` tool provides comprehensive diagnostics: +The `dev_status section="health"` tool provides comprehensive diagnostics: ### Interpreting Health Status @@ -919,7 +919,7 @@ du -sh ~/.dev-agent/indexes/ - After major code changes (adding features, refactoring) - Weekly for active projects - Monthly for stable projects -- Use `dev_health` to check if index is stale +- Use `dev_status section="health"` to check if index is stale ### Q: Can multiple AI tools use dev-agent simultaneously? @@ -942,7 +942,7 @@ du -sh ~/.dev-agent/indexes/ 2. **Use health check:** ``` - dev_health --verbose + dev_status section="health" format="verbose" ``` 3. **Enable debug logging:** @@ -953,7 +953,7 @@ du -sh ~/.dev-agent/indexes/ Include: - `dev --version` -- `dev_health --verbose` output +- `dev_status section="health" format="verbose"` output - Steps to reproduce - Expected vs actual behavior - Debug logs (if applicable) diff --git a/examples/README.md b/examples/README.md index 86bb7e0..106e5a3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -150,16 +150,18 @@ dev_status: --- -### `dev_health` - Health Check +### Health Check (via `dev_status`) Diagnose issues: ``` -dev_health +dev_status: + section: health # Verbose output -dev_health: - verbose: true +dev_status: + section: health + format: verbose ``` --- @@ -230,7 +232,7 @@ dev_health: dev index # Check health -dev_health +dev_status section="health" ``` --- @@ -292,7 +294,7 @@ dev mcp install --cursor - Reduce `limit` - Use `compact` format -- Check `dev_health` +- Check `dev_status section="health"` --- diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts index 3480c96..5a5ae4c 100644 --- a/packages/cli/src/commands/mcp.ts +++ b/packages/cli/src/commands/mcp.ts @@ -48,9 +48,9 @@ Setup: 2. Install MCP integration: dev mcp install --cursor 3. Restart Cursor to activate -Available Tools (6): +Available Tools (5): dev_search, dev_status, dev_patterns, - dev_health, dev_refs, dev_map + dev_refs, dev_map ` ) .addCommand( @@ -161,9 +161,7 @@ Available Tools (6): await server.start(); logger.info(chalk.green('MCP server started successfully!')); - logger.info( - 'Available tools: dev_search, dev_status, dev_patterns, dev_health, dev_refs, dev_map' - ); + logger.info('Available tools: dev_search, dev_status, dev_patterns, dev_refs, dev_map'); if (options.transport === 'stdio') { logger.info('Server running on stdio transport (for AI tools)'); diff --git a/packages/dev-agent/README.md b/packages/dev-agent/README.md index 747bec3..0f13a77 100644 --- a/packages/dev-agent/README.md +++ b/packages/dev-agent/README.md @@ -39,8 +39,7 @@ When integrated with Cursor or Claude Code, you get 6 powerful tools: - `dev_refs` - Find callers/callees of functions - `dev_map` - Codebase structure with change frequency - `dev_patterns` - File analysis and pattern checking -- `dev_status` - Repository status and health -- `dev_health` - Component health checks +- `dev_status` - Repository status, health checks, and Antfly stats ## Requirements @@ -110,11 +109,11 @@ All processing happens on your machine: # Find authentication-related code dev_search: "JWT token validation middleware" -# Find similar code patterns -dev_patterns: { action: "compare", query: "src/auth/middleware.ts" } +# Analyze coding patterns +dev_patterns: { filePath: "src/auth/middleware.ts" } # Check system health -dev_health: verbose +dev_status: { section: "health" } ``` ## Support diff --git a/packages/mcp-server/CURSOR_SETUP.md b/packages/mcp-server/CURSOR_SETUP.md index 8efd776..048bf28 100644 --- a/packages/mcp-server/CURSOR_SETUP.md +++ b/packages/mcp-server/CURSOR_SETUP.md @@ -66,20 +66,17 @@ Compare src/auth/middleware.ts with similar implementations - `limit`: Number of results (default: 10, for compare action) - `format`: Output format (`compact` or `verbose`) -### `dev_health` - Server Health Check -Check the health of dev-agent MCP server and its components. +### Health Checks (via `dev_status`) + +Use `dev_status` with `section="health"` for server diagnostics: ``` -Check server health +Check server health status ``` -**Parameters:** -- `verbose`: Include detailed diagnostics (default: false) - **Checks:** -- Vector storage (indexed code) -- Repository accessibility -- GitHub index status and age +- Repository access +- Antfly connectivity ## Management Commands diff --git a/website/content/docs/tools/dev-health.mdx b/website/content/docs/tools/dev-health.mdx index 0910b47..242e502 100644 --- a/website/content/docs/tools/dev-health.mdx +++ b/website/content/docs/tools/dev-health.mdx @@ -1,96 +1,11 @@ -# dev_health +# dev_health (removed) -Monitor MCP server health and component status. Useful for diagnostics and proactive monitoring. +`dev_health` has been merged into [`dev_status`](/docs/tools/dev-status). -## Usage +Use `dev_status` with `section="health"` for health checks: ``` -dev_health(format?) -``` - -## Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `format` | `"compact"` \| `"verbose"` | `"compact"` | Output format | - -## Example - -> "Use dev_health" - -``` -## MCP Server Health - -Status: ✅ Healthy -Uptime: 2h 34m 12s - -Components: -- Vector Storage: ✅ Pass (24ms) -- Repository Index: ✅ Pass (12ms) -- GitHub Index: ✅ Pass (8ms) -- Rate Limiter: ✅ Pass (89/100 tokens available) -``` - -## Verbose Output - -> "Use dev_health format verbose" - -``` -## MCP Server Health (Verbose) - -Status: ✅ Healthy -Version: 0.1.0 -Started: 2024-01-15T08:00:00Z -Uptime: 2h 34m 12s -Process ID: 12345 - -Components: -┌─────────────────┬────────┬──────────┬─────────────────────┐ -│ Component │ Status │ Latency │ Details │ -├─────────────────┼────────┼──────────┼─────────────────────┤ -│ Vector Storage │ ✅ Pass │ 24ms │ Antfly connected │ -│ Repository Index│ ✅ Pass │ 12ms │ 1,832 components │ -│ GitHub Index │ ✅ Pass │ 8ms │ 172 documents │ -│ Rate Limiter │ ✅ Pass │ <1ms │ 89/100 tokens │ -└─────────────────┴────────┴──────────┴─────────────────────┘ - -Memory Usage: 145 MB -CPU: 2.3% -``` - -## Health States - -| State | Meaning | -|-------|---------| -| ✅ **Healthy** | All components operational | -| ⚠️ **Degraded** | Some components have warnings | -| ❌ **Unhealthy** | Critical components failing | - -## Component Checks - -| Component | What It Checks | -|-----------|----------------| -| **Vector Storage** | Antfly connection and query capability | -| **Repository Index** | Index exists and is readable | -| **GitHub Index** | GitHub metadata is accessible | -| **Rate Limiter** | Token bucket has capacity | - -## Use Cases - -> **Proactive monitoring.** Check health before important operations to catch issues early. - -> **Debugging tool failures.** If a tool returns errors, health check can identify the root cause. - -## Response Metadata - -```json -{ - "metadata": { - "tokens": 156, - "duration_ms": 45, - "status": "healthy", - "components_checked": 4 - } -} +dev_status(section: "health") ``` +This returns the same Antfly connectivity and repository access checks that `dev_health` provided. diff --git a/website/content/docs/tools/dev-patterns.mdx b/website/content/docs/tools/dev-patterns.mdx index b400e1c..efdae70 100644 --- a/website/content/docs/tools/dev-patterns.mdx +++ b/website/content/docs/tools/dev-patterns.mdx @@ -1,36 +1,35 @@ # dev_patterns -Inspect a file for pattern analysis. Finds similar code and compares patterns like error handling, type coverage, imports, and testing. +Analyze coding patterns in a file against similar code in the codebase. Compares import style, error handling, type coverage, test coverage, and file size. + +**Not for finding code** (use `dev_search`) **or tracing calls** (use `dev_refs`). ## Usage ``` -dev_patterns(query, format?, limit?, threshold?) +dev_patterns(filePath, format?, limit?) ``` ## Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `query` | string | required | File path to inspect | -| `format` | `"compact"` \| `"verbose"` | `"compact"` | Output format | +| `filePath` | string | required | File path to analyze (e.g., `src/auth/middleware.ts`) | +| `format` | `"compact"` \| `"verbose"` \| `"json"` | `"compact"` | Output format | | `limit` | number | 10 | Maximum similar files to analyze | -| `threshold` | number | 0.7 | Similarity threshold (0-1) | ## What It Does `dev_patterns` performs comprehensive file analysis in two steps: -1. **Finds similar files** - Uses semantic search to find code with similar structure/purpose -2. **Analyzes patterns** - Compares error handling, type coverage, imports, testing, and file size - -This helps you understand how your file compares to similar code in the repository. +1. **Finds similar files** — Uses semantic search to find code with similar structure/purpose +2. **Analyzes patterns** — Compares error handling, type coverage, imports, testing, and file size ## Examples ### Basic Inspection -> "Use dev_patterns with query 'src/auth/middleware.ts'" +> "Use dev_patterns with filePath 'src/auth/middleware.ts'" ``` ## File Inspection: src/auth/middleware.ts @@ -51,96 +50,53 @@ This helps you understand how your file compares to similar code in the reposito ### Verbose Output -> "Use dev_patterns with query 'packages/core/src/indexer/index.ts', format 'verbose'" +> "Use dev_patterns with filePath 'packages/core/src/indexer/index.ts', format 'verbose'" Returns detailed pattern distribution and statistics for each pattern category. -### High Similarity Only +### JSON Output (Token-Efficient) -> "Use dev_patterns with query 'src/utils/date.ts', threshold 0.9" +> "Use dev_patterns with filePath 'src/auth/middleware.ts', format 'json'" -Only analyzes very similar files (90%+ match). +Returns structured JSON — compact and machine-parseable for agent workflows: -## Pattern Categories +```json +{ + "fileSize": { "yourFile": 234, "average": 312, "deviation": "smaller" }, + "importStyle": { "yourFile": "esm", "common": "esm", "percentage": 100 }, + "errorHandling": { "yourFile": "throw", "common": "result", "percentage": 60 }, + "typeAnnotations": { "yourFile": "full", "common": "full", "percentage": 80 }, + "testing": { "yourFile": false, "percentage": 40 } +} +``` -`dev_patterns` analyzes 5 key patterns: +## Pattern Categories | Pattern | What It Checks | |---------|----------------| | **Import Style** | `esm`, `cjs`, `mixed`, or `unknown` | -| **Error Handling** | `throw`, `result`, `callback`, or `unknown` | -| **Type Coverage** | `full`, `partial`, or `none` (TypeScript only) | +| **Error Handling** | `throw`, `result`, `error-return`, or `unknown` | +| **Type Coverage** | `full`, `partial`, `minimal`, or `none` (TypeScript only) | | **Testing** | Whether a co-located test file exists | | **File Size** | Line count vs. similar files | -## Output Formats - -### Compact (Default) - -Shows pattern summary with actionable insights: - -``` -**Error Handling:** Your file uses `throw`, but 60% of similar files use `result`. -``` - -Highlights differences from similar files to guide improvements. - -### Verbose - -Shows full pattern distribution: - -``` -#### Error Handling -- **Your File:** `throw` -- **Common Style:** `result` (60% of similar files) -- **Distribution:** - - result: 3 files (60%) - - throw: 2 files (40%) -``` - -## Use Cases - -| Use Case | Description | -|----------|-------------| -| **Code Review** | Check if your code follows project patterns | -| **Refactoring** | Understand pattern usage before making changes | -| **Consistency** | Ensure new code matches similar implementations | -| **Learning** | See how patterns are used across the codebase | - ## Workflow **Typical usage pattern:** 1. **Search** for concepts: `dev_search { query: "authentication middleware" }` -2. **Inspect** what you found: `dev_patterns { query: "src/auth/middleware.ts" }` +2. **Inspect** what you found: `dev_patterns { filePath: "src/auth/middleware.ts" }` 3. **Analyze** dependencies: `dev_refs { name: "authMiddleware" }` -## Tips - -> **Always provide a file path.** Unlike `dev_search`, `dev_patterns` requires a specific file to analyze. - -> **Extension filtering.** Only compares files with the same extension (e.g., `.ts` with `.ts`). - -> **Use after search.** `dev_patterns` is most valuable after finding relevant files with `dev_search`. - -> **Pattern-driven insights.** Focus on patterns where your file differs from similar code. - ## Performance -`dev_patterns` is optimized for speed: -- Batch scanning (5-10x faster than individual scans) -- Semantic similarity matching (not just path-based) -- Extension-based filtering for relevant comparisons - -Typical analysis time: **500-1000ms** for 5 similar files. +Pattern analysis reads from the Antfly index when available (~100ms). Falls back to ts-morph scanning when not (~1-3s). ## Migration from dev_explore If you previously used `dev_explore`: -- `dev_explore { action: "similar", query: "file.ts" }` → `dev_patterns { query: "file.ts" }` -- `dev_explore { action: "validate", query: "file.ts" }` → `dev_patterns { query: "file.ts" }` (now does both!) +- `dev_explore { action: "similar", query: "file.ts" }` → `dev_patterns { filePath: "file.ts" }` +- `dev_explore { action: "validate", query: "file.ts" }` → `dev_patterns { filePath: "file.ts" }` (now does both!) - `dev_explore { action: "pattern" }` → Use `dev_search` instead - `dev_explore { action: "relationships" }` → Use `dev_refs` instead - -**Key change:** `dev_patterns` no longer requires an `action` parameter. It automatically finds similar files AND performs pattern analysis in one call. diff --git a/website/content/docs/tools/index.mdx b/website/content/docs/tools/index.mdx index 2078706..5c2448c 100644 --- a/website/content/docs/tools/index.mdx +++ b/website/content/docs/tools/index.mdx @@ -1,6 +1,6 @@ # MCP Tools Overview -dev-agent provides six tools through the Model Context Protocol (MCP). These tools give AI assistants deep understanding of your codebase. +dev-agent provides five tools through the Model Context Protocol (MCP). These tools give AI assistants deep understanding of your codebase. ## Available Tools @@ -9,9 +9,8 @@ dev-agent provides six tools through the Model Context Protocol (MCP). These too | [`dev_search`](/docs/tools/dev-search) | Semantic code search with snippets | | [`dev_refs`](/docs/tools/dev-refs) | Query code relationships (callers/callees) | | [`dev_map`](/docs/tools/dev-map) | Codebase structure overview with change frequency | -| [`dev_patterns`](/docs/tools/dev-inspect) | File pattern analysis (finds similar code, compares 5 pattern categories) ✨ v0.8.5 | -| [`dev_status`](/docs/tools/dev-status) | Check repository indexing status | -| [`dev_health`](/docs/tools/dev-health) | Monitor MCP server health | +| [`dev_patterns`](/docs/tools/dev-patterns) | File pattern analysis (finds similar code, compares 5 pattern categories) | +| [`dev_status`](/docs/tools/dev-status) | Repository indexing status and health checks | ## New in v0.8.5 @@ -49,8 +48,7 @@ AI Assistant (Cursor/Claude) ├── RefsAdapter → dev_refs ├── MapAdapter → dev_map ├── InspectAdapter → dev_patterns - ├── StatusAdapter → dev_status - └── HealthAdapter → dev_health + └── StatusAdapter → dev_status (includes health checks) ``` ## Tool Response Format diff --git a/website/content/docs/troubleshooting.mdx b/website/content/docs/troubleshooting.mdx index 089b512..f1ae6f6 100644 --- a/website/content/docs/troubleshooting.mdx +++ b/website/content/docs/troubleshooting.mdx @@ -92,7 +92,7 @@ dev index ### Rate limit errors (429) - Wait for `retryAfterMs` period -- Check health: `dev_health` +- Check health: `dev_status section="health"` - Restart AI tool if persistent ## Search Issues @@ -183,7 +183,7 @@ dev mcp install --cursor ### Check health ``` -Use dev_health tool in Cursor/Claude Code +Use dev_status with section="health" in Cursor/Claude Code ``` ### Enable debug logging @@ -195,10 +195,10 @@ Use dev_health tool in Cursor/Claude Code ## Getting Help -1. Run `dev_health` for diagnostics +1. Run `dev_status section="health"` for diagnostics 2. Check [full troubleshooting guide](https://github.com/prosdevlab/dev-agent/blob/main/TROUBLESHOOTING.md) 3. [File an issue](https://github.com/prosdevlab/dev-agent/issues) with: - `dev --version` - - `dev_health` output + - `dev_status section="health"` output - Steps to reproduce diff --git a/website/content/index.mdx b/website/content/index.mdx index a7e034a..ef7d9f9 100644 --- a/website/content/index.mdx +++ b/website/content/index.mdx @@ -21,7 +21,7 @@ Local semantic code search for Cursor and Claude Code via MCP. ## What it does -Your AI tool gets 6 MCP tools that understand your codebase: +Your AI tool gets 5 MCP tools that understand your codebase: | Tool | What it does | |------|--------------| @@ -29,8 +29,7 @@ Your AI tool gets 6 MCP tools that understand your codebase: | [`dev_refs`](/docs/tools/dev-refs) | Find callers/callees of any function | | [`dev_map`](/docs/tools/dev-map) | Codebase structure with hot paths (most referenced files) | | [`dev_patterns`](/docs/tools/dev-patterns) | Compare coding patterns against similar files | -| [`dev_status`](/docs/tools/dev-status) | Repository indexing status and health | -| [`dev_health`](/docs/tools/dev-health) | Server health checks | +| [`dev_status`](/docs/tools/dev-status) | Repository indexing status, health checks, and Antfly stats | ## How it works @@ -42,7 +41,7 @@ flowchart LR subgraph Agent["dev-agent"] B["MCP Server"] - C["6 Tools"] + C["5 Tools"] end subgraph Local["Antfly (local)"] diff --git a/website/content/latest-version.ts b/website/content/latest-version.ts index c6a9871..170ba67 100644 --- a/website/content/latest-version.ts +++ b/website/content/latest-version.ts @@ -4,10 +4,10 @@ */ export const latestVersion = { - version: '0.10.3', - title: 'Fix Setup/Index Model Directory Mismatch', - date: 'March 30, 2026', + version: '0.10.4', + title: 'MCP Tools Improvement (Phase 1)', + date: 'March 31, 2026', summary: - 'Fixed dev setup reporting model ready while dev index fails with "model not found" due to mismatched model directories.', - link: '/updates#v0103--fix-setupindex-model-directory-mismatch', + 'dev_patterns is 10-30x faster, dev_health merged into dev_status, all tools return error recovery suggestions.', + link: '/updates#v0104--mcp-tools-improvement-phase-1', } as const; diff --git a/website/content/updates/index.mdx b/website/content/updates/index.mdx index c8b5c11..a5048a4 100644 --- a/website/content/updates/index.mdx +++ b/website/content/updates/index.mdx @@ -9,6 +9,22 @@ What's new in dev-agent. We ship improvements regularly to help AI assistants un --- +## v0.10.4 — MCP Tools Improvement (Phase 1) + +*March 31, 2026* + +**Faster pattern analysis, merged health into status, better agent usability.** + +- `dev_patterns` is 10-30x faster — reads from Antfly index instead of re-scanning with ts-morph +- `dev_health` merged into `dev_status` (`section="health"`) — reduced from 6 tools to 5 +- `dev_patterns` parameter renamed from `query` to `filePath` to prevent LLM misuse +- New `format: "json"` option on `dev_patterns` for token-efficient agent workflows +- All tools now return a `suggestion` field on errors for agent recovery guidance +- Removed stale GitHub code from health adapter +- Extracted pure pattern analyzers for testability + +--- + ## v0.10.3 — Fix Setup/Index Model Directory Mismatch *March 30, 2026*