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.