Skip to content

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.

  1. Overview
  2. Plugin Package Structure
  3. Plugin Implementation
  4. Building Your Plugin
  5. Distributing Your Plugin
  6. Installing Third-Party Plugins

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).


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 instructions
{
"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"
}
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"outDir": "./dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"]
}

Note: experimentalDecorators and emitDecoratorMetadata are 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.


src/index.ts
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;
MemberTypeDescription
agentRunnerAgentRunnerExecute AI agent prompts
registerService<T>(name, service)functionRegister a service for other plugins to consume
getService<T>(name)functionGet a registered service by name with type safety
registerCapability(name)functionRegister a capability provided by this plugin
hasCapability(name)functionCheck if a capability is registered
configobjectValidated plugin configuration
loggerLoggerStructured logging
enqueuefunctionQueue tasks for sequential execution

Terminal window
mkdir my-plugin && cd my-plugin
pnpm init
Terminal window
pnpm add @agent-detective/types
pnpm add -D typescript tsx
Terminal window
pnpm run build

After building, dist/ contains:

dist/
├── index.js # ES module bundle
└── index.d.ts # Type declarations

Section titled “Option A: npm Registry (Recommended for Public Plugins)”
Terminal window
# Build
pnpm run build
# Publish to npm
npm publish --access public

Users can then install it via:

Terminal window
npm install @myorg/agent-detective-my-plugin
Terminal window
# Create a release on GitHub
git tag v1.0.0
git push origin v1.0.0
# Users download and extract the dist/ folder

Distribute the dist/ folder directly within your organization:

Terminal window
# Copy dist/ to a shared location
scp -r dist/ user@server:/path/to/plugins/my-plugin/

See extending-with-plugins.md for:

  • package specifiers (npm, path, monorepo packages/*)
  • dependsOn and 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.


my-jira-plugin/
├── package.json
├── tsconfig.json
├── tsconfig.build.json
├── src/
│ └── index.ts
├── dist/
│ ├── index.js
│ └── index.d.ts
└── README.md
{
"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"
}
}
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;

Plugins can provide OpenAPI metadata for auto-generated API documentation at /docs using decorator-based approach.

First, add @agent-detective/core as a dependency:

{
"dependencies": {
"@agent-detective/core": "workspace:*"
}
}

Then create a controller class with decorators:

src/my-controller.ts
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' });
}
}

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;
DecoratorTypeDescription
@Controller(prefix, options?)ClassMarks a class as a controller with base path
@Get(path)MethodMarks a method as GET endpoint
@Post(path)MethodMarks a method as POST endpoint
@Put(path)MethodMarks a method as PUT endpoint
@Delete(path)MethodMarks a method as DELETE endpoint
@Patch(path)MethodMarks a method as PATCH endpoint
@Summary(text)MethodAdds summary to OpenAPI spec
@Description(text)MethodAdds description to OpenAPI spec
@Tags(...tags)MethodAdds tags for grouping in docs
@RequestBody(options?)MethodDocuments request body
@Response(status, desc, options?)MethodDocuments response
@OperationId(id)MethodSets operation ID
@Deprecated()MethodMarks endpoint as deprecated
@Security(scheme)MethodAdds security scheme
@OpenApiResponse(200, 'Success', {
contentType: 'application/json',
example: { id: 1, name: 'Example' },
schema: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' }
}
}
})
@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']
}
})
  • Without auth: Visit /docs directly
  • With auth: Set X-API-KEY header or configure DOCS_AUTH_REQUIRED=true and DOCS_API_KEY
VariableDescription
DOCS_AUTH_REQUIRED=trueRequire 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"
}

  1. Follow semver - Use meaningful version numbers
  2. Document configuration - Clear schema with defaults
  3. Handle errors gracefully - Don’t crash the host app
  4. Use logging - Help users debug issues
  5. Support hot reload - Design for development ease
  6. Test thoroughly - Mock external dependencies

  1. Check the plugin directory structure is correct
  2. Verify index.js is in the right place (not in dist/)
  3. Ensure package.json name matches directory name
  4. Check logs for schema validation errors
  1. Ensure @agent-detective/types version is compatible
  2. Run pnpm run build to generate .d.ts files
  3. Use import type for type-only imports
  1. Verify volume mount path is correct
  2. Ensure plugin files are readable
  3. Check config syntax in default.json