Skip to main content

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

TypeConventionExample
Extension classPascalCaseMyExtension
Extension filekebab-casemy-extension.ts
Tool nameskebab-caserun-linter
Command nameskebab-casegenerate-tests
UI element IDskebab-casecreate-jira-ticket

Best Practices

  1. Keep it focused - Each extension should do one thing well
  2. Handle errors gracefully - Catch exceptions and return meaningful error messages
  3. Use TypeScript - Get compile-time checks and better IDE support
  4. Log appropriately - Use context.log() for debugging, not console.log()
  5. Clean up resources - Implement onUnload() to release resources
  6. Validate inputs - Use Zod schemas for tool parameters
  7. Document your extension - Include description in metadata