@codehabits/mcp
Advanced tools
| // src/server.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { z as z4 } from "zod"; | ||
| // src/intelligence/reader.ts | ||
| import { readFileSync, existsSync, watchFile, unwatchFile } from "fs"; | ||
| import { join } from "path"; | ||
| // ../shared/src/emit-targets.ts | ||
| import { z } from "zod"; | ||
| var emitTargetSchema = z.enum([ | ||
| "agents-md", | ||
| "claude-skills", | ||
| "agents-skills", | ||
| "cursor-skills" | ||
| ]); | ||
| // ../shared/src/schemas/intelligence.ts | ||
| import { z as z2 } from "zod"; | ||
| var intelligenceSourceSchema = z2.enum([ | ||
| "code_analysis", | ||
| "config_import", | ||
| "stack_baseline", | ||
| "git_history", | ||
| "pr_analysis", | ||
| "rejection_analysis", | ||
| "comment_analysis", | ||
| "agent_proposal" | ||
| ]); | ||
| var maturityLevelSchema = z2.union([ | ||
| z2.literal(1), | ||
| z2.literal(2), | ||
| z2.literal(3), | ||
| z2.literal(4) | ||
| ]); | ||
| var maturityLabelSchema = z2.enum([ | ||
| "baseline", | ||
| "growing", | ||
| "established", | ||
| "mature" | ||
| ]); | ||
| var conventionCategorySchema = z2.enum([ | ||
| "imports", | ||
| "testing", | ||
| "naming", | ||
| "async", | ||
| "structure", | ||
| "error-handling", | ||
| "api", | ||
| "documentation", | ||
| "security", | ||
| "performance", | ||
| "type-safety", | ||
| "other" | ||
| ]); | ||
| var antiPatternSeveritySchema = z2.enum([ | ||
| "critical", | ||
| "high", | ||
| "medium", | ||
| "low" | ||
| ]); | ||
| var techStackSchema = z2.object({ | ||
| language: z2.string(), | ||
| languages: z2.array(z2.string()).optional(), | ||
| framework: z2.string().optional(), | ||
| framework_version: z2.string().optional(), | ||
| test_framework: z2.string().optional(), | ||
| linter: z2.string().optional(), | ||
| formatter: z2.string().optional(), | ||
| build_tool: z2.string().optional(), | ||
| package_manager: z2.string().optional() | ||
| }); | ||
| var conventionExampleSchema = z2.object({ | ||
| pr_number: z2.number().optional(), | ||
| file_path: z2.string(), | ||
| snippet: z2.string().optional() | ||
| }); | ||
| var conventionSchema = z2.object({ | ||
| id: z2.string(), | ||
| category: conventionCategorySchema, | ||
| rule: z2.string(), | ||
| confidence: z2.number().min(0).max(1), | ||
| source: intelligenceSourceSchema, | ||
| evidence_count: z2.number(), | ||
| first_seen: z2.string(), | ||
| examples: z2.array(conventionExampleSchema), | ||
| metadata: z2.record(z2.string(), z2.unknown()).optional() | ||
| }); | ||
| var antiPatternEvidenceTypeSchema = z2.enum([ | ||
| "rejected_pr", | ||
| "review_comment", | ||
| "bug_fix" | ||
| ]); | ||
| function normalizeAntiPatternEvidenceType(input) { | ||
| if (input === null || input === void 0 || input === "") { | ||
| return "review_comment"; | ||
| } | ||
| const s = String(input).trim().toLowerCase().replace(/-/g, "_").replace(/\s+/g, "_"); | ||
| if (s === "rejected_pr" || s === "rejection" || s === "pr_rejection" || s === "rejected" || s.includes("reject")) { | ||
| return "rejected_pr"; | ||
| } | ||
| if (s === "bug_fix" || s === "bugfix" || s === "fix" || s.includes("bug") && s.includes("fix")) { | ||
| return "bug_fix"; | ||
| } | ||
| if (s === "review_comment" || s === "comment" || s === "pr_comment" || s === "inline_comment" || s === "discussion" || s === "review" || s.includes("comment")) { | ||
| return "review_comment"; | ||
| } | ||
| if (s === "merged_pr" || s === "merged" || s === "pr") { | ||
| return "review_comment"; | ||
| } | ||
| return "review_comment"; | ||
| } | ||
| var antiPatternEvidenceSchema = z2.object({ | ||
| pr_number: z2.coerce.number(), | ||
| type: z2.preprocess( | ||
| (raw) => normalizeAntiPatternEvidenceType(raw), | ||
| antiPatternEvidenceTypeSchema | ||
| ), | ||
| excerpt: z2.string().optional() | ||
| }); | ||
| var antiPatternSchema = z2.object({ | ||
| id: z2.string(), | ||
| pattern: z2.string(), | ||
| severity: antiPatternSeveritySchema, | ||
| reason: z2.string(), | ||
| correct_approach: z2.string(), | ||
| evidence: z2.array(antiPatternEvidenceSchema) | ||
| }); | ||
| var knowledgeEntrySchema = z2.object({ | ||
| value: z2.string(), | ||
| confidence: z2.number().min(0).max(1), | ||
| source_prs: z2.array(z2.number()) | ||
| }); | ||
| var knowledgeGraphSchema = z2.record( | ||
| z2.string(), | ||
| z2.record(z2.string(), knowledgeEntrySchema) | ||
| ); | ||
| var reviewerExpertiseSchema = z2.object({ | ||
| area: z2.string(), | ||
| confidence: z2.number().min(0).max(1), | ||
| review_count: z2.number() | ||
| }); | ||
| var reviewerStatsSchema = z2.object({ | ||
| total_reviews: z2.number(), | ||
| avg_review_time_hours: z2.number(), | ||
| approval_rate: z2.number().min(0).max(1) | ||
| }); | ||
| var reviewerProfileSchema = z2.object({ | ||
| github_login: z2.string(), | ||
| expertise: z2.array(reviewerExpertiseSchema), | ||
| stats: reviewerStatsSchema | ||
| }); | ||
| var riskFactorSchema = z2.object({ | ||
| type: z2.enum(["file", "size", "timing", "complexity"]), | ||
| description: z2.string(), | ||
| value: z2.string(), | ||
| risk_multiplier: z2.number(), | ||
| evidence: z2.object({ | ||
| total_prs: z2.number(), | ||
| incidents: z2.number() | ||
| }) | ||
| }); | ||
| var maturitySchema = z2.object({ | ||
| level: maturityLevelSchema, | ||
| label: maturityLabelSchema, | ||
| prs_analyzed: z2.number(), | ||
| next_level_at: z2.number(), | ||
| sources: z2.array(intelligenceSourceSchema) | ||
| }); | ||
| var repositoryInfoSchema = z2.object({ | ||
| owner: z2.string(), | ||
| name: z2.string(), | ||
| full_name: z2.string(), | ||
| analyzed_prs: z2.number(), | ||
| analyzed_period: z2.object({ | ||
| from: z2.string(), | ||
| to: z2.string() | ||
| }), | ||
| tech_stack: techStackSchema, | ||
| codehabits_version: z2.string() | ||
| }); | ||
| var intelligenceFileSchema = z2.object({ | ||
| version: z2.string(), | ||
| generated_at: z2.string(), | ||
| last_synced_at: z2.string(), | ||
| maturity: maturitySchema, | ||
| repository: repositoryInfoSchema, | ||
| conventions: z2.array(conventionSchema), | ||
| anti_patterns: z2.array(antiPatternSchema), | ||
| knowledge_graph: knowledgeGraphSchema, | ||
| reviewers: z2.array(reviewerProfileSchema), | ||
| risk_factors: z2.array(riskFactorSchema) | ||
| }); | ||
| var conventionsFileSchema = z2.array(conventionSchema); | ||
| var antiPatternsFileSchema = z2.array(antiPatternSchema); | ||
| var knowledgeFileSchema = knowledgeGraphSchema; | ||
| var reviewersFileSchema = z2.array(reviewerProfileSchema); | ||
| var intelligenceMetaSchema = z2.object({ | ||
| version: z2.string(), | ||
| generated_at: z2.string(), | ||
| last_synced_at: z2.string(), | ||
| maturity: maturitySchema, | ||
| repository: repositoryInfoSchema, | ||
| risk_factors: z2.array(riskFactorSchema), | ||
| counts: z2.object({ | ||
| conventions: z2.number(), | ||
| anti_patterns: z2.number(), | ||
| reviewers: z2.number(), | ||
| knowledge_topics: z2.number() | ||
| }) | ||
| }); | ||
| var proposalSchema = z2.object({ | ||
| id: z2.string(), | ||
| type: z2.enum(["new_convention", "update_convention", "new_exception", "deprecate"]), | ||
| targetId: z2.string().optional(), | ||
| data: conventionSchema.partial(), | ||
| reason: z2.string(), | ||
| filePath: z2.string().optional(), | ||
| createdAt: z2.string(), | ||
| status: z2.enum(["pending", "approved", "rejected"]), | ||
| approvedBy: z2.string().optional() | ||
| }); | ||
| var proposalsFileSchema = z2.array(proposalSchema); | ||
| // ../shared/src/schemas/config.ts | ||
| import { z as z3 } from "zod"; | ||
| var customRuleSchema = z3.object({ | ||
| name: z3.string(), | ||
| severity: z3.enum(["error", "warning", "info"]), | ||
| description: z3.string() | ||
| }); | ||
| var CodehabitsConfigSchema = z3.object({ | ||
| version: z3.string(), | ||
| analysis: z3.object({ | ||
| lookback_months: z3.number().default(12), | ||
| min_prs_for_pattern: z3.number().default(10), | ||
| min_confidence: z3.number().min(0).max(1).default(0.7), | ||
| include_closed_prs: z3.boolean().default(true) | ||
| }), | ||
| ignore: z3.object({ | ||
| files: z3.array(z3.string()).default([]), | ||
| authors: z3.array(z3.string()).default(["dependabot", "renovate"]), | ||
| labels: z3.array(z3.string()).default(["wip", "draft"]) | ||
| }), | ||
| sync: z3.object({ | ||
| auto_commit: z3.boolean().default(false) | ||
| }), | ||
| /** Where to write derived views (AGENTS.md, per-agent skill dirs). Omitted = use CLI defaults. */ | ||
| emit_targets: z3.array(emitTargetSchema).optional(), | ||
| custom_rules: z3.array(customRuleSchema).default([]) | ||
| }); | ||
| var userConfigSchema = z3.object({ | ||
| auth: z3.object({ | ||
| token: z3.string(), | ||
| github_token: z3.string(), | ||
| user: z3.object({ | ||
| id: z3.string(), | ||
| login: z3.string(), | ||
| email: z3.string() | ||
| }) | ||
| }).optional(), | ||
| defaults: z3.object({ | ||
| ai_model: z3.string().default("deepseek") | ||
| }).optional() | ||
| }); | ||
| // ../shared/src/constants.ts | ||
| var SKILL_ROOT_NAME = "codehabits-team-intel"; | ||
| var PATHS = { | ||
| /** @deprecated Use individual file paths instead */ | ||
| INTELLIGENCE_FILE: ".codehabits/intelligence.json", | ||
| CONVENTIONS_FILE: ".codehabits/conventions.json", | ||
| ANTI_PATTERNS_FILE: ".codehabits/anti-patterns.json", | ||
| KNOWLEDGE_FILE: ".codehabits/knowledge.json", | ||
| REVIEWERS_FILE: ".codehabits/reviewers.json", | ||
| META_FILE: ".codehabits/meta.json", | ||
| PROPOSALS_FILE: ".codehabits/proposals.json", | ||
| README_FILE: ".codehabits/README.md", | ||
| CODEHABITS_GITIGNORE: ".codehabits/.gitignore", | ||
| CONFIG_FILE: ".codehabits/config.json", | ||
| /** Claude Code project skills (Cursor also discovers `.claude/skills/`). */ | ||
| CLAUDE_SKILL_DIR: `.claude/skills/${SKILL_ROOT_NAME}`, | ||
| CLAUDE_SKILL_FILE: `.claude/skills/${SKILL_ROOT_NAME}/SKILL.md`, | ||
| CLAUDE_SKILL_REFERENCES: `.claude/skills/${SKILL_ROOT_NAME}/references`, | ||
| CLAUDE_SKILL_SCRIPTS: `.claude/skills/${SKILL_ROOT_NAME}/scripts`, | ||
| /** Cross-client convention (agentskills.io / VS Code Copilot / many installers). */ | ||
| AGENTS_SKILL_DIR: `.agents/skills/${SKILL_ROOT_NAME}`, | ||
| AGENTS_SKILL_FILE: `.agents/skills/${SKILL_ROOT_NAME}/SKILL.md`, | ||
| AGENTS_SKILL_REFERENCES: `.agents/skills/${SKILL_ROOT_NAME}/references`, | ||
| AGENTS_SKILL_SCRIPTS: `.agents/skills/${SKILL_ROOT_NAME}/scripts`, | ||
| /** Legacy Cursor-only layout (optional emit / migration detection). */ | ||
| LEGACY_CURSOR_SKILL_DIR: `.cursor/skills/${SKILL_ROOT_NAME}`, | ||
| LEGACY_CURSOR_SKILL_FILE: `.cursor/skills/${SKILL_ROOT_NAME}/SKILL.md`, | ||
| /** | ||
| * Primary skill entry path (alias for Claude layout). | ||
| * @deprecated Prefer CLAUDE_SKILL_FILE or resolve paths from emit_targets. | ||
| */ | ||
| SKILL_FILE: `.claude/skills/${SKILL_ROOT_NAME}/SKILL.md`, | ||
| /** @deprecated Prefer CLAUDE_SKILL_DIR */ | ||
| SKILL_DIR: `.claude/skills/${SKILL_ROOT_NAME}`, | ||
| /** @deprecated Prefer CLAUDE_SKILL_REFERENCES */ | ||
| SKILL_REFERENCES: `.claude/skills/${SKILL_ROOT_NAME}/references`, | ||
| /** @deprecated Prefer CLAUDE_SKILL_SCRIPTS */ | ||
| SKILL_SCRIPTS: `.claude/skills/${SKILL_ROOT_NAME}/scripts`, | ||
| USER_CONFIG: ".codehabits/config.json" | ||
| }; | ||
| var AI_MODELS = { | ||
| FREE: "deepseek/deepseek-v3.2", | ||
| PAID: "anthropic/claude-sonnet-4-20250514" | ||
| }; | ||
| var PLAN_LIMITS = { | ||
| free: { | ||
| private_repos: 1, | ||
| pr_lookback: 100, | ||
| ai_model: AI_MODELS.FREE | ||
| }, | ||
| team: { | ||
| private_repos: Infinity, | ||
| pr_lookback: Infinity, | ||
| ai_model: AI_MODELS.PAID | ||
| }, | ||
| enterprise: { | ||
| private_repos: Infinity, | ||
| pr_lookback: Infinity, | ||
| ai_model: AI_MODELS.PAID | ||
| } | ||
| }; | ||
| // src/intelligence/reader.ts | ||
| var SPLIT_FILES = [ | ||
| PATHS.META_FILE, | ||
| PATHS.CONVENTIONS_FILE, | ||
| PATHS.ANTI_PATTERNS_FILE, | ||
| PATHS.KNOWLEDGE_FILE, | ||
| PATHS.REVIEWERS_FILE | ||
| ]; | ||
| function readIntelligenceFile(cwd) { | ||
| const metaPath = join(cwd, PATHS.META_FILE); | ||
| if (existsSync(metaPath)) { | ||
| return readSplitFiles(cwd); | ||
| } | ||
| return readLegacyFile(cwd); | ||
| } | ||
| function watchIntelligenceFile(cwd, onChange) { | ||
| const reload = () => { | ||
| onChange(readIntelligenceFile(cwd)); | ||
| }; | ||
| const allPaths = [ | ||
| join(cwd, PATHS.INTELLIGENCE_FILE), | ||
| ...SPLIT_FILES.map((rel) => join(cwd, rel)) | ||
| ]; | ||
| for (const abs of allPaths) { | ||
| watchFile(abs, { interval: 2e3 }, reload); | ||
| } | ||
| return () => { | ||
| for (const abs of allPaths) { | ||
| unwatchFile(abs, reload); | ||
| } | ||
| }; | ||
| } | ||
| function readSplitFiles(cwd) { | ||
| try { | ||
| const meta = readJson(join(cwd, PATHS.META_FILE), intelligenceMetaSchema); | ||
| if (!meta) return null; | ||
| const conventions = readJson(join(cwd, PATHS.CONVENTIONS_FILE), conventionsFileSchema) ?? []; | ||
| const antiPatterns = readJson(join(cwd, PATHS.ANTI_PATTERNS_FILE), antiPatternsFileSchema) ?? []; | ||
| const knowledgeGraph = readJson(join(cwd, PATHS.KNOWLEDGE_FILE), knowledgeFileSchema) ?? {}; | ||
| const reviewers = readJson(join(cwd, PATHS.REVIEWERS_FILE), reviewersFileSchema) ?? []; | ||
| return { | ||
| version: meta.version, | ||
| generated_at: meta.generated_at, | ||
| last_synced_at: meta.last_synced_at, | ||
| maturity: meta.maturity, | ||
| repository: meta.repository, | ||
| conventions, | ||
| anti_patterns: antiPatterns, | ||
| knowledge_graph: knowledgeGraph, | ||
| reviewers, | ||
| risk_factors: meta.risk_factors | ||
| }; | ||
| } catch (error) { | ||
| console.error("Failed to read split intelligence files:", error); | ||
| return null; | ||
| } | ||
| } | ||
| function readLegacyFile(cwd) { | ||
| return readJson( | ||
| join(cwd, PATHS.INTELLIGENCE_FILE), | ||
| intelligenceFileSchema | ||
| ); | ||
| } | ||
| function readJson(filePath, schema) { | ||
| if (!existsSync(filePath)) return null; | ||
| try { | ||
| const raw = readFileSync(filePath, "utf-8"); | ||
| const parsed = JSON.parse(raw); | ||
| return schema.parse(parsed); | ||
| } catch (error) { | ||
| console.error(`Failed to read ${filePath}:`, error); | ||
| return null; | ||
| } | ||
| } | ||
| // src/tools/get-team-context.ts | ||
| function handleGetTeamContext(intelligence2, input) { | ||
| const scope = input.scope || "all"; | ||
| const sections = []; | ||
| sections.push(`# Team Intelligence: ${intelligence2.repository.full_name}`); | ||
| sections.push( | ||
| `Maturity: ${intelligence2.maturity.label} | PRs analyzed: ${intelligence2.maturity.prs_analyzed}` | ||
| ); | ||
| sections.push( | ||
| `Tech stack: ${intelligence2.repository.tech_stack.language}${intelligence2.repository.tech_stack.framework ? ` / ${intelligence2.repository.tech_stack.framework}` : ""}` | ||
| ); | ||
| sections.push(""); | ||
| let conventions; | ||
| if (scope === "all") { | ||
| conventions = intelligence2.conventions.slice(0, 20); | ||
| } else { | ||
| conventions = intelligence2.conventions.filter((c) => { | ||
| if (c.category === scope) return true; | ||
| if (input.file_path) { | ||
| return isConventionRelevantToFile(c, input.file_path); | ||
| } | ||
| return false; | ||
| }); | ||
| if (conventions.length < 3) { | ||
| conventions = [ | ||
| ...conventions, | ||
| ...intelligence2.conventions.filter((c) => c.category === scope).slice(0, 5) | ||
| ]; | ||
| } | ||
| } | ||
| if (conventions.length > 0) { | ||
| sections.push("## Conventions"); | ||
| sections.push(""); | ||
| for (const conv of conventions) { | ||
| sections.push( | ||
| `- **${conv.rule}** (${conv.category}, ${Math.round(conv.confidence * 100)}% confidence)` | ||
| ); | ||
| } | ||
| sections.push(""); | ||
| } | ||
| let antiPatterns; | ||
| if (scope === "all") { | ||
| antiPatterns = intelligence2.anti_patterns.slice(0, 10); | ||
| } else { | ||
| antiPatterns = intelligence2.anti_patterns.filter((ap) => { | ||
| const patternLower = ap.pattern.toLowerCase(); | ||
| const scopeLower = scope.toLowerCase(); | ||
| return patternLower.includes(scopeLower); | ||
| }); | ||
| if (antiPatterns.length === 0) { | ||
| antiPatterns = intelligence2.anti_patterns.slice(0, 5); | ||
| } | ||
| } | ||
| if (antiPatterns.length > 0) { | ||
| sections.push("## Anti-Patterns (Avoid These)"); | ||
| sections.push(""); | ||
| for (const ap of antiPatterns) { | ||
| sections.push( | ||
| `- **${ap.pattern}** [${ap.severity}] - ${ap.correct_approach}` | ||
| ); | ||
| } | ||
| sections.push(""); | ||
| } | ||
| if (scope !== "all") { | ||
| const relevantTopics = Object.entries(intelligence2.knowledge_graph).filter( | ||
| ([topic]) => topic.toLowerCase().includes(scope.toLowerCase()) | ||
| ); | ||
| if (relevantTopics.length > 0) { | ||
| sections.push("## Domain Knowledge"); | ||
| sections.push(""); | ||
| for (const [topic, entries] of relevantTopics) { | ||
| sections.push(`### ${topic}`); | ||
| for (const [key, entry] of Object.entries(entries)) { | ||
| sections.push(`- ${key}: ${entry.value}`); | ||
| } | ||
| } | ||
| sections.push(""); | ||
| } | ||
| } | ||
| if (input.file_path) { | ||
| const fileReviewers = intelligence2.reviewers.filter( | ||
| (r) => r.expertise.some( | ||
| (e) => e.area.split("/").some((part) => input.file_path.includes(part)) | ||
| ) | ||
| ).slice(0, 3); | ||
| if (fileReviewers.length > 0) { | ||
| sections.push("## Suggested Reviewers for This File"); | ||
| sections.push(""); | ||
| for (const r of fileReviewers) { | ||
| sections.push( | ||
| `- @${r.github_login} (${r.stats.total_reviews} reviews)` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| return sections.join("\n"); | ||
| } | ||
| function isConventionRelevantToFile(conv, filePath) { | ||
| const fp = filePath.toLowerCase(); | ||
| if (fp.includes(".test.") || fp.includes(".spec.") || fp.includes("__tests__")) { | ||
| return conv.category === "testing"; | ||
| } | ||
| if (fp.includes("/api/") || fp.includes("route.ts") || fp.includes("handler")) { | ||
| return conv.category === "api" || conv.category === "error-handling"; | ||
| } | ||
| if (fp.includes("component") || fp.endsWith(".tsx")) { | ||
| return conv.category === "naming" || conv.category === "structure"; | ||
| } | ||
| return false; | ||
| } | ||
| // src/tools/check-code.ts | ||
| function handleCheckCode(intelligence2, input) { | ||
| const violations = []; | ||
| const code = input.code; | ||
| const filePath = input.file_path || ""; | ||
| for (const conv of intelligence2.conventions) { | ||
| if (conv.confidence < 0.7) continue; | ||
| const violation = checkConventionViolation(code, filePath, conv); | ||
| if (violation) { | ||
| violations.push({ | ||
| convention_id: conv.id, | ||
| rule: conv.rule, | ||
| severity: violation.severity, | ||
| suggestion: violation.suggestion | ||
| }); | ||
| } | ||
| } | ||
| for (const ap of intelligence2.anti_patterns) { | ||
| const violation = checkAntiPatternViolation(code, filePath, ap); | ||
| if (violation) { | ||
| violations.push({ | ||
| convention_id: ap.id, | ||
| rule: ap.pattern, | ||
| severity: violation.severity, | ||
| suggestion: violation.suggestion | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| violations, | ||
| passed: violations.filter((v) => v.severity === "error").length === 0 | ||
| }; | ||
| } | ||
| function checkConventionViolation(code, filePath, conv) { | ||
| const rule = conv.rule.toLowerCase(); | ||
| if (rule.includes("named export") && conv.category === "imports") { | ||
| if (code.includes("export default ") && !filePath.includes("page.") && !filePath.includes("layout.")) { | ||
| return { | ||
| severity: "warning", | ||
| suggestion: "Use named exports. Replace `export default X` with `export { X }` or `export const/function X`." | ||
| }; | ||
| } | ||
| } | ||
| if (rule.includes("path alias") && conv.category === "imports") { | ||
| if (code.match(/from\s+['"]\.\.\/\.\.\/\.\.\//)) { | ||
| return { | ||
| severity: "info", | ||
| suggestion: "Use path aliases (e.g., @/) instead of deep relative imports." | ||
| }; | ||
| } | ||
| } | ||
| if (rule.includes("async/await") || rule.includes("async_await")) { | ||
| const thenCount = (code.match(/\.then\s*\(/g) || []).length; | ||
| if (thenCount > 0) { | ||
| return { | ||
| severity: "info", | ||
| suggestion: "Use async/await instead of .then() chains." | ||
| }; | ||
| } | ||
| } | ||
| if (rule.includes("explicit return type") || rule.includes("return types")) { | ||
| const functionsWithoutReturnType = code.match(/(?:export\s+)?(?:async\s+)?function\s+\w+\s*\([^)]*\)\s*\{/g) || []; | ||
| if (functionsWithoutReturnType.length > 0) { | ||
| return { | ||
| severity: "info", | ||
| suggestion: "Add explicit return type annotations to functions." | ||
| }; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function checkAntiPatternViolation(code, filePath, ap) { | ||
| const pattern = ap.pattern.toLowerCase(); | ||
| if (pattern.includes("console.log") || pattern.includes("console statement")) { | ||
| const matches = code.match(/console\.(log|debug|info)\s*\(/g) || []; | ||
| if (matches.length > 0 && !filePath.includes(".test.") && !filePath.includes(".spec.")) { | ||
| return { | ||
| severity: ap.severity === "critical" ? "error" : "warning", | ||
| suggestion: ap.correct_approach || "Remove console statements or use a proper logger." | ||
| }; | ||
| } | ||
| } | ||
| if (pattern.includes("any type") || pattern.includes("typescript any")) { | ||
| if (code.match(/:\s*any\b/) && !code.includes("eslint-disable")) { | ||
| return { | ||
| severity: "warning", | ||
| suggestion: ap.correct_approach || "Use specific types instead of `any`." | ||
| }; | ||
| } | ||
| } | ||
| if (pattern.includes("todo") && pattern.includes("comment")) { | ||
| if (code.match(/\/\/\s*TODO/i)) { | ||
| return { | ||
| severity: "info", | ||
| suggestion: ap.correct_approach || "Resolve TODO comments before submitting." | ||
| }; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| // src/tools/get-knowledge.ts | ||
| function handleGetKnowledge(intelligence2, input) { | ||
| const topic = input.topic.toLowerCase(); | ||
| const exactMatch = Object.entries(intelligence2.knowledge_graph).find(([key]) => key.toLowerCase() === topic); | ||
| if (exactMatch) { | ||
| return formatKnowledge(exactMatch[0], exactMatch[1]); | ||
| } | ||
| const fuzzyMatches = Object.entries(intelligence2.knowledge_graph).filter( | ||
| ([key]) => key.toLowerCase().includes(topic) || topic.includes(key.toLowerCase()) | ||
| ); | ||
| if (fuzzyMatches.length > 0) { | ||
| const sections = fuzzyMatches.map(([key, entries]) => formatKnowledge(key, entries)); | ||
| return sections.join("\n\n"); | ||
| } | ||
| const availableTopics = Object.keys(intelligence2.knowledge_graph); | ||
| if (availableTopics.length === 0) { | ||
| return "No domain knowledge available yet. Run `codehabits sync` to analyze PRs and extract knowledge."; | ||
| } | ||
| return `No knowledge found for topic: "${input.topic}" | ||
| Available topics: ${availableTopics.join(", ")}`; | ||
| } | ||
| function formatKnowledge(topic, entries) { | ||
| const lines = [`## ${topic}`, ""]; | ||
| for (const [key, entry] of Object.entries(entries)) { | ||
| lines.push(`### ${key}`); | ||
| lines.push(entry.value); | ||
| lines.push(`(confidence: ${Math.round(entry.confidence * 100)}%, sources: ${entry.source_prs.length} PRs)`); | ||
| lines.push(""); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| // src/tools/suggest-reviewers.ts | ||
| function handleSuggestReviewers(intelligence2, input) { | ||
| if (intelligence2.reviewers.length === 0) { | ||
| return []; | ||
| } | ||
| const changedDirs = /* @__PURE__ */ new Set(); | ||
| for (const file of input.files) { | ||
| const parts = file.split("/"); | ||
| if (parts.length > 1) { | ||
| changedDirs.add(parts.slice(0, 2).join("/")); | ||
| changedDirs.add(parts[0]); | ||
| } else { | ||
| changedDirs.add(parts[0]); | ||
| } | ||
| } | ||
| const scored = []; | ||
| for (const reviewer of intelligence2.reviewers) { | ||
| let relevance = 0; | ||
| const matchedAreas = []; | ||
| for (const expertise of reviewer.expertise) { | ||
| const area = expertise.area.toLowerCase(); | ||
| for (const dir of changedDirs) { | ||
| const dirLower = dir.toLowerCase(); | ||
| if (area.includes(dirLower) || dirLower.includes(area)) { | ||
| relevance += expertise.confidence * expertise.review_count; | ||
| matchedAreas.push(expertise.area); | ||
| } | ||
| } | ||
| for (const file of input.files) { | ||
| const ext = file.split(".").pop()?.toLowerCase() || ""; | ||
| if (area.includes(ext) || area.includes(file.split("/").pop() || "")) { | ||
| relevance += expertise.confidence * 0.5; | ||
| if (!matchedAreas.includes(expertise.area)) { | ||
| matchedAreas.push(expertise.area); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (relevance > 0) { | ||
| scored.push({ | ||
| login: reviewer.github_login, | ||
| relevance: Math.round(relevance * 100) / 100, | ||
| areas: [...new Set(matchedAreas)] | ||
| }); | ||
| } | ||
| } | ||
| return scored.sort((a, b) => b.relevance - a.relevance).slice(0, 5); | ||
| } | ||
| // src/tools/record-feedback.ts | ||
| import { recordProposal } from "@codehabits/cli/mcp-proposals"; | ||
| function handleRecordFeedback(cwd, input) { | ||
| const result = recordProposal(cwd, input); | ||
| if (!result.ok) { | ||
| return { text: `Error: ${result.error}` }; | ||
| } | ||
| return { | ||
| text: `Recorded proposal ${result.id}. It is pending in .codehabits/proposals.json. Use approve_proposal to apply it to tracked intelligence.` | ||
| }; | ||
| } | ||
| // src/tools/approve-proposal.ts | ||
| import { approveProposal } from "@codehabits/cli/mcp-proposals"; | ||
| async function handleApproveProposal(cwd, proposalId) { | ||
| const result = await approveProposal(cwd, proposalId); | ||
| if (!result.ok) { | ||
| return { text: `Error: ${result.error}` }; | ||
| } | ||
| return { | ||
| text: `Proposal ${proposalId} approved. conventions.json and generated skill files were updated. Commit tracked .codehabits/, .claude/skills/, .agents/skills/, and AGENTS.md when ready.` | ||
| }; | ||
| } | ||
| // src/server.ts | ||
| var intelligence = null; | ||
| async function startServer() { | ||
| const cwd = process.cwd(); | ||
| intelligence = readIntelligenceFile(cwd); | ||
| if (!intelligence) { | ||
| console.error("Warning: No .codehabits/ intelligence files found in current directory."); | ||
| console.error("Run `codehabits enable` to generate intelligence first."); | ||
| } | ||
| const unwatch = watchIntelligenceFile(cwd, (updated) => { | ||
| intelligence = updated; | ||
| if (updated) { | ||
| console.error("Intelligence file updated, reloaded."); | ||
| } | ||
| }); | ||
| const server = new McpServer({ | ||
| name: "codehabits", | ||
| version: "0.1.0" | ||
| }); | ||
| server.tool( | ||
| "get_team_context", | ||
| "Get team coding conventions, anti-patterns, and domain knowledge. Use this before writing or reviewing code to understand how the team works.", | ||
| { | ||
| scope: z4.enum(["all", "imports", "testing", "naming", "async", "api", "auth", "database", "structure", "error-handling", "documentation", "security", "performance", "type-safety"]).optional().describe("Filter context to a specific area"), | ||
| file_path: z4.string().optional().describe("File path to get context specific to that file type") | ||
| }, | ||
| async (params) => { | ||
| if (!intelligence) { | ||
| return { | ||
| content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }] | ||
| }; | ||
| } | ||
| const result = handleGetTeamContext(intelligence, { | ||
| scope: params.scope, | ||
| file_path: params.file_path | ||
| }); | ||
| return { | ||
| content: [{ type: "text", text: result }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "check_code", | ||
| "Validate a code snippet against team conventions and anti-patterns. Returns violations with suggestions.", | ||
| { | ||
| code: z4.string().describe("The code snippet to check"), | ||
| file_path: z4.string().optional().describe("File path for context-aware checking") | ||
| }, | ||
| async (params) => { | ||
| if (!intelligence) { | ||
| return { | ||
| content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }] | ||
| }; | ||
| } | ||
| const result = handleCheckCode(intelligence, { | ||
| code: params.code, | ||
| file_path: params.file_path | ||
| }); | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(result, null, 2) }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_knowledge", | ||
| "Get domain-specific knowledge about a topic (e.g., authentication, database, deployment)", | ||
| { | ||
| topic: z4.string().describe('The topic to look up (e.g., "authentication", "database", "api")') | ||
| }, | ||
| async (params) => { | ||
| if (!intelligence) { | ||
| return { | ||
| content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }] | ||
| }; | ||
| } | ||
| const result = handleGetKnowledge(intelligence, { topic: params.topic }); | ||
| return { | ||
| content: [{ type: "text", text: result }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "suggest_reviewers", | ||
| "Suggest the best reviewers for a set of changed files based on team expertise", | ||
| { | ||
| files: z4.array(z4.string()).describe("List of file paths that were changed") | ||
| }, | ||
| async (params) => { | ||
| if (!intelligence) { | ||
| return { | ||
| content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }] | ||
| }; | ||
| } | ||
| const result = handleSuggestReviewers(intelligence, { files: params.files }); | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(result, null, 2) }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "record_feedback", | ||
| "Record when a user overrides or corrects team intelligence, or propose a new convention. Writes to local .codehabits/proposals.json (gitignored); does not change tracked intelligence until approve_proposal.", | ||
| { | ||
| type: z4.enum(["new_convention", "update_convention", "new_exception", "deprecate"]).describe("Kind of change"), | ||
| targetId: z4.string().optional().describe("ID of existing convention (required for update, exception, or deprecate)"), | ||
| rule: z4.string().optional().describe("Convention rule text (for new conventions)"), | ||
| reason: z4.string().describe("Why this change is being proposed"), | ||
| category: z4.string().optional().describe("Convention category (e.g. testing, naming)"), | ||
| file_path: z4.string().optional().describe("File path where this came up") | ||
| }, | ||
| async (params) => { | ||
| const result = handleRecordFeedback(cwd, { | ||
| type: params.type, | ||
| targetId: params.targetId, | ||
| rule: params.rule, | ||
| reason: params.reason, | ||
| category: params.category, | ||
| filePath: params.file_path | ||
| }); | ||
| return { | ||
| content: [{ type: "text", text: result.text }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "approve_proposal", | ||
| "Approve a pending proposal and merge it into active intelligence (conventions.json) and regenerate skill markdown. Creates tracked git changes.", | ||
| { | ||
| proposal_id: z4.string().describe("Proposal id from record_feedback (e.g. prop-001)") | ||
| }, | ||
| async (params) => { | ||
| const result = await handleApproveProposal(cwd, params.proposal_id); | ||
| return { | ||
| content: [{ type: "text", text: result.text }] | ||
| }; | ||
| } | ||
| ); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| process.on("SIGINT", () => { | ||
| unwatch(); | ||
| process.exit(0); | ||
| }); | ||
| process.on("SIGTERM", () => { | ||
| unwatch(); | ||
| process.exit(0); | ||
| }); | ||
| } | ||
| export { | ||
| startServer | ||
| }; |
+239
| # @codehabits/mcp | ||
| [](https://www.npmjs.com/package/@codehabits/mcp) | ||
| [](https://github.com/codehabits-dev/codehabits/blob/main/LICENSE) | ||
| **MCP server for codehabits team intelligence.** Expose your repository’s conventions, anti-patterns, and domain knowledge to AI coding agents through the [Model Context Protocol](https://modelcontextprotocol.io). | ||
| Works with **Cursor**, **Claude Code**, and other MCP-capable clients. Pair with [@codehabits/cli](https://www.npmjs.com/package/@codehabits/cli) to generate `.codehabits/` from PR history. | ||
| - **Website:** [codehabits.dev](https://codehabits.dev) | ||
| - **Docs:** [codehabits.dev/docs](https://codehabits.dev/docs) | ||
| - **CLI (generate intelligence):** [@codehabits/cli](https://www.npmjs.com/package/@codehabits/cli) | ||
| --- | ||
| ## Prerequisites | ||
| 1. **Node.js 20+** | ||
| 2. Intelligence files in the project root (run once per repo): | ||
| ```bash | ||
| npx @codehabits/cli login | ||
| npx @codehabits/cli enable | ||
| ``` | ||
| The MCP server reads `.codehabits/` from the process **working directory** (`cwd`). It reloads when intelligence files change on disk. | ||
| --- | ||
| ## Quick install (Cursor) | ||
| From your repository root (after `enable`): | ||
| ```bash | ||
| npx @codehabits/cli mcp-install | ||
| ``` | ||
| This writes `.cursor/mcp.json` (or `~/.cursor/mcp.json` with `--global`): | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "codehabits": { | ||
| "command": "npx", | ||
| "args": ["@codehabits/mcp"], | ||
| "cwd": "/absolute/path/to/your/repo" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Restart the IDE so the server loads. | ||
| --- | ||
| ## Manual MCP configuration | ||
| Use the same shape anywhere MCP stdio servers are supported. **Important:** set `cwd` to the repo that contains `.codehabits/`. | ||
| ### Cursor (project) | ||
| Path: `.cursor/mcp.json` | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "codehabits": { | ||
| "command": "npx", | ||
| "args": ["-y", "@codehabits/mcp"], | ||
| "cwd": "${workspaceFolder}" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ### Claude Code (example) | ||
| Add to your Claude MCP config (path varies by install): | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "codehabits": { | ||
| "command": "npx", | ||
| "args": ["-y", "@codehabits/mcp"], | ||
| "cwd": "/path/to/your/repo" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ### Run directly (debug) | ||
| ```bash | ||
| cd /path/to/your/repo | ||
| npx @codehabits/mcp | ||
| ``` | ||
| Stdio transport only; intended for IDE integration, not HTTP. | ||
| --- | ||
| ## Tools | ||
| | Tool | Description | | ||
| | --- | --- | | ||
| | `get_team_context` | Conventions, anti-patterns, and knowledge; optionally filtered by `scope` or `file_path` | | ||
| | `check_code` | Validate a code snippet against team rules; returns violations and suggestions | | ||
| | `get_knowledge` | Look up a domain topic (e.g. `authentication`, `database`, `deployment`) | | ||
| | `suggest_reviewers` | Rank reviewers for a list of changed file paths | | ||
| | `record_feedback` | Propose a convention change or correction (writes local `proposals.json`, gitignored) | | ||
| | `approve_proposal` | Merge an approved proposal into tracked intelligence and regenerate skills | | ||
| ### `get_team_context` | ||
| Use **before** writing or reviewing code. | ||
| | Parameter | Type | Description | | ||
| | --- | --- | --- | | ||
| | `scope` | enum (optional) | `all`, `imports`, `testing`, `naming`, `async`, `api`, `auth`, `database`, `structure`, `error-handling`, `documentation`, `security`, `performance`, `type-safety` | | ||
| | `file_path` | string (optional) | Tailor context to a file type or path | | ||
| ### `check_code` | ||
| | Parameter | Type | Description | | ||
| | --- | --- | --- | | ||
| | `code` | string | Snippet to validate | | ||
| | `file_path` | string (optional) | Path for context-aware rules | | ||
| ### `get_knowledge` | ||
| | Parameter | Type | Description | | ||
| | --- | --- | --- | | ||
| | `topic` | string | Topic name (fuzzy match supported) | | ||
| ### `suggest_reviewers` | ||
| | Parameter | Type | Description | | ||
| | --- | --- | --- | | ||
| | `files` | string[] | Changed file paths | | ||
| ### `record_feedback` / `approve_proposal` | ||
| Agents can suggest updates when the user overrides team rules: | ||
| 1. `record_feedback` stores a proposal in `.codehabits/proposals.json` (not committed by default). | ||
| 2. Human reviews, then `approve_proposal` with `proposal_id` updates `conventions.json` and regenerates skill markdown / `AGENTS.md` (creates normal git changes to commit). | ||
| | `record_feedback` field | Description | | ||
| | --- | --- | | ||
| | `type` | `new_convention`, `update_convention`, `new_exception`, `deprecate` | | ||
| | `targetId` | Existing convention id (for update/exception/deprecate) | | ||
| | `rule` | Rule text (new conventions) | | ||
| | `reason` | Why the change is proposed | | ||
| | `category` | e.g. `testing`, `naming` | | ||
| | `file_path` | Where the issue appeared | | ||
| --- | ||
| ## Example agent workflows | ||
| **Before implementing a feature** | ||
| > Call `get_team_context` with `scope: "api"` or `file_path` set to the file you are editing. | ||
| **During review** | ||
| > Call `check_code` on the diff snippet; fix violations the tool reports. | ||
| **When the model disagrees with a convention** | ||
| > Call `record_feedback` with `type: "new_exception"` and a clear `reason`; a teammate runs `approve_proposal` after review. | ||
| **PR assignment** | ||
| > Call `suggest_reviewers` with the list of changed paths. | ||
| --- | ||
| ## How it works | ||
| ```mermaid | ||
| sequenceDiagram | ||
| participant IDE as IDE / Agent | ||
| participant MCP as @codehabits/mcp | ||
| participant FS as .codehabits/*.json | ||
| IDE->>MCP: tools/call (stdio) | ||
| MCP->>FS: read + watch | ||
| FS-->>MCP: IntelligenceFile | ||
| MCP-->>IDE: conventions / violations / knowledge | ||
| ``` | ||
| - **Transport:** MCP over stdio (`@modelcontextprotocol/sdk`). | ||
| - **State:** Loaded from disk; no separate database. | ||
| - **Hot reload:** File watcher updates in-memory intelligence when JSON changes (e.g. after `codehabits sync`). | ||
| If intelligence is missing, tools return a message asking you to run `codehabits enable` first. | ||
| --- | ||
| ## Passive vs active intelligence | ||
| | Mechanism | Package | When agents use it | | ||
| | --- | --- | --- | | ||
| | `AGENTS.md` + `.claude/skills/` + `.agents/skills/` | CLI `enable` | Automatically in context / skill discovery | | ||
| | MCP tools (this package) | `@codehabits/mcp` | On demand during chat or agent loops | | ||
| Use both: skills for always-on team voice; MCP for lookups, checks, and structured feedback. | ||
| --- | ||
| ## Troubleshooting | ||
| | Symptom | Fix | | ||
| | --- | --- | | ||
| | “No intelligence data available” | Run `npx @codehabits/cli enable` in the repo; confirm `.codehabits/meta.json` exists | | ||
| | Tools return stale rules | Run `npx @codehabits/cli sync`; restart IDE if the watcher did not fire | | ||
| | Server starts but wrong conventions | Check MCP `cwd` points at the repo root, not a subfolder | | ||
| | `npx` fails in IDE | Use full path to `codehabits-mcp` or pin version: `npx -y @codehabits/mcp@0.1.1` | | ||
| Verify integration: | ||
| ```bash | ||
| npx @codehabits/cli status | ||
| ``` | ||
| --- | ||
| ## Related packages | ||
| | Package | Role | | ||
| | --- | --- | | ||
| | [@codehabits/cli](https://www.npmjs.com/package/@codehabits/cli) | Login, enable, sync, `mcp-install`, CI tokens | | ||
| --- | ||
| ## License | ||
| MIT © [codehabits](https://codehabits.dev) |
| #!/usr/bin/env node | ||
| import { | ||
| startServer | ||
| } from "../chunk-AXQMVMBH.js"; | ||
| } from "../chunk-KUSOV7Y3.js"; | ||
@@ -6,0 +6,0 @@ // bin/codehabits-mcp.ts |
| import { | ||
| startServer | ||
| } from "../chunk-AXQMVMBH.js"; | ||
| } from "../chunk-KUSOV7Y3.js"; | ||
| export { | ||
| startServer | ||
| }; |
+11
-5
| { | ||
| "name": "@codehabits/mcp", | ||
| "version": "0.1.1", | ||
| "description": "MCP server for Codehabits team intelligence. Serves team conventions, anti-patterns, and domain knowledge to AI coding agents via the Model Context Protocol.", | ||
| "version": "0.1.2", | ||
| "description": "MCP server for codehabits: get_team_context, check_code, get_knowledge, and reviewer tools over stdio for Cursor, Claude Code, and MCP-compatible agents.", | ||
| "type": "module", | ||
@@ -18,5 +18,10 @@ "license": "MIT", | ||
| "cursor", | ||
| "claude-code", | ||
| "copilot", | ||
| "vscode", | ||
| "conventions", | ||
| "code-review", | ||
| "team-intelligence" | ||
| "agent-skills", | ||
| "team-intelligence", | ||
| "codehabits" | ||
| ], | ||
@@ -30,3 +35,4 @@ "engines": { | ||
| "files": [ | ||
| "dist" | ||
| "dist", | ||
| "README.md" | ||
| ], | ||
@@ -36,3 +42,3 @@ "dependencies": { | ||
| "zod": "^4.3.6", | ||
| "@codehabits/cli": "0.2.2" | ||
| "@codehabits/cli": "0.2.3" | ||
| }, | ||
@@ -39,0 +45,0 @@ "devDependencies": { |
| // src/server.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { z as z3 } from "zod"; | ||
| // src/intelligence/reader.ts | ||
| import { readFileSync, existsSync, watchFile, unwatchFile } from "fs"; | ||
| import { join } from "path"; | ||
| // ../shared/src/schemas/intelligence.ts | ||
| import { z } from "zod"; | ||
| var intelligenceSourceSchema = z.enum([ | ||
| "code_analysis", | ||
| "config_import", | ||
| "stack_baseline", | ||
| "git_history", | ||
| "pr_analysis", | ||
| "rejection_analysis", | ||
| "comment_analysis", | ||
| "agent_proposal" | ||
| ]); | ||
| var maturityLevelSchema = z.union([ | ||
| z.literal(1), | ||
| z.literal(2), | ||
| z.literal(3), | ||
| z.literal(4) | ||
| ]); | ||
| var maturityLabelSchema = z.enum([ | ||
| "baseline", | ||
| "growing", | ||
| "established", | ||
| "mature" | ||
| ]); | ||
| var conventionCategorySchema = z.enum([ | ||
| "imports", | ||
| "testing", | ||
| "naming", | ||
| "async", | ||
| "structure", | ||
| "error-handling", | ||
| "api", | ||
| "documentation", | ||
| "security", | ||
| "performance", | ||
| "type-safety", | ||
| "other" | ||
| ]); | ||
| var antiPatternSeveritySchema = z.enum([ | ||
| "critical", | ||
| "high", | ||
| "medium", | ||
| "low" | ||
| ]); | ||
| var techStackSchema = z.object({ | ||
| language: z.string(), | ||
| languages: z.array(z.string()).optional(), | ||
| framework: z.string().optional(), | ||
| framework_version: z.string().optional(), | ||
| test_framework: z.string().optional(), | ||
| linter: z.string().optional(), | ||
| formatter: z.string().optional(), | ||
| build_tool: z.string().optional(), | ||
| package_manager: z.string().optional() | ||
| }); | ||
| var conventionExampleSchema = z.object({ | ||
| pr_number: z.number().optional(), | ||
| file_path: z.string(), | ||
| snippet: z.string().optional() | ||
| }); | ||
| var conventionSchema = z.object({ | ||
| id: z.string(), | ||
| category: conventionCategorySchema, | ||
| rule: z.string(), | ||
| confidence: z.number().min(0).max(1), | ||
| source: intelligenceSourceSchema, | ||
| evidence_count: z.number(), | ||
| first_seen: z.string(), | ||
| examples: z.array(conventionExampleSchema), | ||
| metadata: z.record(z.string(), z.unknown()).optional() | ||
| }); | ||
| var antiPatternEvidenceTypeSchema = z.enum([ | ||
| "rejected_pr", | ||
| "review_comment", | ||
| "bug_fix" | ||
| ]); | ||
| function normalizeAntiPatternEvidenceType(input) { | ||
| if (input === null || input === void 0 || input === "") { | ||
| return "review_comment"; | ||
| } | ||
| const s = String(input).trim().toLowerCase().replace(/-/g, "_").replace(/\s+/g, "_"); | ||
| if (s === "rejected_pr" || s === "rejection" || s === "pr_rejection" || s === "rejected" || s.includes("reject")) { | ||
| return "rejected_pr"; | ||
| } | ||
| if (s === "bug_fix" || s === "bugfix" || s === "fix" || s.includes("bug") && s.includes("fix")) { | ||
| return "bug_fix"; | ||
| } | ||
| if (s === "review_comment" || s === "comment" || s === "pr_comment" || s === "inline_comment" || s === "discussion" || s === "review" || s.includes("comment")) { | ||
| return "review_comment"; | ||
| } | ||
| if (s === "merged_pr" || s === "merged" || s === "pr") { | ||
| return "review_comment"; | ||
| } | ||
| return "review_comment"; | ||
| } | ||
| var antiPatternEvidenceSchema = z.object({ | ||
| pr_number: z.coerce.number(), | ||
| type: z.preprocess( | ||
| (raw) => normalizeAntiPatternEvidenceType(raw), | ||
| antiPatternEvidenceTypeSchema | ||
| ), | ||
| excerpt: z.string().optional() | ||
| }); | ||
| var antiPatternSchema = z.object({ | ||
| id: z.string(), | ||
| pattern: z.string(), | ||
| severity: antiPatternSeveritySchema, | ||
| reason: z.string(), | ||
| correct_approach: z.string(), | ||
| evidence: z.array(antiPatternEvidenceSchema) | ||
| }); | ||
| var knowledgeEntrySchema = z.object({ | ||
| value: z.string(), | ||
| confidence: z.number().min(0).max(1), | ||
| source_prs: z.array(z.number()) | ||
| }); | ||
| var knowledgeGraphSchema = z.record( | ||
| z.string(), | ||
| z.record(z.string(), knowledgeEntrySchema) | ||
| ); | ||
| var reviewerExpertiseSchema = z.object({ | ||
| area: z.string(), | ||
| confidence: z.number().min(0).max(1), | ||
| review_count: z.number() | ||
| }); | ||
| var reviewerStatsSchema = z.object({ | ||
| total_reviews: z.number(), | ||
| avg_review_time_hours: z.number(), | ||
| approval_rate: z.number().min(0).max(1) | ||
| }); | ||
| var reviewerProfileSchema = z.object({ | ||
| github_login: z.string(), | ||
| expertise: z.array(reviewerExpertiseSchema), | ||
| stats: reviewerStatsSchema | ||
| }); | ||
| var riskFactorSchema = z.object({ | ||
| type: z.enum(["file", "size", "timing", "complexity"]), | ||
| description: z.string(), | ||
| value: z.string(), | ||
| risk_multiplier: z.number(), | ||
| evidence: z.object({ | ||
| total_prs: z.number(), | ||
| incidents: z.number() | ||
| }) | ||
| }); | ||
| var maturitySchema = z.object({ | ||
| level: maturityLevelSchema, | ||
| label: maturityLabelSchema, | ||
| prs_analyzed: z.number(), | ||
| next_level_at: z.number(), | ||
| sources: z.array(intelligenceSourceSchema) | ||
| }); | ||
| var repositoryInfoSchema = z.object({ | ||
| owner: z.string(), | ||
| name: z.string(), | ||
| full_name: z.string(), | ||
| analyzed_prs: z.number(), | ||
| analyzed_period: z.object({ | ||
| from: z.string(), | ||
| to: z.string() | ||
| }), | ||
| tech_stack: techStackSchema, | ||
| codehabits_version: z.string() | ||
| }); | ||
| var intelligenceFileSchema = z.object({ | ||
| version: z.string(), | ||
| generated_at: z.string(), | ||
| last_synced_at: z.string(), | ||
| maturity: maturitySchema, | ||
| repository: repositoryInfoSchema, | ||
| conventions: z.array(conventionSchema), | ||
| anti_patterns: z.array(antiPatternSchema), | ||
| knowledge_graph: knowledgeGraphSchema, | ||
| reviewers: z.array(reviewerProfileSchema), | ||
| risk_factors: z.array(riskFactorSchema) | ||
| }); | ||
| var conventionsFileSchema = z.array(conventionSchema); | ||
| var antiPatternsFileSchema = z.array(antiPatternSchema); | ||
| var knowledgeFileSchema = knowledgeGraphSchema; | ||
| var reviewersFileSchema = z.array(reviewerProfileSchema); | ||
| var intelligenceMetaSchema = z.object({ | ||
| version: z.string(), | ||
| generated_at: z.string(), | ||
| last_synced_at: z.string(), | ||
| maturity: maturitySchema, | ||
| repository: repositoryInfoSchema, | ||
| risk_factors: z.array(riskFactorSchema), | ||
| counts: z.object({ | ||
| conventions: z.number(), | ||
| anti_patterns: z.number(), | ||
| reviewers: z.number(), | ||
| knowledge_topics: z.number() | ||
| }) | ||
| }); | ||
| var proposalSchema = z.object({ | ||
| id: z.string(), | ||
| type: z.enum(["new_convention", "update_convention", "new_exception", "deprecate"]), | ||
| targetId: z.string().optional(), | ||
| data: conventionSchema.partial(), | ||
| reason: z.string(), | ||
| filePath: z.string().optional(), | ||
| createdAt: z.string(), | ||
| status: z.enum(["pending", "approved", "rejected"]), | ||
| approvedBy: z.string().optional() | ||
| }); | ||
| var proposalsFileSchema = z.array(proposalSchema); | ||
| // ../shared/src/schemas/config.ts | ||
| import { z as z2 } from "zod"; | ||
| var customRuleSchema = z2.object({ | ||
| name: z2.string(), | ||
| severity: z2.enum(["error", "warning", "info"]), | ||
| description: z2.string() | ||
| }); | ||
| var CodehabitsConfigSchema = z2.object({ | ||
| version: z2.string(), | ||
| analysis: z2.object({ | ||
| lookback_months: z2.number().default(12), | ||
| min_prs_for_pattern: z2.number().default(10), | ||
| min_confidence: z2.number().min(0).max(1).default(0.7), | ||
| include_closed_prs: z2.boolean().default(true) | ||
| }), | ||
| ignore: z2.object({ | ||
| files: z2.array(z2.string()).default([]), | ||
| authors: z2.array(z2.string()).default(["dependabot", "renovate"]), | ||
| labels: z2.array(z2.string()).default(["wip", "draft"]) | ||
| }), | ||
| sync: z2.object({ | ||
| auto_commit: z2.boolean().default(false) | ||
| }), | ||
| custom_rules: z2.array(customRuleSchema).default([]) | ||
| }); | ||
| var userConfigSchema = z2.object({ | ||
| auth: z2.object({ | ||
| token: z2.string(), | ||
| github_token: z2.string(), | ||
| user: z2.object({ | ||
| id: z2.string(), | ||
| login: z2.string(), | ||
| email: z2.string() | ||
| }) | ||
| }).optional(), | ||
| defaults: z2.object({ | ||
| ai_model: z2.string().default("deepseek") | ||
| }).optional() | ||
| }); | ||
| // ../shared/src/constants.ts | ||
| var PATHS = { | ||
| /** @deprecated Use individual file paths instead */ | ||
| INTELLIGENCE_FILE: ".codehabits/intelligence.json", | ||
| CONVENTIONS_FILE: ".codehabits/conventions.json", | ||
| ANTI_PATTERNS_FILE: ".codehabits/anti-patterns.json", | ||
| KNOWLEDGE_FILE: ".codehabits/knowledge.json", | ||
| REVIEWERS_FILE: ".codehabits/reviewers.json", | ||
| META_FILE: ".codehabits/meta.json", | ||
| PROPOSALS_FILE: ".codehabits/proposals.json", | ||
| README_FILE: ".codehabits/README.md", | ||
| CODEHABITS_GITIGNORE: ".codehabits/.gitignore", | ||
| CONFIG_FILE: ".codehabits/config.json", | ||
| SKILL_DIR: ".cursor/skills/codehabits-team-intel", | ||
| SKILL_FILE: ".cursor/skills/codehabits-team-intel/SKILL.md", | ||
| SKILL_REFERENCES: ".cursor/skills/codehabits-team-intel/references", | ||
| SKILL_SCRIPTS: ".cursor/skills/codehabits-team-intel/scripts", | ||
| USER_CONFIG: ".codehabits/config.json" | ||
| }; | ||
| var AI_MODELS = { | ||
| FREE: "deepseek/deepseek-v3.2", | ||
| PAID: "anthropic/claude-sonnet-4-20250514" | ||
| }; | ||
| var PLAN_LIMITS = { | ||
| free: { | ||
| private_repos: 1, | ||
| pr_lookback: 100, | ||
| ai_model: AI_MODELS.FREE | ||
| }, | ||
| team: { | ||
| private_repos: Infinity, | ||
| pr_lookback: Infinity, | ||
| ai_model: AI_MODELS.PAID | ||
| }, | ||
| enterprise: { | ||
| private_repos: Infinity, | ||
| pr_lookback: Infinity, | ||
| ai_model: AI_MODELS.PAID | ||
| } | ||
| }; | ||
| // src/intelligence/reader.ts | ||
| var SPLIT_FILES = [ | ||
| PATHS.META_FILE, | ||
| PATHS.CONVENTIONS_FILE, | ||
| PATHS.ANTI_PATTERNS_FILE, | ||
| PATHS.KNOWLEDGE_FILE, | ||
| PATHS.REVIEWERS_FILE | ||
| ]; | ||
| function readIntelligenceFile(cwd) { | ||
| const metaPath = join(cwd, PATHS.META_FILE); | ||
| if (existsSync(metaPath)) { | ||
| return readSplitFiles(cwd); | ||
| } | ||
| return readLegacyFile(cwd); | ||
| } | ||
| function watchIntelligenceFile(cwd, onChange) { | ||
| const reload = () => { | ||
| onChange(readIntelligenceFile(cwd)); | ||
| }; | ||
| const allPaths = [ | ||
| join(cwd, PATHS.INTELLIGENCE_FILE), | ||
| ...SPLIT_FILES.map((rel) => join(cwd, rel)) | ||
| ]; | ||
| for (const abs of allPaths) { | ||
| watchFile(abs, { interval: 2e3 }, reload); | ||
| } | ||
| return () => { | ||
| for (const abs of allPaths) { | ||
| unwatchFile(abs, reload); | ||
| } | ||
| }; | ||
| } | ||
| function readSplitFiles(cwd) { | ||
| try { | ||
| const meta = readJson(join(cwd, PATHS.META_FILE), intelligenceMetaSchema); | ||
| if (!meta) return null; | ||
| const conventions = readJson(join(cwd, PATHS.CONVENTIONS_FILE), conventionsFileSchema) ?? []; | ||
| const antiPatterns = readJson(join(cwd, PATHS.ANTI_PATTERNS_FILE), antiPatternsFileSchema) ?? []; | ||
| const knowledgeGraph = readJson(join(cwd, PATHS.KNOWLEDGE_FILE), knowledgeFileSchema) ?? {}; | ||
| const reviewers = readJson(join(cwd, PATHS.REVIEWERS_FILE), reviewersFileSchema) ?? []; | ||
| return { | ||
| version: meta.version, | ||
| generated_at: meta.generated_at, | ||
| last_synced_at: meta.last_synced_at, | ||
| maturity: meta.maturity, | ||
| repository: meta.repository, | ||
| conventions, | ||
| anti_patterns: antiPatterns, | ||
| knowledge_graph: knowledgeGraph, | ||
| reviewers, | ||
| risk_factors: meta.risk_factors | ||
| }; | ||
| } catch (error) { | ||
| console.error("Failed to read split intelligence files:", error); | ||
| return null; | ||
| } | ||
| } | ||
| function readLegacyFile(cwd) { | ||
| return readJson( | ||
| join(cwd, PATHS.INTELLIGENCE_FILE), | ||
| intelligenceFileSchema | ||
| ); | ||
| } | ||
| function readJson(filePath, schema) { | ||
| if (!existsSync(filePath)) return null; | ||
| try { | ||
| const raw = readFileSync(filePath, "utf-8"); | ||
| const parsed = JSON.parse(raw); | ||
| return schema.parse(parsed); | ||
| } catch (error) { | ||
| console.error(`Failed to read ${filePath}:`, error); | ||
| return null; | ||
| } | ||
| } | ||
| // src/tools/get-team-context.ts | ||
| function handleGetTeamContext(intelligence2, input) { | ||
| const scope = input.scope || "all"; | ||
| const sections = []; | ||
| sections.push(`# Team Intelligence: ${intelligence2.repository.full_name}`); | ||
| sections.push( | ||
| `Maturity: ${intelligence2.maturity.label} | PRs analyzed: ${intelligence2.maturity.prs_analyzed}` | ||
| ); | ||
| sections.push( | ||
| `Tech stack: ${intelligence2.repository.tech_stack.language}${intelligence2.repository.tech_stack.framework ? ` / ${intelligence2.repository.tech_stack.framework}` : ""}` | ||
| ); | ||
| sections.push(""); | ||
| let conventions; | ||
| if (scope === "all") { | ||
| conventions = intelligence2.conventions.slice(0, 20); | ||
| } else { | ||
| conventions = intelligence2.conventions.filter((c) => { | ||
| if (c.category === scope) return true; | ||
| if (input.file_path) { | ||
| return isConventionRelevantToFile(c, input.file_path); | ||
| } | ||
| return false; | ||
| }); | ||
| if (conventions.length < 3) { | ||
| conventions = [ | ||
| ...conventions, | ||
| ...intelligence2.conventions.filter((c) => c.category === scope).slice(0, 5) | ||
| ]; | ||
| } | ||
| } | ||
| if (conventions.length > 0) { | ||
| sections.push("## Conventions"); | ||
| sections.push(""); | ||
| for (const conv of conventions) { | ||
| sections.push( | ||
| `- **${conv.rule}** (${conv.category}, ${Math.round(conv.confidence * 100)}% confidence)` | ||
| ); | ||
| } | ||
| sections.push(""); | ||
| } | ||
| let antiPatterns; | ||
| if (scope === "all") { | ||
| antiPatterns = intelligence2.anti_patterns.slice(0, 10); | ||
| } else { | ||
| antiPatterns = intelligence2.anti_patterns.filter((ap) => { | ||
| const patternLower = ap.pattern.toLowerCase(); | ||
| const scopeLower = scope.toLowerCase(); | ||
| return patternLower.includes(scopeLower); | ||
| }); | ||
| if (antiPatterns.length === 0) { | ||
| antiPatterns = intelligence2.anti_patterns.slice(0, 5); | ||
| } | ||
| } | ||
| if (antiPatterns.length > 0) { | ||
| sections.push("## Anti-Patterns (Avoid These)"); | ||
| sections.push(""); | ||
| for (const ap of antiPatterns) { | ||
| sections.push( | ||
| `- **${ap.pattern}** [${ap.severity}] - ${ap.correct_approach}` | ||
| ); | ||
| } | ||
| sections.push(""); | ||
| } | ||
| if (scope !== "all") { | ||
| const relevantTopics = Object.entries(intelligence2.knowledge_graph).filter( | ||
| ([topic]) => topic.toLowerCase().includes(scope.toLowerCase()) | ||
| ); | ||
| if (relevantTopics.length > 0) { | ||
| sections.push("## Domain Knowledge"); | ||
| sections.push(""); | ||
| for (const [topic, entries] of relevantTopics) { | ||
| sections.push(`### ${topic}`); | ||
| for (const [key, entry] of Object.entries(entries)) { | ||
| sections.push(`- ${key}: ${entry.value}`); | ||
| } | ||
| } | ||
| sections.push(""); | ||
| } | ||
| } | ||
| if (input.file_path) { | ||
| const fileReviewers = intelligence2.reviewers.filter( | ||
| (r) => r.expertise.some( | ||
| (e) => e.area.split("/").some((part) => input.file_path.includes(part)) | ||
| ) | ||
| ).slice(0, 3); | ||
| if (fileReviewers.length > 0) { | ||
| sections.push("## Suggested Reviewers for This File"); | ||
| sections.push(""); | ||
| for (const r of fileReviewers) { | ||
| sections.push( | ||
| `- @${r.github_login} (${r.stats.total_reviews} reviews)` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| return sections.join("\n"); | ||
| } | ||
| function isConventionRelevantToFile(conv, filePath) { | ||
| const fp = filePath.toLowerCase(); | ||
| if (fp.includes(".test.") || fp.includes(".spec.") || fp.includes("__tests__")) { | ||
| return conv.category === "testing"; | ||
| } | ||
| if (fp.includes("/api/") || fp.includes("route.ts") || fp.includes("handler")) { | ||
| return conv.category === "api" || conv.category === "error-handling"; | ||
| } | ||
| if (fp.includes("component") || fp.endsWith(".tsx")) { | ||
| return conv.category === "naming" || conv.category === "structure"; | ||
| } | ||
| return false; | ||
| } | ||
| // src/tools/check-code.ts | ||
| function handleCheckCode(intelligence2, input) { | ||
| const violations = []; | ||
| const code = input.code; | ||
| const filePath = input.file_path || ""; | ||
| for (const conv of intelligence2.conventions) { | ||
| if (conv.confidence < 0.7) continue; | ||
| const violation = checkConventionViolation(code, filePath, conv); | ||
| if (violation) { | ||
| violations.push({ | ||
| convention_id: conv.id, | ||
| rule: conv.rule, | ||
| severity: violation.severity, | ||
| suggestion: violation.suggestion | ||
| }); | ||
| } | ||
| } | ||
| for (const ap of intelligence2.anti_patterns) { | ||
| const violation = checkAntiPatternViolation(code, filePath, ap); | ||
| if (violation) { | ||
| violations.push({ | ||
| convention_id: ap.id, | ||
| rule: ap.pattern, | ||
| severity: violation.severity, | ||
| suggestion: violation.suggestion | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| violations, | ||
| passed: violations.filter((v) => v.severity === "error").length === 0 | ||
| }; | ||
| } | ||
| function checkConventionViolation(code, filePath, conv) { | ||
| const rule = conv.rule.toLowerCase(); | ||
| if (rule.includes("named export") && conv.category === "imports") { | ||
| if (code.includes("export default ") && !filePath.includes("page.") && !filePath.includes("layout.")) { | ||
| return { | ||
| severity: "warning", | ||
| suggestion: "Use named exports. Replace `export default X` with `export { X }` or `export const/function X`." | ||
| }; | ||
| } | ||
| } | ||
| if (rule.includes("path alias") && conv.category === "imports") { | ||
| if (code.match(/from\s+['"]\.\.\/\.\.\/\.\.\//)) { | ||
| return { | ||
| severity: "info", | ||
| suggestion: "Use path aliases (e.g., @/) instead of deep relative imports." | ||
| }; | ||
| } | ||
| } | ||
| if (rule.includes("async/await") || rule.includes("async_await")) { | ||
| const thenCount = (code.match(/\.then\s*\(/g) || []).length; | ||
| if (thenCount > 0) { | ||
| return { | ||
| severity: "info", | ||
| suggestion: "Use async/await instead of .then() chains." | ||
| }; | ||
| } | ||
| } | ||
| if (rule.includes("explicit return type") || rule.includes("return types")) { | ||
| const functionsWithoutReturnType = code.match(/(?:export\s+)?(?:async\s+)?function\s+\w+\s*\([^)]*\)\s*\{/g) || []; | ||
| if (functionsWithoutReturnType.length > 0) { | ||
| return { | ||
| severity: "info", | ||
| suggestion: "Add explicit return type annotations to functions." | ||
| }; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function checkAntiPatternViolation(code, filePath, ap) { | ||
| const pattern = ap.pattern.toLowerCase(); | ||
| if (pattern.includes("console.log") || pattern.includes("console statement")) { | ||
| const matches = code.match(/console\.(log|debug|info)\s*\(/g) || []; | ||
| if (matches.length > 0 && !filePath.includes(".test.") && !filePath.includes(".spec.")) { | ||
| return { | ||
| severity: ap.severity === "critical" ? "error" : "warning", | ||
| suggestion: ap.correct_approach || "Remove console statements or use a proper logger." | ||
| }; | ||
| } | ||
| } | ||
| if (pattern.includes("any type") || pattern.includes("typescript any")) { | ||
| if (code.match(/:\s*any\b/) && !code.includes("eslint-disable")) { | ||
| return { | ||
| severity: "warning", | ||
| suggestion: ap.correct_approach || "Use specific types instead of `any`." | ||
| }; | ||
| } | ||
| } | ||
| if (pattern.includes("todo") && pattern.includes("comment")) { | ||
| if (code.match(/\/\/\s*TODO/i)) { | ||
| return { | ||
| severity: "info", | ||
| suggestion: ap.correct_approach || "Resolve TODO comments before submitting." | ||
| }; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| // src/tools/get-knowledge.ts | ||
| function handleGetKnowledge(intelligence2, input) { | ||
| const topic = input.topic.toLowerCase(); | ||
| const exactMatch = Object.entries(intelligence2.knowledge_graph).find(([key]) => key.toLowerCase() === topic); | ||
| if (exactMatch) { | ||
| return formatKnowledge(exactMatch[0], exactMatch[1]); | ||
| } | ||
| const fuzzyMatches = Object.entries(intelligence2.knowledge_graph).filter( | ||
| ([key]) => key.toLowerCase().includes(topic) || topic.includes(key.toLowerCase()) | ||
| ); | ||
| if (fuzzyMatches.length > 0) { | ||
| const sections = fuzzyMatches.map(([key, entries]) => formatKnowledge(key, entries)); | ||
| return sections.join("\n\n"); | ||
| } | ||
| const availableTopics = Object.keys(intelligence2.knowledge_graph); | ||
| if (availableTopics.length === 0) { | ||
| return "No domain knowledge available yet. Run `codehabits sync` to analyze PRs and extract knowledge."; | ||
| } | ||
| return `No knowledge found for topic: "${input.topic}" | ||
| Available topics: ${availableTopics.join(", ")}`; | ||
| } | ||
| function formatKnowledge(topic, entries) { | ||
| const lines = [`## ${topic}`, ""]; | ||
| for (const [key, entry] of Object.entries(entries)) { | ||
| lines.push(`### ${key}`); | ||
| lines.push(entry.value); | ||
| lines.push(`(confidence: ${Math.round(entry.confidence * 100)}%, sources: ${entry.source_prs.length} PRs)`); | ||
| lines.push(""); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| // src/tools/suggest-reviewers.ts | ||
| function handleSuggestReviewers(intelligence2, input) { | ||
| if (intelligence2.reviewers.length === 0) { | ||
| return []; | ||
| } | ||
| const changedDirs = /* @__PURE__ */ new Set(); | ||
| for (const file of input.files) { | ||
| const parts = file.split("/"); | ||
| if (parts.length > 1) { | ||
| changedDirs.add(parts.slice(0, 2).join("/")); | ||
| changedDirs.add(parts[0]); | ||
| } else { | ||
| changedDirs.add(parts[0]); | ||
| } | ||
| } | ||
| const scored = []; | ||
| for (const reviewer of intelligence2.reviewers) { | ||
| let relevance = 0; | ||
| const matchedAreas = []; | ||
| for (const expertise of reviewer.expertise) { | ||
| const area = expertise.area.toLowerCase(); | ||
| for (const dir of changedDirs) { | ||
| const dirLower = dir.toLowerCase(); | ||
| if (area.includes(dirLower) || dirLower.includes(area)) { | ||
| relevance += expertise.confidence * expertise.review_count; | ||
| matchedAreas.push(expertise.area); | ||
| } | ||
| } | ||
| for (const file of input.files) { | ||
| const ext = file.split(".").pop()?.toLowerCase() || ""; | ||
| if (area.includes(ext) || area.includes(file.split("/").pop() || "")) { | ||
| relevance += expertise.confidence * 0.5; | ||
| if (!matchedAreas.includes(expertise.area)) { | ||
| matchedAreas.push(expertise.area); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (relevance > 0) { | ||
| scored.push({ | ||
| login: reviewer.github_login, | ||
| relevance: Math.round(relevance * 100) / 100, | ||
| areas: [...new Set(matchedAreas)] | ||
| }); | ||
| } | ||
| } | ||
| return scored.sort((a, b) => b.relevance - a.relevance).slice(0, 5); | ||
| } | ||
| // src/tools/record-feedback.ts | ||
| import { recordProposal } from "@codehabits/cli/mcp-proposals"; | ||
| function handleRecordFeedback(cwd, input) { | ||
| const result = recordProposal(cwd, input); | ||
| if (!result.ok) { | ||
| return { text: `Error: ${result.error}` }; | ||
| } | ||
| return { | ||
| text: `Recorded proposal ${result.id}. It is pending in .codehabits/proposals.json. Use approve_proposal to apply it to tracked intelligence.` | ||
| }; | ||
| } | ||
| // src/tools/approve-proposal.ts | ||
| import { approveProposal } from "@codehabits/cli/mcp-proposals"; | ||
| async function handleApproveProposal(cwd, proposalId) { | ||
| const result = await approveProposal(cwd, proposalId); | ||
| if (!result.ok) { | ||
| return { text: `Error: ${result.error}` }; | ||
| } | ||
| return { | ||
| text: `Proposal ${proposalId} approved. conventions.json and generated skill files were updated. Commit the tracked .codehabits/ and .cursor/skills/ changes when ready.` | ||
| }; | ||
| } | ||
| // src/server.ts | ||
| var intelligence = null; | ||
| async function startServer() { | ||
| const cwd = process.cwd(); | ||
| intelligence = readIntelligenceFile(cwd); | ||
| if (!intelligence) { | ||
| console.error("Warning: No .codehabits/ intelligence files found in current directory."); | ||
| console.error("Run `codehabits enable` to generate intelligence first."); | ||
| } | ||
| const unwatch = watchIntelligenceFile(cwd, (updated) => { | ||
| intelligence = updated; | ||
| if (updated) { | ||
| console.error("Intelligence file updated, reloaded."); | ||
| } | ||
| }); | ||
| const server = new McpServer({ | ||
| name: "codehabits", | ||
| version: "0.1.0" | ||
| }); | ||
| server.tool( | ||
| "get_team_context", | ||
| "Get team coding conventions, anti-patterns, and domain knowledge. Use this before writing or reviewing code to understand how the team works.", | ||
| { | ||
| scope: z3.enum(["all", "imports", "testing", "naming", "async", "api", "auth", "database", "structure", "error-handling", "documentation", "security", "performance", "type-safety"]).optional().describe("Filter context to a specific area"), | ||
| file_path: z3.string().optional().describe("File path to get context specific to that file type") | ||
| }, | ||
| async (params) => { | ||
| if (!intelligence) { | ||
| return { | ||
| content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }] | ||
| }; | ||
| } | ||
| const result = handleGetTeamContext(intelligence, { | ||
| scope: params.scope, | ||
| file_path: params.file_path | ||
| }); | ||
| return { | ||
| content: [{ type: "text", text: result }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "check_code", | ||
| "Validate a code snippet against team conventions and anti-patterns. Returns violations with suggestions.", | ||
| { | ||
| code: z3.string().describe("The code snippet to check"), | ||
| file_path: z3.string().optional().describe("File path for context-aware checking") | ||
| }, | ||
| async (params) => { | ||
| if (!intelligence) { | ||
| return { | ||
| content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }] | ||
| }; | ||
| } | ||
| const result = handleCheckCode(intelligence, { | ||
| code: params.code, | ||
| file_path: params.file_path | ||
| }); | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(result, null, 2) }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_knowledge", | ||
| "Get domain-specific knowledge about a topic (e.g., authentication, database, deployment)", | ||
| { | ||
| topic: z3.string().describe('The topic to look up (e.g., "authentication", "database", "api")') | ||
| }, | ||
| async (params) => { | ||
| if (!intelligence) { | ||
| return { | ||
| content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }] | ||
| }; | ||
| } | ||
| const result = handleGetKnowledge(intelligence, { topic: params.topic }); | ||
| return { | ||
| content: [{ type: "text", text: result }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "suggest_reviewers", | ||
| "Suggest the best reviewers for a set of changed files based on team expertise", | ||
| { | ||
| files: z3.array(z3.string()).describe("List of file paths that were changed") | ||
| }, | ||
| async (params) => { | ||
| if (!intelligence) { | ||
| return { | ||
| content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }] | ||
| }; | ||
| } | ||
| const result = handleSuggestReviewers(intelligence, { files: params.files }); | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(result, null, 2) }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "record_feedback", | ||
| "Record when a user overrides or corrects team intelligence, or propose a new convention. Writes to local .codehabits/proposals.json (gitignored); does not change tracked intelligence until approve_proposal.", | ||
| { | ||
| type: z3.enum(["new_convention", "update_convention", "new_exception", "deprecate"]).describe("Kind of change"), | ||
| targetId: z3.string().optional().describe("ID of existing convention (required for update, exception, or deprecate)"), | ||
| rule: z3.string().optional().describe("Convention rule text (for new conventions)"), | ||
| reason: z3.string().describe("Why this change is being proposed"), | ||
| category: z3.string().optional().describe("Convention category (e.g. testing, naming)"), | ||
| file_path: z3.string().optional().describe("File path where this came up") | ||
| }, | ||
| async (params) => { | ||
| const result = handleRecordFeedback(cwd, { | ||
| type: params.type, | ||
| targetId: params.targetId, | ||
| rule: params.rule, | ||
| reason: params.reason, | ||
| category: params.category, | ||
| filePath: params.file_path | ||
| }); | ||
| return { | ||
| content: [{ type: "text", text: result.text }] | ||
| }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "approve_proposal", | ||
| "Approve a pending proposal and merge it into active intelligence (conventions.json) and regenerate skill markdown. Creates tracked git changes.", | ||
| { | ||
| proposal_id: z3.string().describe("Proposal id from record_feedback (e.g. prop-001)") | ||
| }, | ||
| async (params) => { | ||
| const result = await handleApproveProposal(cwd, params.proposal_id); | ||
| return { | ||
| content: [{ type: "text", text: result.text }] | ||
| }; | ||
| } | ||
| ); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| process.on("SIGINT", () => { | ||
| unwatch(); | ||
| process.exit(0); | ||
| }); | ||
| process.on("SIGTERM", () => { | ||
| unwatch(); | ||
| process.exit(0); | ||
| }); | ||
| } | ||
| export { | ||
| startServer | ||
| }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
37596
30.31%7
16.67%877
3.66%0
-100%240
Infinity%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
Updated