Core Concepts

Customization & Extensibility

Learn how to customize and extend AI Developer Assistant for your specific needs

Customization & Extensibility

AI Developer Assistant is built with extensibility in mind, following Hexagonal Architecture principles. This guide covers how to customize the tool for your specific needs, create custom adapters, and extend functionality.

Architecture Overview

AI Developer Assistant uses a clean, modular architecture that separates business logic from external dependencies:

┌─────────────────────┐
│   CLI / IDE / API   │  ← Driving Adapters
│  (Inbound Ports)    │
└─────────┬───────────┘
          │
┌─────────▼───────────┐
│  Application Core   │  ← Domain Entities + Use Cases
│ (Domain Layer)      │
└─────────┬───────────┘
          │
┌─────────▼───────────┐
│ Infrastructure      │  ← Git, LLM, GitHub, Output
│   Adapters          │    (Outbound Adapters)
└─────────────────────┘

Custom Adapters

Creating Custom LLM Providers

You can create custom LLM providers by implementing the LLMProvider interface:

// src/adapters/outbound/LLM/CustomLLMProvider.ts
import { LLMProvider, LLMMessage, LLMResponse } from '../../../domain/ports/LLMPort';

export class CustomLLMProvider implements LLMProvider {
  constructor(private config: CustomLLMConfig) {}

  async generateResponse(messages: LLMMessage[]): Promise<LLMResponse> {
    // Your custom implementation
    const response = await this.callCustomAPI(messages);
    
    return {
      content: response.text,
      usage: {
        promptTokens: response.promptTokens,
        completionTokens: response.completionTokens,
        totalTokens: response.totalTokens
      },
      model: this.config.model,
      provider: 'custom'
    };
  }

  private async callCustomAPI(messages: LLMMessage[]): Promise<any> {
    // Implementation details
  }
}

Registering Custom Providers

// src/adapters/outbound/LLM/LLMAdapter.ts
import { CustomLLMProvider } from './CustomLLMProvider';

export class LLMAdapter {
  private providers: Map<string, LLMProvider> = new Map();

  registerProvider(name: string, provider: LLMProvider): void {
    this.providers.set(name, provider);
  }

  getProvider(name: string): LLMProvider {
    const provider = this.providers.get(name);
    if (!provider) {
      throw new Error(`Provider ${name} not found`);
    }
    return provider;
  }
}

// Usage
const llmAdapter = new LLMAdapter();
llmAdapter.registerProvider('custom', new CustomLLMProvider(config));

Creating Custom Output Formats

Implement custom output formats by extending the OutputPort:

// src/adapters/outbound/Output/CustomOutputAdapter.ts
import { OutputPort, ReviewReport } from '../../../domain/ports/OutputPort';

export class CustomOutputAdapter implements OutputPort {
  constructor(private config: CustomOutputConfig) {}

  async displayReviewReport(report: ReviewReport): Promise<void> {
    const formattedOutput = this.formatReport(report);
    console.log(formattedOutput);
  }

  private formatReport(report: ReviewReport): string {
    // Your custom formatting logic
    return `
      🎯 CUSTOM REVIEW REPORT
      ======================
      
      Files: ${report.summary.filesReviewed}
      Issues: ${report.summary.issuesFound}
      
      ${report.issues.map(issue => 
        `📍 ${issue.file}:${issue.line} - ${issue.message}`
      ).join('\n')}
    `;
  }
}

Creating Custom Git Adapters

For specialized Git operations:

// src/adapters/outbound/Git/CustomGitAdapter.ts
import { GitAdapter, Diff } from '../../../domain/ports/GitPort';

export class CustomGitAdapter implements GitAdapter {
  constructor(private config: CustomGitConfig) {}

  async getDiff(base: string, head: string): Promise<Diff> {
    // Your custom Git implementation
    const diffOutput = await this.executeCustomGitCommand(base, head);
    
    return {
      files: this.parseDiffOutput(diffOutput),
      baseCommit: base,
      headCommit: head,
      timestamp: new Date()
    };
  }

  private async executeCustomGitCommand(base: string, head: string): Promise<string> {
    // Implementation details
  }

  private parseDiffOutput(output: string): any[] {
    // Parse custom diff format
  }
}

Plugin System

Creating Plugins

Create plugins to extend functionality:

// plugins/CustomAnalysisPlugin.ts
import { Plugin, AnalysisContext, AnalysisResult } from '../types/Plugin';

export class CustomAnalysisPlugin implements Plugin {
  name = 'custom-analysis';
  version = '1.0.0';

  async analyze(context: AnalysisContext): Promise<AnalysisResult> {
    // Your custom analysis logic
    const issues = await this.performCustomAnalysis(context.files);
    
    return {
      plugin: this.name,
      issues: issues,
      metadata: {
        analysisType: 'custom',
        timestamp: new Date()
      }
    };
  }

  private async performCustomAnalysis(files: string[]): Promise<any[]> {
    // Implementation details
  }
}

Plugin Registry

// src/core/PluginRegistry.ts
export class PluginRegistry {
  private plugins: Map<string, Plugin> = new Map();

  register(plugin: Plugin): void {
    this.plugins.set(plugin.name, plugin);
  }

  getPlugin(name: string): Plugin | undefined {
    return this.plugins.get(name);
  }

  getAllPlugins(): Plugin[] {
    return Array.from(this.plugins.values());
  }

  async runAllPlugins(context: AnalysisContext): Promise<AnalysisResult[]> {
    const results: AnalysisResult[] = [];
    
    for (const plugin of this.plugins.values()) {
      try {
        const result = await plugin.analyze(context);
        results.push(result);
      } catch (error) {
        console.error(`Plugin ${plugin.name} failed:`, error);
      }
    }
    
    return results;
  }
}

Configuration Extensions

Custom Configuration Options

Extend the configuration system:

// src/config/CustomConfig.ts
export interface CustomConfig {
  customProvider: {
    enabled: boolean;
    apiKey: string;
    baseUrl: string;
    customOptions: {
      timeout: number;
      retries: number;
      customHeaders: Record<string, string>;
    };
  };
  
  customAnalysis: {
    enabled: boolean;
    rules: CustomRule[];
    severity: string[];
  };
}

export interface CustomRule {
  name: string;
  pattern: string;
  severity: 'low' | 'medium' | 'high' | 'critical';
  message: string;
  suggestion: string;
}

Configuration Validation

// src/config/ConfigValidator.ts
export class ConfigValidator {
  validateCustomConfig(config: CustomConfig): ValidationResult {
    const errors: string[] = [];
    
    if (config.customProvider.enabled) {
      if (!config.customProvider.apiKey) {
        errors.push('Custom provider API key is required when enabled');
      }
      
      if (!config.customProvider.baseUrl) {
        errors.push('Custom provider base URL is required when enabled');
      }
    }
    
    return {
      valid: errors.length === 0,
      errors: errors
    };
  }
}

Custom Commands

Creating Custom Commands

Add new commands to the CLI:

// src/adapters/inbound/CLI/Commands/CustomCommand.ts
import { Command } from 'commander';
import { UseCase } from '../../../domain/usecases/UseCase';

export class CustomCommand {
  constructor(private useCase: UseCase) {}

  register(program: Command): void {
    program
      .command('custom')
      .description('Run custom analysis')
      .option('-f, --files <patterns>', 'File patterns to analyze')
      .option('-o, --output <format>', 'Output format')
      .action(async (options) => {
        try {
          const result = await this.useCase.execute(options);
          console.log(result);
        } catch (error) {
          console.error('Custom command failed:', error);
          process.exit(1);
        }
      });
  }
}

Custom Use Cases

Implement custom business logic:

// src/domain/usecases/CustomAnalysisUseCase.ts
export class CustomAnalysisUseCase {
  constructor(
    private gitAdapter: GitAdapter,
    private llmAdapter: LLMAdapter,
    private outputAdapter: OutputAdapter,
    private pluginRegistry: PluginRegistry
  ) {}

  async execute(options: CustomAnalysisOptions): Promise<CustomAnalysisResult> {
    // 1. Get code changes
    const diff = await this.gitAdapter.getDiff(options.base, options.head);
    
    // 2. Run custom analysis
    const analysisResults = await this.runCustomAnalysis(diff.files);
    
    // 3. Run plugins
    const pluginResults = await this.pluginRegistry.runAllPlugins({
      files: diff.files,
      diff: diff
    });
    
    // 4. Combine results
    const combinedResults = this.combineResults(analysisResults, pluginResults);
    
    // 5. Output results
    await this.outputAdapter.displayCustomResults(combinedResults);
    
    return combinedResults;
  }

  private async runCustomAnalysis(files: string[]): Promise<any[]> {
    // Custom analysis logic
  }

  private combineResults(analysis: any[], plugins: any[]): CustomAnalysisResult {
    // Combine and deduplicate results
  }
}

Integration Examples

Slack Integration

// integrations/SlackIntegration.ts
export class SlackIntegration {
  constructor(private webhookUrl: string) {}

  async sendReviewReport(report: ReviewReport): Promise<void> {
    const slackMessage = this.formatForSlack(report);
    
    await fetch(this.webhookUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        text: 'Code Review Report',
        blocks: slackMessage
      })
    });
  }

  private formatForSlack(report: ReviewReport): any[] {
    return [
      {
        type: 'header',
        text: {
          type: 'plain_text',
          text: '🔍 Code Review Report'
        }
      },
      {
        type: 'section',
        fields: [
          {
            type: 'mrkdwn',
            text: `*Files Reviewed:* ${report.summary.filesReviewed}`
          },
          {
            type: 'mrkdwn',
            text: `*Issues Found:* ${report.summary.issuesFound}`
          }
        ]
      }
    ];
  }
}

JIRA Integration

// integrations/JiraIntegration.ts
export class JiraIntegration {
  constructor(private jiraConfig: JiraConfig) {}

  async createIssuesFromReport(report: ReviewReport): Promise<string[]> {
    const issueKeys: string[] = [];
    
    for (const issue of report.issues) {
      if (issue.severity === 'high' || issue.severity === 'critical') {
        const issueKey = await this.createJiraIssue(issue);
        issueKeys.push(issueKey);
      }
    }
    
    return issueKeys;
  }

  private async createJiraIssue(issue: any): Promise<string> {
    const jiraIssue = {
      fields: {
        project: { key: this.jiraConfig.projectKey },
        summary: `Code Review Issue: ${issue.message}`,
        description: this.formatJiraDescription(issue),
        issuetype: { name: 'Bug' },
        priority: { name: this.mapSeverityToPriority(issue.severity) }
      }
    };

    const response = await fetch(`${this.jiraConfig.baseUrl}/rest/api/2/issue`, {
      method: 'POST',
      headers: {
        'Authorization': `Basic ${this.jiraConfig.auth}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(jiraIssue)
    });

    const result = await response.json();
    return result.key;
  }

  private formatJiraDescription(issue: any): string {
    return `
      *File:* ${issue.file}
      *Line:* ${issue.line}
      *Severity:* ${issue.severity}
      
      *Issue:* ${issue.message}
      
      *Suggestion:* ${issue.suggestion}
      
      *Code:*
      {code}
      ${issue.code}
      {code}
    `;
  }

  private mapSeverityToPriority(severity: string): string {
    switch (severity) {
      case 'critical': return 'Highest';
      case 'high': return 'High';
      case 'medium': return 'Medium';
      case 'low': return 'Low';
      default: return 'Medium';
    }
  }
}

Best Practices

1. Follow Hexagonal Architecture

  • Keep domain logic pure - no external dependencies
  • Use ports and adapters - define interfaces for external systems
  • Dependency inversion - depend on abstractions, not concretions

2. Error Handling

export class CustomAdapter {
  async performOperation(): Promise<Result> {
    try {
      // Operation logic
      return { success: true, data: result };
    } catch (error) {
      return { 
        success: false, 
        error: error.message,
        retryable: this.isRetryableError(error)
      };
    }
  }

  private isRetryableError(error: Error): boolean {
    // Determine if error is retryable
    return error.name === 'NetworkError' || error.name === 'TimeoutError';
  }
}

3. Configuration Management

export class ConfigManager {
  private config: Config;
  private validators: ConfigValidator[] = [];

  addValidator(validator: ConfigValidator): void {
    this.validators.push(validator);
  }

  validateConfig(): ValidationResult {
    const errors: string[] = [];
    
    for (const validator of this.validators) {
      const result = validator.validate(this.config);
      if (!result.valid) {
        errors.push(...result.errors);
      }
    }
    
    return {
      valid: errors.length === 0,
      errors: errors
    };
  }
}

4. Testing Custom Extensions

// tests/CustomAdapter.test.ts
describe('CustomAdapter', () => {
  let adapter: CustomAdapter;
  let mockConfig: CustomConfig;

  beforeEach(() => {
    mockConfig = {
      // Mock configuration
    };
    adapter = new CustomAdapter(mockConfig);
  });

  it('should perform operation successfully', async () => {
    const result = await adapter.performOperation();
    expect(result.success).toBe(true);
  });

  it('should handle errors gracefully', async () => {
    // Mock error scenario
    const result = await adapter.performOperation();
    expect(result.success).toBe(false);
    expect(result.error).toBeDefined();
  });
});

Deployment

Packaging Custom Extensions

{
  "name": "ai-dev-custom-extension",
  "version": "1.0.0",
  "description": "Custom extension for AI Developer Assistant",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist/**/*"
  ],
  "peerDependencies": {
    "kg6-codex": "^1.0.0"
  },
  "scripts": {
    "build": "tsc",
    "test": "jest"
  }
}

Installation

# Install custom extension
npm install ai-dev-custom-extension

# Register in configuration
# ai-dev.config.local.yaml
extensions:
  - name: "custom-extension"
    package: "ai-dev-custom-extension"
    config:
      apiKey: "${CUSTOM_API_KEY}"
      enabled: true
Start with simple customizations and gradually build more complex extensions. The modular architecture makes it easy to add new functionality without breaking existing features.