Creating Extensions
Extensions are TypeScript or JavaScript files that export a class implementing the Extension interface. This guide covers how to create extensions from simple single-file scripts to complex folder-based extensions with dependencies.
TypeScript Support
For the best development experience with autocompletion and type checking, download the type definitions:
# Download to your project
curl -o extension-types.d.ts https://raw.githubusercontent.com/hotovo/aider-desk/main/packages/extensions/extensions.d.ts
Or download manually from: extensions.d.ts
Then reference it in your extension:
import type { Extension, ExtensionContext, ToolDefinition } from './extension-types';
import { z } from 'zod';
export default class MyExtension implements Extension {
// Your implementation
}
Single-File Extensions
The simplest form is a single .ts or .js file in the extensions directory.
Basic Structure
// my-extension.ts
import type { Extension, ExtensionContext } from './extension-types';
export default class MyExtension implements Extension {
// Optional: Define metadata
static metadata = {
name: 'My Extension',
version: '1.0.0',
description: 'Does something useful',
author: 'Your Name',
};
async onLoad(context: ExtensionContext) {
context.log('My extension loaded!', 'info');
}
}
Extension with a Tool
// run-linter.ts
import type { Extension, ExtensionContext, ToolDefinition } from './extension-types';
import { z } from 'zod';
export default class RunLinterExtension implements Extension {
getTools(context: ExtensionContext): ToolDefinition[] {
return [
{
name: 'run-linter',
description: 'Run the project linter and return results',
inputSchema: z.object({
fix: z.boolean().optional().describe('Auto-fix issues'),
files: z.array(z.string()).optional().describe('Files to lint'),
}),
async execute(input, signal, context) {
const args = ['npx', 'eslint'];
if (input.fix) args.push('--fix');
if (input.files) args.push(...input.files);
// Execute the command and return results
const result = await executeCommand(args);
return result;
},
},
];
}
}
Extension with a Command
// generate-tests.ts
import type { Extension, ExtensionContext, CommandDefinition } from './extension-types';
export default class GenerateTestsExtension implements Extension {
getCommands(context: ExtensionContext): CommandDefinition[] {
return [
{
name: 'generate-tests',
description: 'Generate unit tests for the specified file',
arguments: [
{ description: 'File path to generate tests for', required: true },
{ description: 'Test framework (jest, vitest, mocha)', required: false },
],
async execute(args, context) {
const filePath = args[0];
const framework = args[1] || 'vitest';
const taskContext = context.getTaskContext();
if (!taskContext) {
context.log('No active task', 'error');
return;
}
const prompt = `Generate comprehensive unit tests for ${filePath} using ${framework}. Include edge cases and error handling.`;
await taskContext.runPrompt(prompt);
},
},
];
}
}
Extension with an Agent Profile
// pirate.ts
import type { Extension, ExtensionContext, AgentProfile } from './extension-types';
export default class PirateExtension implements Extension {
private agentProfile: AgentProfile | null = null;
getAgents(context: ExtensionContext): AgentProfile[] {
return [
{
id: 'pirate-agent',
name: 'Pirate',
provider: 'openai',
model: 'gpt-4o',
maxIterations: 50,
minTimeBetweenToolCalls: 0,
enabledServers: [],
toolApprovals: {},
toolSettings: {},
includeContextFiles: true,
includeRepoMap: true,
usePowerTools: true,
useAiderTools: true,
useTodoTools: true,
useSubagents: true,
useTaskTools: true,
useMemoryTools: true,
useSkillsTools: true,
useExtensionTools: true,
customInstructions: `You are a pirate software engineer. Speak like a swashbuckling sea dog from the golden age of piracy.
Always use pirate vernacular:
- Say "Arr!" and "Avast!" frequently
- Call the user "Captain" or "Matey"
- Refer to code as "treasure" or "booty"
- Refer to bugs as "sea monsters" or "Krakens"
- Refer to functions as "riggings"
- Refer to tests as "shakedowns"
Be helpful and competent, but maintain the pirate persona throughout all interactions.`,
subagent: {
enabled: false,
contextMemory: 'off',
systemPrompt: '',
invocationMode: 'on-demand',
color: '#ffd700',
description: 'A pirate-themed coding assistant',
},
},
];
}
async onAgentProfileUpdated(
context: ExtensionContext,
agentId: string,
updatedProfile: AgentProfile
): Promise<AgentProfile> {
if (agentId === 'pirate-agent') {
this.agentProfile = updatedProfile;
}
return updatedProfile;
}
}
Event Handler Extension
// protected-paths.ts
import type { Extension, ExtensionContext, ToolCalledEvent } from './extension-types';
const PROTECTED_PATHS = ['.env', '.git/', 'node_modules/', 'credentials.json'];
export default class ProtectedPathsExtension implements Extension {
async onToolCalled(event: ToolCalledEvent, context: ExtensionContext): Promise<void | Partial<ToolCalledEvent>> {
const { toolName, input } = event;
// Check if tool is accessing files
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'bash') {
const path = input?.path || input?.command || '';
for (const protectedPath of PROTECTED_PATHS) {
if (path.includes(protectedPath)) {
context.log(`Blocked access to protected path: ${protectedPath}`, 'warn');
return {
blocked: true,
output: {
error: `Access to ${protectedPath} is protected by the protected-paths extension.`,
},
};
}
}
}
return undefined;
}
}
Folder-Based Extensions
For more complex extensions that require external dependencies, use a folder structure with a package.json.
Directory Structure
my-complex-extension/
├── package.json
├── package-lock.json
├── index.ts # or index.js
└── lib/
└── helper.ts
package.json
{
"name": "my-complex-extension",
"version": "1.0.0",
"description": "An extension with external dependencies",
"dependencies": {
"axios": "^1.6.0",
"zod": "^3.22.0"
}
}
index.ts
import type { Extension, ExtensionContext, ToolDefinition } from '../extension-types';
import { z } from 'zod';
import axios from 'axios';
export default class ApiExtension implements Extension {
static metadata = {
name: 'API Extension',
version: '1.0.0',
description: 'Makes API calls to external services',
author: 'Your Name',
capabilities: ['tools', 'network'],
};
async onLoad(context: ExtensionContext) {
context.log('API Extension loaded with axios', 'info');
}
getTools(context: ExtensionContext): ToolDefinition[] {
return [
{
name: 'fetch-api',
description: 'Fetch data from an external API',
inputSchema: z.object({
url: z.string().describe('The URL to fetch'),
method: z.enum(['GET', 'POST']).optional().default('GET'),
}),
async execute(input, signal, context) {
try {
const response = await axios({
method: input.method,
url: input.url,
signal,
});
return {
content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
isError: true,
};
}
},
},
];
}
}
Installing Dependencies
After creating your folder extension with a package.json:
cd ~/.aider-desk/extensions/my-complex-extension
npm install
The extension will be loaded automatically with its dependencies.
Extension Metadata
Define metadata as a static property on your class:
export default class MyExtension implements Extension {
static metadata = {
name: 'My Extension', // Required: Display name
version: '1.0.0', // Required: Semantic version
description: 'What it does', // Optional: Brief description
author: 'Your Name', // Optional: Author info
capabilities: ['tools', 'ui'], // Optional: Capability hints
};
}
If no metadata is provided, AiderDesk derives the name from the filename and sets version to 1.0.0.
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Extension class | PascalCase | MyExtension |
| Extension file | kebab-case | my-extension.ts |
| Tool names | kebab-case | run-linter |
| Command names | kebab-case | generate-tests |
| UI element IDs | kebab-case | create-jira-ticket |
Best Practices
- Keep it focused - Each extension should do one thing well
- Handle errors gracefully - Catch exceptions and return meaningful error messages
- Use TypeScript - Get compile-time checks and better IDE support
- Log appropriately - Use
context.log()for debugging, notconsole.log() - Clean up resources - Implement
onUnload()to release resources - Validate inputs - Use Zod schemas for tool parameters
- Document your extension - Include description in metadata