ESM Plugin Development

ESM (ECMAScript Module) plugins are JavaScript/TypeScript modules that export capabilities as async functions. They are the easiest plugin type to develop and the most common in the CortexPrism ecosystem.

Project Structure

my-plugin/
├── mod.ts              # Plugin entry point (default export)
├── capabilities/       # Optional: split capabilities into modules
│   ├── search.ts
│   └── format.ts
├── test/               # Tests
├── README.md
└── manifest.json       # Plugin manifest (auto-generated or manual)

Entry Point Conventions

The plugin must have a default export that is either:

  1. A plugin definition object (recommended):
export default {
  name: "my-plugin",
  version: "1.0.0",
  capabilities: { /* ... */ },
};
  1. A factory function (for async initialization):
export default async function createPlugin() {
  const client = await connectToService();
  return {
    name: "service-connector",
    version: "1.0.0",
    capabilities: {
      async query(input: string) {
        return client.query(input);
      },
    },
  };
}

Defining Capabilities

Capabilities are async functions that the agent tool system can invoke. Each capability receives a context object with permissions and utilities.

import { definePlugin, type CapabilityContext } from "@cortexprism/plugin-sdk";

export default definePlugin({
  name: "code-analyzer",
  version: "1.1.0",
  description: "Analyze source code for metrics and issues",

  capabilities: {
    async countLines(
      filePath: string,
      ctx: CapabilityContext
    ): Promise<{ total: number; code: number; comments: number }> {
      // ctx.sandbox.readFile is gated by the Parallax policy
      const content = await ctx.sandbox.readFile(filePath);
      const lines = content.split("\n");
      return {
        total: lines.length,
        code: lines.filter((l) => l.trim() && !l.trim().startsWith("//")).length,
        comments: lines.filter((l) => l.trim().startsWith("//")).length,
      };
    },

    async detectLanguage(code: string): Promise<string> {
      // Pure computation, no sandbox access needed
      if (code.includes("fn ") || code.includes("let mut")) return "rust";
      if (code.includes("def ") || code.includes("import ")) return "python";
      if (code.includes("function ") || code.includes("=>")) return "javascript";
      return "unknown";
    },
  },
});

CapabilityContext

Each capability receives a context object that provides:

PropertyDescription
ctx.sandboxSandboxed file system and network access
ctx.logStructured logger scoped to the plugin
ctx.memoryRead access to the agent's episodic/semantic memory
ctx.configPlugin-specific configuration from the agent config
ctx.abortSignalSignal for timeout/cancellation

SDK Usage

The @cortexprism/plugin-sdk package provides:

import {
  definePlugin,        // Type-safe plugin definition helper
  CapabilityContext,   // Type for the context argument
  z,                   // Zod validation (capability input schemas)
  HttpError,           // Error class for HTTP-like errors
  PermissionError,     // Error for policy denial
} from "@cortexprism/plugin-sdk";

Install with:

npm install @cortexprism/plugin-sdk
# or via JSR
deno add @cortexprism/plugin-sdk

Input Validation

Use Zod schemas for capability input validation:

import { definePlugin, z } from "@cortexprism/plugin-sdk";

const SearchInput = z.object({
  query: z.string().min(1).max(500),
  limit: z.number().int().min(1).max(100).default(10),
  source: z.enum(["web", "news", "academic"]).default("web"),
});

export default definePlugin({
  name: "advanced-search",
  version: "1.0.0",
  capabilities: {
    async search(raw: unknown) {
      const input = SearchInput.parse(raw);
      const results = await searchEngine(input.query, input.source, input.limit);
      return results;
    },
  },
});

Error Handling

Always throw typed errors so the agent can handle them gracefully:

import { PluginError, PermissionError } from "@cortexprism/plugin-sdk";

export default definePlugin({
  name: "file-ops",
  version: "1.0.0",
  capabilities: {
    async readFile(path: string, ctx: CapabilityContext) {
      if (!path.startsWith("/allowed/path/")) {
        throw new PermissionError(`Access denied: ${path}`);
      }
      try {
        return await ctx.sandbox.readFile(path);
      } catch (err) {
        throw new PluginError(`Failed to read file: ${err.message}`, err);
      }
    },
  },
});

Extension Points

Beyond capabilities, ESM plugins can extend CortexPrism through several extension points. Export named exports from your entry point module.

Tools

Export a tools array to register tool definitions with the agent loop:

import type { Tool, PluginContext } from "cortex/plugins";

const weatherTool: Tool = {
  definition: {
    name: "get_weather",
    description: "Get current weather for a city",
    params: [
      { name: "city", type: "string", description: "City name", required: true },
    ],
    capabilities: ["network:fetch"],
  },
  execute: async (args, ctx) => {
    const res = await fetch(`https://api.weather.example/${args.city}`);
    const data = await res.text();
    return { toolName: "get_weather", success: true, output: data, durationMs: 0 };
  },
};

export const tools = [weatherTool];

CLI Commands

Declare commands in the manifest and export a handler:

{
  "cliCommands": [
    {
      "name": "my-command",
      "description": "My custom CLI command",
      "options": [
        { "name": "verbose", "type": "boolean", "description": "Verbose output", "flag": "-v" }
      ]
    }
  ]
}
export async function myCommand(args: Record<string, unknown>) {
  console.log(`Running with args:`, args);
}

LLM Providers

Export a providers map to add new AI provider integrations:

export const providers = {
  "my-provider": (config: Record<string, unknown>) => ({
    name: "my-provider",
    defaultModel: "my-model",
    async complete(opts) { /* ... */ },
    async *stream(opts) { /* ... */ },
  }),
};

UI Panels

Define panels in the manifest. Each panel has an HTML file served by the CortexPrism Web UI:

{
  "ui": {
    "panels": [
      { "id": "weather", "title": "Weather", "icon": "cloud", "htmlPath": "./ui/panel.html" }
    ],
    "settings": [
      {
        "section": "API",
        "fields": [
          { "key": "apiKey", "label": "Weather API Key", "type": "secret", "defaultValue": "" }
        ]
      }
    ]
  }
}

Lifecycle Hooks

Your entry point module can export lifecycle hooks that the plugin manager calls automatically:

import type { PluginContext } from "cortex/plugins";

export const onLoad = async (ctx: PluginContext) => {
  // Called when the plugin transitions from INSTALLED to ACTIVE
  // Use for initialization: connect to services, allocate resources
  ctx.logger.info("Plugin loaded");
  await ctx.state.set("startedAt", new Date().toISOString());
};

export const onUnload = async (ctx: PluginContext) => {
  // Called when the plugin transitions from ACTIVE to UNLOADING
  // Use for cleanup: close connections, release resources
  ctx.logger.info("Plugin unloaded");
};

PluginContext

Lifecycle hooks receive a PluginContext with these properties:

PropertyTypeDescription
ctx.loggerLoggerStructured logger prefixed with plugin name
ctx.configConfigStorePlugin configuration from ~/.cortex/config.json
ctx.stateStateStoreIn-memory key-value store persisted to state.json
ctx.pluginDirstringPlugin installation directory
ctx.dataDirstringPlugin-private data directory (~/.cortex/data/plugins/<name>/data/)

Use ctx.config and ctx.state instead of direct file access for portability.

Plugin Store Layout

When installed, plugin data is stored under ~/.cortex/data/plugins/:

~/.cortex/data/plugins/
├── <plugin-name>/
│   ├── esm/          # downloaded ESM source
│   ├── wasm/         # downloaded WASM binaries
│   ├── data/         # plugin-private data
│   └── state.json    # persisted plugin state (ctx.state)

Testing

Test plugins with the CortexPrism test harness:

import { testPlugin } from "@cortexprism/plugin-sdk/testing";
import myPlugin from "./mod.ts";

const harness = await testPlugin(myPlugin);

// Test a capability
const result = await harness.call("countLines", "test/fixture.ts");
console.assert(result.total > 0);

// Test with custom context
const withConfig = await testPlugin(myPlugin, {
  config: { maxLines: 100 },
});

Bundling for Distribution

The plugin manager can load plugins from:

  1. Local directory: cortex plugin install ./path/to/plugin
  2. npm package: cortex plugin install npm:@scope/my-plugin
  3. JSR package: cortex plugin install jsr:@scope/my-plugin
  4. Marketplace: cortex plugin install marketplace:my-plugin
  5. Single file URL: cortex plugin install https://example.com/plugin.js

For distribution, bundle to a single ESM file:

deno bundle mod.ts dist/plugin.js

Permissions Declaration

Declare the permissions your plugin needs in the manifest:

{
  "name": "network-scanner",
  "version": "1.0.0",
  "permissions": {
    "network": ["fetch:https://api.example.com"],
    "filesystem": ["read:/tmp"],
    "env": ["MY_API_KEY"]
  }
}

The Parallax policy engine enforces these permissions at runtime.