@codebakers/cli 3.3.6 → 3.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp/server.ts CHANGED
@@ -1211,6 +1211,104 @@ class CodeBakersServer {
1211
1211
  },
1212
1212
  },
1213
1213
  },
1214
+ // Ripple Detection Tool
1215
+ {
1216
+ name: 'ripple_check',
1217
+ description:
1218
+ 'Detect all files affected by a change to a type, schema, function, or component. Use this BEFORE making breaking changes to understand impact, or AFTER to find files that need updating. Returns a list of files that import/use the entity and may need updates.',
1219
+ inputSchema: {
1220
+ type: 'object' as const,
1221
+ properties: {
1222
+ entityName: {
1223
+ type: 'string',
1224
+ description: 'Name of the type, schema, function, or component that changed (e.g., "User", "createOrder", "Button")',
1225
+ },
1226
+ changeType: {
1227
+ type: 'string',
1228
+ enum: ['added_field', 'removed_field', 'renamed', 'type_changed', 'signature_changed', 'other'],
1229
+ description: 'What kind of change was made',
1230
+ },
1231
+ changeDescription: {
1232
+ type: 'string',
1233
+ description: 'Brief description of the change (e.g., "added teamId field", "renamed email to emailAddress")',
1234
+ },
1235
+ },
1236
+ required: ['entityName'],
1237
+ },
1238
+ },
1239
+ // ============================================
1240
+ // DEPENDENCY GUARDIAN - Auto-Coherence System
1241
+ // ============================================
1242
+ {
1243
+ name: 'guardian_analyze',
1244
+ description:
1245
+ 'AUTOMATIC: Analyzes code for consistency issues, broken contracts, and coherence problems. Runs silently after every code generation. Returns issues found with auto-fix suggestions. User never needs to call this directly.',
1246
+ inputSchema: {
1247
+ type: 'object' as const,
1248
+ properties: {
1249
+ files: {
1250
+ type: 'array',
1251
+ items: { type: 'string' },
1252
+ description: 'Files that were just created or modified',
1253
+ },
1254
+ changeContext: {
1255
+ type: 'string',
1256
+ description: 'What change was made (e.g., "added user authentication", "created invoice API")',
1257
+ },
1258
+ },
1259
+ required: ['files'],
1260
+ },
1261
+ },
1262
+ {
1263
+ name: 'guardian_heal',
1264
+ description:
1265
+ 'AUTOMATIC: Fixes consistency issues, broken imports, type mismatches, and contract violations. Runs automatically when guardian_analyze finds issues. User never needs to call this directly.',
1266
+ inputSchema: {
1267
+ type: 'object' as const,
1268
+ properties: {
1269
+ issues: {
1270
+ type: 'array',
1271
+ items: {
1272
+ type: 'object',
1273
+ properties: {
1274
+ file: { type: 'string' },
1275
+ issue: { type: 'string' },
1276
+ fix: { type: 'string' },
1277
+ },
1278
+ },
1279
+ description: 'Issues to fix (from guardian_analyze)',
1280
+ },
1281
+ autoFix: {
1282
+ type: 'boolean',
1283
+ description: 'Whether to automatically apply fixes (default: true)',
1284
+ },
1285
+ },
1286
+ required: ['issues'],
1287
+ },
1288
+ },
1289
+ {
1290
+ name: 'guardian_verify',
1291
+ description:
1292
+ 'AUTOMATIC: Verifies that all changes are coherent and the codebase compiles. Runs TypeScript check, verifies imports, and checks for common issues. Returns pass/fail with details.',
1293
+ inputSchema: {
1294
+ type: 'object' as const,
1295
+ properties: {
1296
+ deep: {
1297
+ type: 'boolean',
1298
+ description: 'Run deep verification including cross-file contract checking (default: false for speed)',
1299
+ },
1300
+ },
1301
+ },
1302
+ },
1303
+ {
1304
+ name: 'guardian_status',
1305
+ description:
1306
+ 'Shows the current coherence status of the project. Reports any known issues, pending fixes, and overall health score.',
1307
+ inputSchema: {
1308
+ type: 'object' as const,
1309
+ properties: {},
1310
+ },
1311
+ },
1214
1312
  ],
1215
1313
  }));
1216
1314
 
@@ -1372,6 +1470,22 @@ class CodeBakersServer {
1372
1470
  case 'vapi_generate_webhook':
1373
1471
  return this.handleVapiGenerateWebhook(args as { events?: string[] });
1374
1472
 
1473
+ case 'ripple_check':
1474
+ return this.handleRippleCheck(args as { entityName: string; changeType?: string; changeDescription?: string });
1475
+
1476
+ // Dependency Guardian - Auto-Coherence System
1477
+ case 'guardian_analyze':
1478
+ return this.handleGuardianAnalyze(args as { files: string[]; changeContext?: string });
1479
+
1480
+ case 'guardian_heal':
1481
+ return this.handleGuardianHeal(args as { issues: Array<{ file: string; issue: string; fix: string }>; autoFix?: boolean });
1482
+
1483
+ case 'guardian_verify':
1484
+ return this.handleGuardianVerify(args as { deep?: boolean });
1485
+
1486
+ case 'guardian_status':
1487
+ return this.handleGuardianStatus();
1488
+
1375
1489
  default:
1376
1490
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1377
1491
  }
@@ -6126,6 +6240,949 @@ ${events.includes('call-started') ? ` case 'call-started':
6126
6240
  return { content: [{ type: 'text' as const, text: response }] };
6127
6241
  }
6128
6242
 
6243
+ // ============================================
6244
+ // DEPENDENCY GUARDIAN - Auto-Coherence System
6245
+ // ============================================
6246
+
6247
+ /**
6248
+ * Guardian Analyze - Detects consistency issues in changed files
6249
+ * Runs automatically after code generation
6250
+ */
6251
+ private handleGuardianAnalyze(args: { files: string[]; changeContext?: string }) {
6252
+ const { files, changeContext } = args;
6253
+ const cwd = process.cwd();
6254
+
6255
+ interface Issue {
6256
+ file: string;
6257
+ line: number;
6258
+ issue: string;
6259
+ severity: 'error' | 'warning' | 'info';
6260
+ fix: string;
6261
+ autoFixable: boolean;
6262
+ }
6263
+
6264
+ const issues: Issue[] = [];
6265
+ const analyzed: string[] = [];
6266
+
6267
+ // Helper to check if an import path resolves
6268
+ const checkImportResolves = (importPath: string, fromFile: string): boolean => {
6269
+ const fromDir = path.dirname(fromFile);
6270
+
6271
+ // Handle alias imports (@/)
6272
+ if (importPath.startsWith('@/')) {
6273
+ const aliasPath = path.join(cwd, 'src', importPath.slice(2));
6274
+ return fs.existsSync(aliasPath + '.ts') ||
6275
+ fs.existsSync(aliasPath + '.tsx') ||
6276
+ fs.existsSync(path.join(aliasPath, 'index.ts')) ||
6277
+ fs.existsSync(path.join(aliasPath, 'index.tsx'));
6278
+ }
6279
+
6280
+ // Handle relative imports
6281
+ if (importPath.startsWith('.')) {
6282
+ const resolved = path.resolve(fromDir, importPath);
6283
+ return fs.existsSync(resolved + '.ts') ||
6284
+ fs.existsSync(resolved + '.tsx') ||
6285
+ fs.existsSync(path.join(resolved, 'index.ts')) ||
6286
+ fs.existsSync(path.join(resolved, 'index.tsx'));
6287
+ }
6288
+
6289
+ // Node modules - assume they exist
6290
+ return true;
6291
+ };
6292
+
6293
+ // Helper to extract imports from a file
6294
+ const extractImports = (content: string): Array<{ path: string; names: string[]; line: number }> => {
6295
+ const imports: Array<{ path: string; names: string[]; line: number }> = [];
6296
+ const lines = content.split('\n');
6297
+
6298
+ lines.forEach((line, i) => {
6299
+ // Match: import { X, Y } from 'path'
6300
+ const namedMatch = line.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
6301
+ if (namedMatch) {
6302
+ const names = namedMatch[1].split(',').map(n => n.trim().split(' as ')[0]);
6303
+ imports.push({ path: namedMatch[2], names, line: i + 1 });
6304
+ }
6305
+
6306
+ // Match: import X from 'path'
6307
+ const defaultMatch = line.match(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/);
6308
+ if (defaultMatch && !namedMatch) {
6309
+ imports.push({ path: defaultMatch[2], names: [defaultMatch[1]], line: i + 1 });
6310
+ }
6311
+ });
6312
+
6313
+ return imports;
6314
+ };
6315
+
6316
+ // Helper to extract exports from a file
6317
+ const extractExports = (content: string): string[] => {
6318
+ const exports: string[] = [];
6319
+
6320
+ // Match: export const/function/class/type/interface X
6321
+ const exportMatches = content.matchAll(/export\s+(const|function|class|type|interface)\s+(\w+)/g);
6322
+ for (const match of exportMatches) {
6323
+ exports.push(match[2]);
6324
+ }
6325
+
6326
+ // Match: export { X, Y }
6327
+ const namedExportMatch = content.match(/export\s+\{([^}]+)\}/);
6328
+ if (namedExportMatch) {
6329
+ const names = namedExportMatch[1].split(',').map(n => n.trim().split(' as ')[0]);
6330
+ exports.push(...names);
6331
+ }
6332
+
6333
+ // Match: export default
6334
+ if (content.includes('export default')) {
6335
+ exports.push('default');
6336
+ }
6337
+
6338
+ return exports;
6339
+ };
6340
+
6341
+ // Analyze each file
6342
+ for (const file of files) {
6343
+ const fullPath = path.isAbsolute(file) ? file : path.join(cwd, file);
6344
+
6345
+ if (!fs.existsSync(fullPath)) {
6346
+ issues.push({
6347
+ file,
6348
+ line: 0,
6349
+ issue: `File does not exist: ${file}`,
6350
+ severity: 'error',
6351
+ fix: `Create the file or remove the reference`,
6352
+ autoFixable: false,
6353
+ });
6354
+ continue;
6355
+ }
6356
+
6357
+ try {
6358
+ const content = fs.readFileSync(fullPath, 'utf-8');
6359
+ const lines = content.split('\n');
6360
+ analyzed.push(file);
6361
+
6362
+ // Check 1: Broken imports
6363
+ const imports = extractImports(content);
6364
+ for (const imp of imports) {
6365
+ if (!checkImportResolves(imp.path, fullPath)) {
6366
+ issues.push({
6367
+ file,
6368
+ line: imp.line,
6369
+ issue: `Broken import: '${imp.path}' does not resolve`,
6370
+ severity: 'error',
6371
+ fix: `Check the import path or create the missing module`,
6372
+ autoFixable: false,
6373
+ });
6374
+ }
6375
+ }
6376
+
6377
+ // Check 2: Unused imports (basic check)
6378
+ for (const imp of imports) {
6379
+ for (const name of imp.names) {
6380
+ // Skip if it's a type-only import or if name appears elsewhere in file
6381
+ const usageCount = (content.match(new RegExp(`\\b${name}\\b`, 'g')) || []).length;
6382
+ if (usageCount === 1) { // Only appears in import
6383
+ issues.push({
6384
+ file,
6385
+ line: imp.line,
6386
+ issue: `Unused import: '${name}' is imported but never used`,
6387
+ severity: 'warning',
6388
+ fix: `Remove '${name}' from imports`,
6389
+ autoFixable: true,
6390
+ });
6391
+ }
6392
+ }
6393
+ }
6394
+
6395
+ // Check 3: console.log in production code (not in test files)
6396
+ if (!file.includes('.test.') && !file.includes('.spec.')) {
6397
+ lines.forEach((line, i) => {
6398
+ if (line.includes('console.log(') && !line.trim().startsWith('//')) {
6399
+ issues.push({
6400
+ file,
6401
+ line: i + 1,
6402
+ issue: `console.log found in production code`,
6403
+ severity: 'warning',
6404
+ fix: `Remove console.log or replace with proper logging`,
6405
+ autoFixable: true,
6406
+ });
6407
+ }
6408
+ });
6409
+ }
6410
+
6411
+ // Check 4: TODO/FIXME comments
6412
+ lines.forEach((line, i) => {
6413
+ if (line.includes('TODO:') || line.includes('FIXME:')) {
6414
+ issues.push({
6415
+ file,
6416
+ line: i + 1,
6417
+ issue: `Unresolved TODO/FIXME comment`,
6418
+ severity: 'info',
6419
+ fix: `Complete the TODO or remove if no longer needed`,
6420
+ autoFixable: false,
6421
+ });
6422
+ }
6423
+ });
6424
+
6425
+ // Check 5: Type 'any' usage
6426
+ lines.forEach((line, i) => {
6427
+ if (line.includes(': any') || line.includes('<any>') || line.includes('as any')) {
6428
+ issues.push({
6429
+ file,
6430
+ line: i + 1,
6431
+ issue: `'any' type used - weakens type safety`,
6432
+ severity: 'warning',
6433
+ fix: `Replace 'any' with a proper type`,
6434
+ autoFixable: false,
6435
+ });
6436
+ }
6437
+ });
6438
+
6439
+ // Check 6: Missing error handling in async functions
6440
+ if (content.includes('async ') && !content.includes('try {') && !content.includes('.catch(')) {
6441
+ issues.push({
6442
+ file,
6443
+ line: 1,
6444
+ issue: `Async function without error handling`,
6445
+ severity: 'warning',
6446
+ fix: `Add try/catch or .catch() to handle errors`,
6447
+ autoFixable: false,
6448
+ });
6449
+ }
6450
+
6451
+ // Check 7: API routes without proper error handling
6452
+ if (file.includes('/api/') && file.endsWith('route.ts')) {
6453
+ if (!content.includes('try {') || !content.includes('catch')) {
6454
+ issues.push({
6455
+ file,
6456
+ line: 1,
6457
+ issue: `API route without try/catch error handling`,
6458
+ severity: 'error',
6459
+ fix: `Wrap handler logic in try/catch with proper error response`,
6460
+ autoFixable: false,
6461
+ });
6462
+ }
6463
+ }
6464
+
6465
+ // Check 8: Missing return type on exported functions
6466
+ const funcMatches = content.matchAll(/export\s+(async\s+)?function\s+(\w+)\s*\([^)]*\)\s*{/g);
6467
+ for (const match of funcMatches) {
6468
+ const fullMatch = match[0];
6469
+ if (!fullMatch.includes(':')) { // No return type
6470
+ issues.push({
6471
+ file,
6472
+ line: 1,
6473
+ issue: `Exported function '${match[2]}' missing return type`,
6474
+ severity: 'info',
6475
+ fix: `Add explicit return type annotation`,
6476
+ autoFixable: false,
6477
+ });
6478
+ }
6479
+ }
6480
+
6481
+ } catch (err) {
6482
+ issues.push({
6483
+ file,
6484
+ line: 0,
6485
+ issue: `Could not analyze file: ${err instanceof Error ? err.message : 'Unknown error'}`,
6486
+ severity: 'error',
6487
+ fix: `Check file permissions and encoding`,
6488
+ autoFixable: false,
6489
+ });
6490
+ }
6491
+ }
6492
+
6493
+ // Also check cross-file consistency
6494
+ // Look for imports of the changed files from other files
6495
+ const searchDirs = ['src', 'app', 'lib', 'components', 'services'];
6496
+ const extensions = ['.ts', '.tsx'];
6497
+
6498
+ const searchDir = (dir: string) => {
6499
+ if (!fs.existsSync(dir)) return;
6500
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
6501
+
6502
+ for (const entry of entries) {
6503
+ const fullPath = path.join(dir, entry.name);
6504
+
6505
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
6506
+ searchDir(fullPath);
6507
+ } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
6508
+ // Check if this file imports any of our changed files
6509
+ const relativePath = path.relative(cwd, fullPath);
6510
+ if (files.includes(relativePath)) continue; // Skip files we're already analyzing
6511
+
6512
+ try {
6513
+ const content = fs.readFileSync(fullPath, 'utf-8');
6514
+ const imports = extractImports(content);
6515
+
6516
+ for (const imp of imports) {
6517
+ // Check if import matches any of our changed files
6518
+ for (const changedFile of files) {
6519
+ const changedBasename = path.basename(changedFile, path.extname(changedFile));
6520
+ if (imp.path.includes(changedBasename)) {
6521
+ // This file imports a changed file - verify the imports still exist
6522
+ const changedFullPath = path.join(cwd, changedFile);
6523
+ if (fs.existsSync(changedFullPath)) {
6524
+ const changedContent = fs.readFileSync(changedFullPath, 'utf-8');
6525
+ const exports = extractExports(changedContent);
6526
+
6527
+ for (const name of imp.names) {
6528
+ if (!exports.includes(name) && name !== 'default') {
6529
+ issues.push({
6530
+ file: relativePath,
6531
+ line: imp.line,
6532
+ issue: `Import '${name}' no longer exported from '${changedFile}'`,
6533
+ severity: 'error',
6534
+ fix: `Update import or add export to '${changedFile}'`,
6535
+ autoFixable: false,
6536
+ });
6537
+ }
6538
+ }
6539
+ }
6540
+ }
6541
+ }
6542
+ }
6543
+ } catch {
6544
+ // Skip files that can't be read
6545
+ }
6546
+ }
6547
+ }
6548
+ };
6549
+
6550
+ for (const dir of searchDirs) {
6551
+ searchDir(path.join(cwd, dir));
6552
+ }
6553
+
6554
+ // Build response
6555
+ let response = `# 🛡️ Dependency Guardian Analysis\n\n`;
6556
+
6557
+ if (changeContext) {
6558
+ response += `**Context:** ${changeContext}\n\n`;
6559
+ }
6560
+
6561
+ response += `**Files Analyzed:** ${analyzed.length}\n`;
6562
+ response += `**Issues Found:** ${issues.length}\n\n`;
6563
+
6564
+ if (issues.length === 0) {
6565
+ response += `## ✅ All Clear!\n\n`;
6566
+ response += `No consistency issues detected. Code is coherent.\n`;
6567
+ } else {
6568
+ const errors = issues.filter(i => i.severity === 'error');
6569
+ const warnings = issues.filter(i => i.severity === 'warning');
6570
+ const infos = issues.filter(i => i.severity === 'info');
6571
+ const autoFixable = issues.filter(i => i.autoFixable);
6572
+
6573
+ response += `| Severity | Count |\n`;
6574
+ response += `|----------|-------|\n`;
6575
+ response += `| 🔴 Errors | ${errors.length} |\n`;
6576
+ response += `| 🟡 Warnings | ${warnings.length} |\n`;
6577
+ response += `| 🔵 Info | ${infos.length} |\n`;
6578
+ response += `| 🔧 Auto-fixable | ${autoFixable.length} |\n\n`;
6579
+
6580
+ if (errors.length > 0) {
6581
+ response += `## 🔴 Errors (Must Fix)\n\n`;
6582
+ for (const issue of errors) {
6583
+ response += `### \`${issue.file}:${issue.line}\`\n`;
6584
+ response += `**Issue:** ${issue.issue}\n`;
6585
+ response += `**Fix:** ${issue.fix}\n\n`;
6586
+ }
6587
+ }
6588
+
6589
+ if (warnings.length > 0) {
6590
+ response += `## 🟡 Warnings (Should Fix)\n\n`;
6591
+ for (const issue of warnings) {
6592
+ response += `- \`${issue.file}:${issue.line}\` - ${issue.issue}${issue.autoFixable ? ' 🔧' : ''}\n`;
6593
+ }
6594
+ response += `\n`;
6595
+ }
6596
+
6597
+ if (infos.length > 0) {
6598
+ response += `## 🔵 Info (Consider)\n\n`;
6599
+ for (const issue of infos) {
6600
+ response += `- \`${issue.file}:${issue.line}\` - ${issue.issue}\n`;
6601
+ }
6602
+ response += `\n`;
6603
+ }
6604
+
6605
+ if (autoFixable.length > 0) {
6606
+ response += `---\n\n`;
6607
+ response += `**${autoFixable.length} issues can be auto-fixed.** Run \`guardian_heal\` to fix them automatically.\n`;
6608
+ }
6609
+ }
6610
+
6611
+ // Store issues for healing
6612
+ const guardianStatePath = path.join(cwd, '.codebakers', 'guardian-state.json');
6613
+ try {
6614
+ fs.mkdirSync(path.dirname(guardianStatePath), { recursive: true });
6615
+ fs.writeFileSync(guardianStatePath, JSON.stringify({
6616
+ lastAnalysis: new Date().toISOString(),
6617
+ filesAnalyzed: analyzed,
6618
+ issues,
6619
+ changeContext,
6620
+ }, null, 2));
6621
+ } catch {
6622
+ // Ignore write errors
6623
+ }
6624
+
6625
+ return { content: [{ type: 'text' as const, text: response }] };
6626
+ }
6627
+
6628
+ /**
6629
+ * Guardian Heal - Auto-fixes issues found by guardian_analyze
6630
+ */
6631
+ private handleGuardianHeal(args: { issues: Array<{ file: string; issue: string; fix: string }>; autoFix?: boolean }) {
6632
+ const { autoFix = true } = args;
6633
+ const cwd = process.cwd();
6634
+
6635
+ // Load issues from state if not provided
6636
+ let issues = args.issues;
6637
+ if (!issues || issues.length === 0) {
6638
+ const guardianStatePath = path.join(cwd, '.codebakers', 'guardian-state.json');
6639
+ if (fs.existsSync(guardianStatePath)) {
6640
+ try {
6641
+ const state = JSON.parse(fs.readFileSync(guardianStatePath, 'utf-8'));
6642
+ issues = state.issues?.filter((i: { autoFixable?: boolean }) => i.autoFixable) || [];
6643
+ } catch {
6644
+ issues = [];
6645
+ }
6646
+ }
6647
+ }
6648
+
6649
+ if (issues.length === 0) {
6650
+ return {
6651
+ content: [{
6652
+ type: 'text' as const,
6653
+ text: `# 🛡️ Guardian Heal\n\n✅ No issues to fix. Run \`guardian_analyze\` first to detect issues.`
6654
+ }]
6655
+ };
6656
+ }
6657
+
6658
+ let response = `# 🛡️ Guardian Heal\n\n`;
6659
+ const fixed: string[] = [];
6660
+ const failed: string[] = [];
6661
+
6662
+ for (const issue of issues) {
6663
+ const fullPath = path.join(cwd, issue.file);
6664
+
6665
+ if (!fs.existsSync(fullPath)) {
6666
+ failed.push(`${issue.file}: File not found`);
6667
+ continue;
6668
+ }
6669
+
6670
+ try {
6671
+ let content = fs.readFileSync(fullPath, 'utf-8');
6672
+ let modified = false;
6673
+
6674
+ // Fix: Remove unused imports
6675
+ if (issue.issue.includes('Unused import')) {
6676
+ const match = issue.issue.match(/Unused import: '(\w+)'/);
6677
+ if (match) {
6678
+ const name = match[1];
6679
+ // Remove from named imports
6680
+ content = content.replace(new RegExp(`\\b${name}\\b,?\\s*`, 'g'), (m, offset) => {
6681
+ // Only replace in import statements
6682
+ const lineStart = content.lastIndexOf('\n', offset) + 1;
6683
+ const line = content.substring(lineStart, content.indexOf('\n', offset));
6684
+ if (line.includes('import')) {
6685
+ modified = true;
6686
+ return '';
6687
+ }
6688
+ return m;
6689
+ });
6690
+ }
6691
+ }
6692
+
6693
+ // Fix: Remove console.log
6694
+ if (issue.issue.includes('console.log')) {
6695
+ const lines = content.split('\n');
6696
+ const lineIndex = parseInt(issue.issue.match(/line (\d+)/)?.[1] || '0') - 1;
6697
+ if (lineIndex >= 0 && lines[lineIndex]?.includes('console.log')) {
6698
+ lines.splice(lineIndex, 1);
6699
+ content = lines.join('\n');
6700
+ modified = true;
6701
+ }
6702
+ }
6703
+
6704
+ if (modified && autoFix) {
6705
+ fs.writeFileSync(fullPath, content);
6706
+ fixed.push(issue.file);
6707
+ } else if (!modified) {
6708
+ failed.push(`${issue.file}: Could not auto-fix "${issue.issue}"`);
6709
+ }
6710
+ } catch (err) {
6711
+ failed.push(`${issue.file}: ${err instanceof Error ? err.message : 'Unknown error'}`);
6712
+ }
6713
+ }
6714
+
6715
+ if (fixed.length > 0) {
6716
+ response += `## ✅ Fixed (${fixed.length})\n\n`;
6717
+ for (const file of fixed) {
6718
+ response += `- \`${file}\`\n`;
6719
+ }
6720
+ response += `\n`;
6721
+ }
6722
+
6723
+ if (failed.length > 0) {
6724
+ response += `## ⚠️ Could Not Auto-Fix (${failed.length})\n\n`;
6725
+ for (const msg of failed) {
6726
+ response += `- ${msg}\n`;
6727
+ }
6728
+ response += `\nThese require manual intervention.\n`;
6729
+ }
6730
+
6731
+ if (fixed.length > 0) {
6732
+ response += `\n---\n\nRun \`guardian_verify\` to confirm all issues are resolved.\n`;
6733
+ }
6734
+
6735
+ return { content: [{ type: 'text' as const, text: response }] };
6736
+ }
6737
+
6738
+ /**
6739
+ * Guardian Verify - Verifies codebase coherence (TypeScript check, imports, etc.)
6740
+ */
6741
+ private handleGuardianVerify(args: { deep?: boolean }) {
6742
+ const { deep = false } = args;
6743
+ const cwd = process.cwd();
6744
+
6745
+ let response = `# 🛡️ Guardian Verify\n\n`;
6746
+ const checks: Array<{ name: string; status: 'pass' | 'fail' | 'warn'; details: string }> = [];
6747
+
6748
+ // Check 1: TypeScript compilation
6749
+ try {
6750
+ execSync('npx tsc --noEmit', { cwd, stdio: 'pipe', timeout: 60000 });
6751
+ checks.push({ name: 'TypeScript', status: 'pass', details: 'No type errors' });
6752
+ } catch (err) {
6753
+ const output = err instanceof Error && 'stdout' in err ? String((err as { stdout: Buffer }).stdout) : 'Unknown error';
6754
+ const errorCount = (output.match(/error TS/g) || []).length;
6755
+ checks.push({
6756
+ name: 'TypeScript',
6757
+ status: 'fail',
6758
+ details: `${errorCount} type error${errorCount !== 1 ? 's' : ''} found`
6759
+ });
6760
+ }
6761
+
6762
+ // Check 2: Package.json exists and is valid
6763
+ const pkgPath = path.join(cwd, 'package.json');
6764
+ if (fs.existsSync(pkgPath)) {
6765
+ try {
6766
+ JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
6767
+ checks.push({ name: 'package.json', status: 'pass', details: 'Valid JSON' });
6768
+ } catch {
6769
+ checks.push({ name: 'package.json', status: 'fail', details: 'Invalid JSON' });
6770
+ }
6771
+ } else {
6772
+ checks.push({ name: 'package.json', status: 'fail', details: 'File not found' });
6773
+ }
6774
+
6775
+ // Check 3: Environment variables
6776
+ const envExample = path.join(cwd, '.env.example');
6777
+ const envLocal = path.join(cwd, '.env.local');
6778
+ const env = path.join(cwd, '.env');
6779
+
6780
+ if (fs.existsSync(envExample)) {
6781
+ checks.push({ name: '.env.example', status: 'pass', details: 'Exists for documentation' });
6782
+ } else {
6783
+ checks.push({ name: '.env.example', status: 'warn', details: 'Missing - should document required env vars' });
6784
+ }
6785
+
6786
+ if (fs.existsSync(envLocal) || fs.existsSync(env)) {
6787
+ checks.push({ name: '.env', status: 'pass', details: 'Environment configured' });
6788
+ } else {
6789
+ checks.push({ name: '.env', status: 'warn', details: 'No .env file - may need configuration' });
6790
+ }
6791
+
6792
+ // Check 4: Git ignore includes sensitive files
6793
+ const gitignore = path.join(cwd, '.gitignore');
6794
+ if (fs.existsSync(gitignore)) {
6795
+ const content = fs.readFileSync(gitignore, 'utf-8');
6796
+ if (content.includes('.env') && content.includes('node_modules')) {
6797
+ checks.push({ name: '.gitignore', status: 'pass', details: 'Properly configured' });
6798
+ } else {
6799
+ checks.push({ name: '.gitignore', status: 'warn', details: 'May be missing important entries' });
6800
+ }
6801
+ } else {
6802
+ checks.push({ name: '.gitignore', status: 'warn', details: 'No .gitignore file' });
6803
+ }
6804
+
6805
+ // Check 5: Deep mode - check all imports resolve
6806
+ if (deep) {
6807
+ let brokenImports = 0;
6808
+ const searchDirs = ['src', 'app'];
6809
+
6810
+ const checkDir = (dir: string) => {
6811
+ if (!fs.existsSync(dir)) return;
6812
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
6813
+
6814
+ for (const entry of entries) {
6815
+ const fullPath = path.join(dir, entry.name);
6816
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
6817
+ checkDir(fullPath);
6818
+ } else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
6819
+ try {
6820
+ const content = fs.readFileSync(fullPath, 'utf-8');
6821
+ const importMatches = content.matchAll(/from\s+['"]([^'"]+)['"]/g);
6822
+ for (const match of importMatches) {
6823
+ const importPath = match[1];
6824
+ if (importPath.startsWith('.') || importPath.startsWith('@/')) {
6825
+ // Check if resolves
6826
+ let resolved: string;
6827
+ if (importPath.startsWith('@/')) {
6828
+ resolved = path.join(cwd, 'src', importPath.slice(2));
6829
+ } else {
6830
+ resolved = path.resolve(path.dirname(fullPath), importPath);
6831
+ }
6832
+
6833
+ const exists = fs.existsSync(resolved + '.ts') ||
6834
+ fs.existsSync(resolved + '.tsx') ||
6835
+ fs.existsSync(path.join(resolved, 'index.ts')) ||
6836
+ fs.existsSync(path.join(resolved, 'index.tsx'));
6837
+
6838
+ if (!exists) {
6839
+ brokenImports++;
6840
+ }
6841
+ }
6842
+ }
6843
+ } catch {
6844
+ // Skip unreadable files
6845
+ }
6846
+ }
6847
+ }
6848
+ };
6849
+
6850
+ for (const dir of searchDirs) {
6851
+ checkDir(path.join(cwd, dir));
6852
+ }
6853
+
6854
+ if (brokenImports === 0) {
6855
+ checks.push({ name: 'Import Resolution', status: 'pass', details: 'All imports resolve' });
6856
+ } else {
6857
+ checks.push({ name: 'Import Resolution', status: 'fail', details: `${brokenImports} broken import${brokenImports !== 1 ? 's' : ''}` });
6858
+ }
6859
+ }
6860
+
6861
+ // Build response
6862
+ const passed = checks.filter(c => c.status === 'pass').length;
6863
+ const failed = checks.filter(c => c.status === 'fail').length;
6864
+ const warned = checks.filter(c => c.status === 'warn').length;
6865
+
6866
+ const overallStatus = failed > 0 ? '❌ FAIL' : warned > 0 ? '⚠️ WARN' : '✅ PASS';
6867
+
6868
+ response += `## Overall: ${overallStatus}\n\n`;
6869
+ response += `| Check | Status | Details |\n`;
6870
+ response += `|-------|--------|--------|\n`;
6871
+
6872
+ for (const check of checks) {
6873
+ const icon = check.status === 'pass' ? '✅' : check.status === 'fail' ? '❌' : '⚠️';
6874
+ response += `| ${check.name} | ${icon} | ${check.details} |\n`;
6875
+ }
6876
+
6877
+ response += `\n**Summary:** ${passed} passed, ${failed} failed, ${warned} warnings\n`;
6878
+
6879
+ if (failed > 0) {
6880
+ response += `\n---\n\n⚠️ **Action Required:** Fix the failing checks before deploying.\n`;
6881
+ }
6882
+
6883
+ // Update guardian state
6884
+ const guardianStatePath = path.join(cwd, '.codebakers', 'guardian-state.json');
6885
+ try {
6886
+ let state: Record<string, unknown> = {};
6887
+ if (fs.existsSync(guardianStatePath)) {
6888
+ state = JSON.parse(fs.readFileSync(guardianStatePath, 'utf-8'));
6889
+ }
6890
+ state.lastVerification = new Date().toISOString();
6891
+ state.verificationResult = { passed, failed, warned, checks };
6892
+ fs.mkdirSync(path.dirname(guardianStatePath), { recursive: true });
6893
+ fs.writeFileSync(guardianStatePath, JSON.stringify(state, null, 2));
6894
+ } catch {
6895
+ // Ignore write errors
6896
+ }
6897
+
6898
+ return { content: [{ type: 'text' as const, text: response }] };
6899
+ }
6900
+
6901
+ /**
6902
+ * Guardian Status - Shows overall project coherence health
6903
+ */
6904
+ private handleGuardianStatus() {
6905
+ const cwd = process.cwd();
6906
+ const guardianStatePath = path.join(cwd, '.codebakers', 'guardian-state.json');
6907
+
6908
+ let response = `# 🛡️ Dependency Guardian Status\n\n`;
6909
+
6910
+ // Check if guardian state exists
6911
+ if (!fs.existsSync(guardianStatePath)) {
6912
+ response += `## ⚪ Not Initialized\n\n`;
6913
+ response += `Guardian hasn't analyzed this project yet.\n\n`;
6914
+ response += `Run \`guardian_analyze\` on your files to start tracking coherence.\n`;
6915
+ return { content: [{ type: 'text' as const, text: response }] };
6916
+ }
6917
+
6918
+ try {
6919
+ const state = JSON.parse(fs.readFileSync(guardianStatePath, 'utf-8'));
6920
+
6921
+ // Last analysis
6922
+ if (state.lastAnalysis) {
6923
+ const analysisTime = new Date(state.lastAnalysis);
6924
+ const hoursAgo = Math.round((Date.now() - analysisTime.getTime()) / (1000 * 60 * 60));
6925
+ response += `**Last Analysis:** ${hoursAgo < 1 ? 'Just now' : `${hoursAgo} hour${hoursAgo !== 1 ? 's' : ''} ago`}\n`;
6926
+ }
6927
+
6928
+ // Last verification
6929
+ if (state.lastVerification) {
6930
+ const verifyTime = new Date(state.lastVerification);
6931
+ const hoursAgo = Math.round((Date.now() - verifyTime.getTime()) / (1000 * 60 * 60));
6932
+ response += `**Last Verification:** ${hoursAgo < 1 ? 'Just now' : `${hoursAgo} hour${hoursAgo !== 1 ? 's' : ''} ago`}\n`;
6933
+ }
6934
+
6935
+ response += `\n`;
6936
+
6937
+ // Issue summary
6938
+ if (state.issues && state.issues.length > 0) {
6939
+ const errors = state.issues.filter((i: { severity: string }) => i.severity === 'error').length;
6940
+ const warnings = state.issues.filter((i: { severity: string }) => i.severity === 'warning').length;
6941
+
6942
+ if (errors > 0) {
6943
+ response += `## ❌ Issues Found\n\n`;
6944
+ response += `- 🔴 ${errors} error${errors !== 1 ? 's' : ''}\n`;
6945
+ response += `- 🟡 ${warnings} warning${warnings !== 1 ? 's' : ''}\n\n`;
6946
+ response += `Run \`guardian_heal\` to auto-fix what's possible.\n`;
6947
+ } else if (warnings > 0) {
6948
+ response += `## ⚠️ Warnings\n\n`;
6949
+ response += `- 🟡 ${warnings} warning${warnings !== 1 ? 's' : ''} (no critical errors)\n\n`;
6950
+ } else {
6951
+ response += `## ✅ All Clear\n\n`;
6952
+ response += `No known issues in the codebase.\n`;
6953
+ }
6954
+ } else {
6955
+ response += `## ✅ No Issues\n\n`;
6956
+ response += `Last analysis found no problems.\n`;
6957
+ }
6958
+
6959
+ // Verification result
6960
+ if (state.verificationResult) {
6961
+ const { passed, failed, warned } = state.verificationResult;
6962
+ response += `\n### Verification Summary\n`;
6963
+ response += `✅ ${passed} checks passed\n`;
6964
+ if (failed > 0) response += `❌ ${failed} checks failed\n`;
6965
+ if (warned > 0) response += `⚠️ ${warned} warnings\n`;
6966
+ }
6967
+
6968
+ // Health score
6969
+ const issueCount = state.issues?.length || 0;
6970
+ const errorCount = state.issues?.filter((i: { severity: string }) => i.severity === 'error').length || 0;
6971
+ const verifyFailed = state.verificationResult?.failed || 0;
6972
+
6973
+ let healthScore = 100;
6974
+ healthScore -= errorCount * 20;
6975
+ healthScore -= verifyFailed * 15;
6976
+ healthScore -= (issueCount - errorCount) * 5;
6977
+ healthScore = Math.max(0, Math.min(100, healthScore));
6978
+
6979
+ response += `\n---\n\n`;
6980
+ response += `## Health Score: ${healthScore}/100 ${healthScore >= 80 ? '🟢' : healthScore >= 50 ? '🟡' : '🔴'}\n`;
6981
+
6982
+ } catch {
6983
+ response += `## ⚠️ State Corrupted\n\n`;
6984
+ response += `Could not read guardian state. Run \`guardian_analyze\` to rebuild.\n`;
6985
+ }
6986
+
6987
+ return { content: [{ type: 'text' as const, text: response }] };
6988
+ }
6989
+
6990
+ /**
6991
+ * Ripple Check - Detect all files affected by a change to a type/schema/function
6992
+ * Searches the codebase for imports and usages of the entity
6993
+ */
6994
+ private handleRippleCheck(args: { entityName: string; changeType?: string; changeDescription?: string }) {
6995
+ const { entityName, changeType, changeDescription } = args;
6996
+ const cwd = process.cwd();
6997
+
6998
+ let response = `# 🌊 Ripple Check: \`${entityName}\`\n\n`;
6999
+
7000
+ if (changeType || changeDescription) {
7001
+ response += `**Change:** ${changeDescription || changeType || 'Unknown'}\n\n`;
7002
+ }
7003
+
7004
+ // Define search directories
7005
+ const searchDirs = ['src', 'app', 'lib', 'components', 'services', 'types', 'utils', 'hooks'];
7006
+ const extensions = ['.ts', '.tsx', '.js', '.jsx'];
7007
+
7008
+ interface FileMatch {
7009
+ file: string;
7010
+ importLine?: string;
7011
+ usageLines: string[];
7012
+ lineNumbers: number[];
7013
+ isDefinition: boolean;
7014
+ }
7015
+
7016
+ const matches: FileMatch[] = [];
7017
+ let definitionFile: string | null = null;
7018
+
7019
+ // Helper to search a directory recursively
7020
+ const searchDir = (dir: string) => {
7021
+ if (!fs.existsSync(dir)) return;
7022
+
7023
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
7024
+ for (const entry of entries) {
7025
+ const fullPath = path.join(dir, entry.name);
7026
+
7027
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
7028
+ searchDir(fullPath);
7029
+ } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
7030
+ try {
7031
+ const content = fs.readFileSync(fullPath, 'utf-8');
7032
+ const lines = content.split('\n');
7033
+
7034
+ // Check if file contains the entity
7035
+ if (!content.includes(entityName)) continue;
7036
+
7037
+ const relativePath = path.relative(cwd, fullPath);
7038
+ const match: FileMatch = {
7039
+ file: relativePath,
7040
+ usageLines: [],
7041
+ lineNumbers: [],
7042
+ isDefinition: false,
7043
+ };
7044
+
7045
+ lines.forEach((line, i) => {
7046
+ const lineNum = i + 1;
7047
+
7048
+ // Check for import statements
7049
+ if (line.includes('import') && line.includes(entityName)) {
7050
+ match.importLine = line.trim();
7051
+ }
7052
+
7053
+ // Check for type/interface/function/const definition
7054
+ const defPatterns = [
7055
+ `type ${entityName}`,
7056
+ `interface ${entityName}`,
7057
+ `function ${entityName}`,
7058
+ `const ${entityName}`,
7059
+ `class ${entityName}`,
7060
+ `export type ${entityName}`,
7061
+ `export interface ${entityName}`,
7062
+ `export function ${entityName}`,
7063
+ `export const ${entityName}`,
7064
+ `export class ${entityName}`,
7065
+ ];
7066
+
7067
+ if (defPatterns.some(p => line.includes(p))) {
7068
+ match.isDefinition = true;
7069
+ definitionFile = relativePath;
7070
+ }
7071
+
7072
+ // Check for usage (not just import or definition)
7073
+ if (line.includes(entityName) && !line.includes('import ')) {
7074
+ match.usageLines.push(line.trim().substring(0, 100));
7075
+ match.lineNumbers.push(lineNum);
7076
+ }
7077
+ });
7078
+
7079
+ // Only add if there are usages (not just definition)
7080
+ if (match.usageLines.length > 0) {
7081
+ matches.push(match);
7082
+ }
7083
+ } catch {
7084
+ // Skip files that can't be read
7085
+ }
7086
+ }
7087
+ }
7088
+ };
7089
+
7090
+ // Search all directories
7091
+ for (const dir of searchDirs) {
7092
+ searchDir(path.join(cwd, dir));
7093
+ }
7094
+
7095
+ // Sort: definition first, then by number of usages
7096
+ matches.sort((a, b) => {
7097
+ if (a.isDefinition && !b.isDefinition) return -1;
7098
+ if (!a.isDefinition && b.isDefinition) return 1;
7099
+ return b.usageLines.length - a.usageLines.length;
7100
+ });
7101
+
7102
+ // Generate report
7103
+ if (matches.length === 0) {
7104
+ response += `## ✅ No usages found\n\n`;
7105
+ response += `The entity \`${entityName}\` was not found in the codebase, or has no usages.\n`;
7106
+ response += `This change is safe to make without ripple effects.\n`;
7107
+ } else {
7108
+ // Summary
7109
+ response += `## 📊 Impact Summary\n\n`;
7110
+ response += `| Metric | Count |\n`;
7111
+ response += `|--------|-------|\n`;
7112
+ response += `| Files affected | ${matches.length} |\n`;
7113
+ response += `| Total usages | ${matches.reduce((sum, m) => sum + m.usageLines.length, 0)} |\n`;
7114
+ if (definitionFile) {
7115
+ response += `| Definition | \`${definitionFile}\` |\n`;
7116
+ }
7117
+ response += `\n`;
7118
+
7119
+ // Categorize by impact level
7120
+ const highImpact = matches.filter(m => m.usageLines.length >= 5);
7121
+ const mediumImpact = matches.filter(m => m.usageLines.length >= 2 && m.usageLines.length < 5);
7122
+ const lowImpact = matches.filter(m => m.usageLines.length === 1);
7123
+
7124
+ if (highImpact.length > 0) {
7125
+ response += `## 🔴 High Impact (5+ usages)\n\n`;
7126
+ for (const m of highImpact) {
7127
+ response += `### \`${m.file}\`${m.isDefinition ? ' (DEFINITION)' : ''}\n`;
7128
+ if (m.importLine) response += `Import: \`${m.importLine}\`\n`;
7129
+ response += `Usages (${m.usageLines.length}):\n`;
7130
+ for (let i = 0; i < Math.min(5, m.usageLines.length); i++) {
7131
+ response += `- Line ${m.lineNumbers[i]}: \`${m.usageLines[i].substring(0, 80)}${m.usageLines[i].length > 80 ? '...' : ''}\`\n`;
7132
+ }
7133
+ if (m.usageLines.length > 5) {
7134
+ response += `- ... and ${m.usageLines.length - 5} more\n`;
7135
+ }
7136
+ response += `\n`;
7137
+ }
7138
+ }
7139
+
7140
+ if (mediumImpact.length > 0) {
7141
+ response += `## 🟡 Medium Impact (2-4 usages)\n\n`;
7142
+ for (const m of mediumImpact) {
7143
+ response += `- \`${m.file}\` (${m.usageLines.length} usages)\n`;
7144
+ }
7145
+ response += `\n`;
7146
+ }
7147
+
7148
+ if (lowImpact.length > 0) {
7149
+ response += `## 🟢 Low Impact (1 usage)\n\n`;
7150
+ for (const m of lowImpact) {
7151
+ response += `- \`${m.file}\`\n`;
7152
+ }
7153
+ response += `\n`;
7154
+ }
7155
+
7156
+ // Recommendations
7157
+ response += `## 💡 Recommendations\n\n`;
7158
+
7159
+ if (changeType === 'added_field') {
7160
+ response += `**Adding a field is usually safe.** Optional fields won't break existing code.\n`;
7161
+ response += `If the field is required, update these files to provide the new field.\n`;
7162
+ } else if (changeType === 'removed_field') {
7163
+ response += `**Removing a field is breaking.** Check each file above to remove usages of the deleted field.\n`;
7164
+ response += `Run \`npx tsc --noEmit\` after making changes to find remaining issues.\n`;
7165
+ } else if (changeType === 'renamed') {
7166
+ response += `**Renaming is breaking.** Update all files above to use the new name.\n`;
7167
+ response += `Consider using IDE "Rename Symbol" for safer refactoring.\n`;
7168
+ } else if (changeType === 'type_changed') {
7169
+ response += `**Type changes may break.** Review each usage to ensure compatibility.\n`;
7170
+ response += `Run \`npx tsc --noEmit\` to find type errors.\n`;
7171
+ } else if (changeType === 'signature_changed') {
7172
+ response += `**Signature changes are breaking.** Update all call sites with new parameters.\n`;
7173
+ } else {
7174
+ response += `1. Review the high-impact files first\n`;
7175
+ response += `2. Run \`npx tsc --noEmit\` after changes to catch type errors\n`;
7176
+ response += `3. Run tests to verify functionality\n`;
7177
+ }
7178
+
7179
+ response += `\n---\n`;
7180
+ response += `*Run this check again after making changes to verify all ripples are addressed.*\n`;
7181
+ }
7182
+
7183
+ return { content: [{ type: 'text' as const, text: response }] };
7184
+ }
7185
+
6129
7186
  async run(): Promise<void> {
6130
7187
  const transport = new StdioServerTransport();
6131
7188
  await this.server.connect(transport);