Third-Party Plugin Development Guide
Third-Party Plugin Development Guide
Section titled “Third-Party Plugin Development Guide”This guide explains how to develop and build third-party plugins (TypeScript, dist/, examples). How to install and wire a plugin in production (npm, private registry, /app/plugins mounts) is in extending-with-plugins.md — read that first if you only need deployment steps.
Table of Contents
Section titled “Table of Contents”- Overview
- Plugin Package Structure
- Plugin Implementation
- Building Your Plugin
- Distributing Your Plugin
- Installing Third-Party Plugins
Overview
Section titled “Overview”Third-party plugins extend agent-detective’s capabilities. They can be published to npm (or a private registry), vendored as a path on disk, or added to a fork under packages/* (see extending-with-plugins.md for how the runtime resolves each case).
Plugin Package Structure
Section titled “Plugin Package Structure”my-plugin/├── package.json # Package metadata├── tsconfig.json # TypeScript config├── tsconfig.build.json # Build-specific config├── src/│ └── index.ts # Plugin source├── dist/│ ├── index.js # Compiled JavaScript│ └── index.d.ts # TypeScript declarations└── README.md # Installation instructionspackage.json
Section titled “package.json”{ "name": "@myorg/agent-detective-my-plugin", "version": "1.0.0", "description": "My custom plugin for agent-detective", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "scripts": { "build": "tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.json --watch" }, "keywords": ["agent-detective", "plugin"], "peerDependencies": { "@agent-detective/types": "^1.0.0" }, "devDependencies": { "@agent-detective/types": "^1.0.0", "typescript": "^5.7.0" }}tsconfig.build.json
Section titled “tsconfig.build.json”{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"]}tsconfig.json
Section titled “tsconfig.json”{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "declaration": true, "outDir": "./dist", "experimentalDecorators": true, "emitDecoratorMetadata": true }, "include": ["src/**/*"]}Note:
experimentalDecoratorsandemitDecoratorMetadataare required for OpenAPI decorators to work properly.
Monorepo-only: use "@agent-detective/types": "workspace:*"; published plugins should use a semver range and depend on the npm release of @agent-detective/types.
Plugin Implementation
Section titled “Plugin Implementation”Basic Plugin Structure
Section titled “Basic Plugin Structure”import type { Plugin, PluginContext } from '@agent-detective/types';
const myPlugin: Plugin = { name: '@myorg/agent-detective-my-plugin', version: '1.0.0', schemaVersion: '1.0',
schema: { type: 'object', properties: { enabled: { type: 'boolean', default: true }, webhookPath: { type: 'string', default: '/plugins/agent-detective-my-plugin/webhook' }, someOption: { type: 'string', default: 'default' }, }, required: [] },
register(app, context: PluginContext) { const { config, agentRunner, logger } = context;
if (!config.enabled) { logger.info('Plugin is disabled'); return; }
const webhookPath = config.webhookPath as string;
app.post(webhookPath, async (req, res) => { // Handle webhook... });
logger.info('My plugin registered successfully'); }};
export default myPlugin;PluginContext Members Available
Section titled “PluginContext Members Available”| Member | Type | Description |
|---|---|---|
agentRunner | AgentRunner | Execute AI agent prompts |
registerService<T>(name, service) | function | Register a service for other plugins to consume |
getService<T>(name) | function | Get a registered service by name with type safety |
registerCapability(name) | function | Register a capability provided by this plugin |
hasCapability(name) | function | Check if a capability is registered |
config | object | Validated plugin configuration |
logger | Logger | Structured logging |
enqueue | function | Queue tasks for sequential execution |
Building Your Plugin
Section titled “Building Your Plugin”1. Create the plugin project
Section titled “1. Create the plugin project”mkdir my-plugin && cd my-pluginpnpm init2. Install dependencies
Section titled “2. Install dependencies”pnpm add @agent-detective/typespnpm add -D typescript tsx3. Build
Section titled “3. Build”pnpm run build4. Output
Section titled “4. Output”After building, dist/ contains:
dist/├── index.js # ES module bundle└── index.d.ts # Type declarationsDistributing Your Plugin
Section titled “Distributing Your Plugin”Option A: npm Registry (Recommended for Public Plugins)
Section titled “Option A: npm Registry (Recommended for Public Plugins)”# Buildpnpm run build
# Publish to npmnpm publish --access publicUsers can then install it via:
npm install @myorg/agent-detective-my-pluginOption B: GitHub Release
Section titled “Option B: GitHub Release”# Create a release on GitHubgit tag v1.0.0git push origin v1.0.0
# Users download and extract the dist/ folderOption C: Private Distribution
Section titled “Option C: Private Distribution”Distribute the dist/ folder directly within your organization:
# Copy dist/ to a shared locationscp -r dist/ user@server:/path/to/plugins/my-plugin/Installing Third-Party Plugins (runtime)
Section titled “Installing Third-Party Plugins (runtime)”See extending-with-plugins.md for:
packagespecifiers (npm, path, monorepopackages/*)dependsOnand load order- private registry /
.npmrc - Docker
plugins/volume and/app/plugins/...config
The sections above (Distributing) describe how to publish or copy artifacts; the extending guide ties that to a running server.
Example: Complete Jira-Style Plugin
Section titled “Example: Complete Jira-Style Plugin”Project Structure
Section titled “Project Structure”my-jira-plugin/├── package.json├── tsconfig.json├── tsconfig.build.json├── src/│ └── index.ts├── dist/│ ├── index.js│ └── index.d.ts└── README.mdpackage.json
Section titled “package.json”{ "name": "@myorg/agent-detective-jira-plus", "version": "1.0.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "scripts": { "build": "tsc -p tsconfig.build.json" }, "peerDependencies": { "@agent-detective/types": "^1.0.0" }}src/index.ts
Section titled “src/index.ts”import type { Plugin, PluginContext, TaskEvent } from '@agent-detective/types';
const jiraPlusPlugin: Plugin = { name: '@myorg/agent-detective-jira-plus', version: '1.0.0', schemaVersion: '1.0',
schema: { type: 'object', properties: { enabled: { type: 'boolean', default: true }, webhookPath: { type: 'string', default: '/plugins/agent-detective-my-plugin/webhook' }, baseUrl: { type: 'string', default: '' }, email: { type: 'string', default: '' }, apiToken: { type: 'string', default: '' }, priorityMapping: { type: 'object', default: { 'Critical': 1, 'Major': 2, 'Minor': 3 } } }, required: ['webhookPath'] },
register(app, context: PluginContext) { const { config, agentRunner, logger } = context;
if (!config.enabled) { logger.info('Jira Plus plugin is disabled'); return; }
app.post(config.webhookPath as string, async (req, res) => { const taskEvent = normalizePayload(req.body);
// Discovery logic... const repo = localRepos?.getRepo(taskEvent.metadata.repoName as string); if (repo) { taskEvent.context.repoPath = repo.path; }
logger.info(`Processing: ${taskEvent.id}`);
// Process with agent... res.json({ status: 'queued', taskId: taskEvent.id }); });
logger.info('Jira Plus plugin registered'); }};
export default jiraPlusPlugin;API Documentation (OpenAPI)
Section titled “API Documentation (OpenAPI)”Plugins can provide OpenAPI metadata for auto-generated API documentation at /docs using decorator-based approach.
Adding OpenAPI Metadata with Decorators
Section titled “Adding OpenAPI Metadata with Decorators”First, add @agent-detective/core as a dependency:
{ "dependencies": { "@agent-detective/core": "workspace:*" }}Then create a controller class with decorators:
import type { Request, Response } from 'express';import { Controller, Get, Post, Summary, Description, Tags, Response as OpenApiResponse, RequestBody,} from '@agent-detective/core';
const PLUGIN_TAG = '@myorg/my-plugin';
@Controller('/api', { tags: [PLUGIN_TAG], description: 'My plugin endpoints' })export class MyController { private myService?: MyService;
constructor(myService?: MyService) { this.myService = myService; }
setMyService(service: MyService): void { this.myService = service; }
@Get('/status') @Summary('Get status') @Description('Returns current plugin status') @Tags(PLUGIN_TAG) @OpenApiResponse(200, 'Success', { example: { status: 'ok', plugin: 'my-plugin' } }) getStatus(_req: Request, res: Response) { res.json({ status: 'ok', plugin: 'my-plugin' }); }
@Post('/webhook') @Summary('Handle webhook') @Description('Receives events from external systems') @Tags(PLUGIN_TAG) @RequestBody({ description: 'Webhook payload', required: true, example: { event: 'issue_created', data: { id: '123' } }, schema: { type: 'object', properties: { event: { type: 'string' }, data: { type: 'object' } }, required: ['event'] } }) @OpenApiResponse(200, 'Success', { example: { status: 'received', taskId: 'abc123' } }) @OpenApiResponse(400, 'Bad Request') handleWebhook(req: Request, res: Response) { // Handle webhook res.json({ status: 'received' }); }}Registering the Controller
Section titled “Registering the Controller”In your plugin’s register function:
import type { Plugin, PluginContext } from '@agent-detective/types';import { registerController } from '@agent-detective/core';import { MyController } from './my-controller.js';
const myPlugin: Plugin = { name: '@myorg/my-plugin', version: '1.0.0', schemaVersion: '1.0',
schema: { type: 'object', properties: { enabled: { type: 'boolean', default: true } } },
register(app, context) { const { logger } = context;
const myService = new MyService(); const controller = new MyController(myService); controller.setMyService(myService);
registerController(app, controller);
logger.info('My plugin registered'); }};
export default myPlugin;Available Decorators
Section titled “Available Decorators”| Decorator | Type | Description |
|---|---|---|
@Controller(prefix, options?) | Class | Marks a class as a controller with base path |
@Get(path) | Method | Marks a method as GET endpoint |
@Post(path) | Method | Marks a method as POST endpoint |
@Put(path) | Method | Marks a method as PUT endpoint |
@Delete(path) | Method | Marks a method as DELETE endpoint |
@Patch(path) | Method | Marks a method as PATCH endpoint |
@Summary(text) | Method | Adds summary to OpenAPI spec |
@Description(text) | Method | Adds description to OpenAPI spec |
@Tags(...tags) | Method | Adds tags for grouping in docs |
@RequestBody(options?) | Method | Documents request body |
@Response(status, desc, options?) | Method | Documents response |
@OperationId(id) | Method | Sets operation ID |
@Deprecated() | Method | Marks endpoint as deprecated |
@Security(scheme) | Method | Adds security scheme |
Response Decorator Options
Section titled “Response Decorator Options”@OpenApiResponse(200, 'Success', { contentType: 'application/json', example: { id: 1, name: 'Example' }, schema: { type: 'object', properties: { id: { type: 'number' }, name: { type: 'string' } } }})RequestBody Decorator Options
Section titled “RequestBody Decorator Options”@RequestBody({ description: 'User data', required: true, contentType: 'application/json', example: { name: 'John', email: 'john@example.com' }, schema: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string', format: 'email' } }, required: ['name', 'email'] }})Accessing API Documentation
Section titled “Accessing API Documentation”- Without auth: Visit
/docsdirectly - With auth: Set
X-API-KEYheader or configureDOCS_AUTH_REQUIRED=trueandDOCS_API_KEY
Environment Variables for Docs
Section titled “Environment Variables for Docs”| Variable | Description |
|---|---|
DOCS_AUTH_REQUIRED=true | Require API key to access docs |
DOCS_API_KEY=<key> | The API key to use for authentication |
Or via config:
{ "docsAuthRequired": true, "docsApiKey": "your-secret-key"}Best Practices
Section titled “Best Practices”- Follow semver - Use meaningful version numbers
- Document configuration - Clear schema with defaults
- Handle errors gracefully - Don’t crash the host app
- Use logging - Help users debug issues
- Support hot reload - Design for development ease
- Test thoroughly - Mock external dependencies
Troubleshooting
Section titled “Troubleshooting”Plugin Not Loading
Section titled “Plugin Not Loading”- Check the plugin directory structure is correct
- Verify
index.jsis in the right place (not indist/) - Ensure
package.jsonnamematches directory name - Check logs for schema validation errors
Type Errors
Section titled “Type Errors”- Ensure
@agent-detective/typesversion is compatible - Run
pnpm run buildto generate.d.tsfiles - Use
import typefor type-only imports
Container Won’t Start
Section titled “Container Won’t Start”- Verify volume mount path is correct
- Ensure plugin files are readable
- Check config syntax in
default.json