Skip to content

xopc Extension System

xopc provides a lightweight but powerful extension system for customizing and extending functionality.

Features

  • 🏗️ Three-tier Storage - Workspace / Global / Bundled
  • 🔌 Extension SDK - Official SDK with unified imports
  • Native TypeScript - Instant loading via jiti, no compilation
  • 📦 Multi-source Installation - npm, local directory, Git repository

Quick Start

Install Extension

Using CLI (recommended):

bash
# Install from npm to workspace
xopc extension install xopc-extension-hello

# Install to global (shared across projects)
xopc extension install xopc-extension-hello --global

# Install from local directory
xopc extension install ./my-local-extension

# View installed extensions
xopc extension list

# Remove extension
xopc extension remove hello

Enable Extension

Configure in ~/.xopc/xopc.json:

json
{
  "extensions": {
    "enabled": ["hello", "echo"],
    "hello": { "greeting": "Hi there!" },
    "echo": true
  }
}

Configuration format:

FieldTypeDescription
enabledstring[]List of extension IDs to enable
disabledstring[](Optional) List of extension IDs to disable
[extension-id]object | booleanExtension-specific configuration

Create New Extension

bash
xopc extension create my-extension --name "My Extension" --kind utility

Supported kinds: channel | provider | memory | tool | utility

This creates:

  • package.json - npm config
  • index.ts - Extension entry (TypeScript)
  • xopc.extension.json - Extension manifest
  • README.md - Documentation template

Three-tier Storage Architecture

xopc supports three-tier extension storage:

LevelPathUse CasePriority
Workspaceworkspace/.extensions/Project-private extensions⭐⭐⭐ Highest
Global~/.xopc/extensions/User-level shared extensions⭐⭐ Medium
Bundledxopc/extensions/Built-in extensions⭐ Lowest

Priority Rules

  • Workspace extensions override Global and Bundled extensions with same name
  • Global extensions override Bundled extensions with same name

Use cases:

  • Workspace: Project-specific custom extensions
  • Global: Commonly used shared extensions (like telegram-channel)
  • Bundled: Official extensions shipped with xopc

Monorepo note: The Telegram channel is a workspace package under extensions/telegram (@xopcai/xopc-extension-telegram) and is wired into the core via src/channels/plugins/bundled.ts. It is not loaded from xopc/extensions/ at runtime; that path refers to other bundled extension assets.


Extension SDK

The npm package name is @xopcai/xopc. Import the SDK through the published subpath:

typescript
// Recommended: published package subpath
import type { ExtensionApi, ExtensionDefinition } from '@xopcai/xopc/extension-sdk';

When developing extensions against a local checkout, the loader may still resolve the legacy alias xopc/extension-sdk to src/extension-sdk/index.ts.

Exported Types

typescript
// Core types
import type {
  ExtensionDefinition,      // Extension definition
  ExtensionApi,             // Extension API
  ExtensionLogger,          // Logger interface
} from '@xopcai/xopc/extension-sdk';

// Tools (re-exported from pi-agent-core)
import type {
  AgentTool,
  AgentToolResult,
} from '@xopcai/xopc/extension-sdk';

// Hooks
import type {
  ExtensionHookEvent,       // Hook event type
  ExtensionHookHandler,     // Hook handler
  HookOptions,              // Hook options
} from '@xopcai/xopc/extension-sdk';

// Channels (ChannelPlugin registry)
import type {
  ChannelPlugin,
  ChannelPluginInitOptions,
  ChannelPluginStartOptions,
} from '@xopcai/xopc/extension-sdk';

import {
  defineChannelPluginEntry,
  registerExtensionCliProgram,
} from '@xopcai/xopc/extension-sdk';

// Commands
import type { ExtensionCommand } from '@xopcai/xopc/extension-sdk';

// Services
import type { ExtensionService } from '@xopcai/xopc/extension-sdk';

Extension Structure

Manifest File

Each extension must include xopc.extension.json:

json
{
  "id": "my-extension",
  "name": "My Extension",
  "description": "A description of my extension",
  "version": "1.0.0",
  "main": "index.js",
  "kind": "utility",
  "configSchema": {
    "type": "object",
    "properties": {
      "option1": {
        "type": "string",
        "default": "value"
      }
    }
  }
}

Extension Entry File

typescript
import type { ExtensionApi } from '@xopcai/xopc/extension-sdk';

const extension = {
  id: 'my-extension',
  name: 'My Extension',
  description: 'Description here',
  version: '1.0.0',
  kind: 'utility',

  // Called when extension is registered
  register(api: ExtensionApi) {
    // Register tool
    api.registerTool({...});
    
    // Register command
    api.registerCommand({...});
    
    // Register hook
    api.registerHook('message_received', async (event, ctx) => {...});
    
    // Register HTTP route
    api.registerHttpRoute('/my-route', async (req, res) => {...});
  },

  // Called when extension is enabled
  activate(api: ExtensionApi) {
    console.log('Extension activated');
  },

  // Called when extension is disabled
  deactivate(api: ExtensionApi) {
    console.log('Extension deactivated');
  },
};

export default extension;

Core Concepts

Tools

Extensions can register custom tools:

typescript
api.registerTool({
  name: 'my_tool',
  description: 'Do something useful',
  parameters: {
    type: 'object',
    properties: {
      input: { 
        type: 'string', 
        description: 'Input value' 
      }
    },
    required: ['input']
  },
  async execute(params, ctx) {
    const input = params.input;
    // Perform operation
    return `Result: ${input}`;
  }
});

Hooks

Hooks intercept and modify behavior at lifecycle points:

HookTimingUse Case
before_agent_startBefore Agent startsModify system prompt
agent_endAfter Agent completesPost-process results
message_receivedWhen message receivedMessage pre-processing
message_sendingBefore sending messageIntercept/modify content
message_sentAfter message sentSend logging
before_tool_callBefore tool callParameter validation
after_tool_callAfter tool callResult processing
session_startSession startInitialization
session_endSession endCleanup

Example - Block sensitive content:

typescript
api.registerHook('message_sending', async (event, ctx) => {
  const { content } = event;

  // Block sensitive information
  if (content.includes('sensitive info')) {
    return {
      cancel: true,
      cancelReason: 'Content contains sensitive information'
    };
  }

  // Add signature
  if (content.includes('{{signature}}')) {
    return {
      content: content.replace(
        '{{signature}}', 
        '\n\n— Sent by AI Assistant'
      )
    };
  }
});

Example - Block dangerous tools:

typescript
api.registerHook('before_tool_call', async (event, ctx) => {
  const { toolName } = event;

  // Block dangerous operations
  if (toolName === 'delete_file' || toolName === 'execute_command') {
    return {
      block: true,
      blockReason: 'This operation is disabled for safety'
    };
  }
});

Commands

Register custom CLI commands:

typescript
api.registerCommand({
  name: 'status',
  description: 'Check extension status',
  acceptArgs: false,
  requireAuth: true,
  handler: async (args, ctx) => {
    return {
      content: 'Extension is running!',
      success: true
    };
  }
});

HTTP Routes

typescript
api.registerHttpRoute('/my-extension/status', async (req, res) => {
  res.json({ status: 'running', extension: 'my-extension' });
});

Gateway Methods

typescript
api.registerGatewayMethod('my-extension.status', async (params) => {
  return { status: 'running' };
});

Background Services

typescript
api.registerService({
  id: 'my-service',
  start(context) {
    // Start background task
    this.interval = setInterval(() => {
      // Scheduled task
    }, 60000);
  },
  stop(context) {
    if (this.interval) {
      clearInterval(this.interval);
    }
  }
});

Configuration Management

Define Configuration Schema

json
{
  "configSchema": {
    "type": "object",
    "properties": {
      "apiKey": {
        "type": "string",
        "description": "API Key for the service"
      },
      "maxResults": {
        "type": "number",
        "default": 10
      }
    },
    "required": ["apiKey"]
  }
}

Access Configuration

typescript
const apiKey = api.extensionConfig.apiKey;
const maxResults = api.extensionConfig.maxResults || 10;

Logging

typescript
api.logger.debug('Detailed debug information');
api.logger.info('General information');
api.logger.warn('Warning message');
api.logger.error('Error message');

Path Resolution

typescript
// Resolve workspace path
const configPath = api.resolvePath('config.json');

// Resolve extension relative path
const dataPath = api.resolvePath('./data.json');

Event System

typescript
// Emit event
api.emit('my-event', { key: 'value' });

// Listen for event
api.on('other-event', (data) => {
  console.log('Received:', data);
});

// Remove listener
api.off('my-event', handler);

Complete Example

typescript
import type { ExtensionApi } from '@xopcai/xopc/extension-sdk';

const extension = {
  id: 'example',
  name: 'Example Extension',
  description: 'A complete example extension',
  version: '1.0.0',
  kind: 'utility',
  configSchema: {
    type: 'object',
    properties: {
      enabled: { type: 'boolean', default: true }
    }
  },

  register(api) {
    // Register tool
    api.registerTool({
      name: 'example_tool',
      description: 'Example tool',
      parameters: {
        type: 'object',
        properties: { input: { type: 'string' } },
        required: ['input']
      },
      async execute(params) {
        return `Processed: ${params.input}`;
      }
    });

    // Register hook
    api.registerHook('message_received', async (event) => {
      console.log('Received:', event.content);
    });

    // Register command
    api.registerCommand({
      name: 'example',
      description: 'Example command',
      handler: async (args) => {
        return { content: 'Example!', success: true };
      }
    });
  },

  activate(api) {
    console.log('Extension activated');
  },

  deactivate(api) {
    console.log('Extension deactivated');
  }
};

export default extension;

Publishing Extensions

  1. Create xopc.extension.json manifest
  2. Create index.ts entry file
  3. Push to GitHub or publish to npm
bash
# Publish to npm (public)
npm publish --access public

# If using scoped package name (recommended)
# package.json: { "name": "@yourname/xopc-extension-name" }
npm publish --access public

Best Practices

  1. Error handling: All async operations should use try/catch
  2. Logging: Use the API's logging system instead of console
  3. Resource cleanup: Release resources in deactivate
  4. Configuration validation: Use JSON Schema to validate configuration
  5. Version management: Follow semantic versioning
  6. TypeScript: Use TypeScript for better type safety
  7. Minimal dependencies: Keep extensions lightweight

CLI Command Reference

extension install

bash
# Install from npm
xopc extension install <package-name>

# Install specific version
xopc extension install my-extension@1.0.0

# Install from local directory
xopc extension install ./local-extension-dir

# Set timeout (default 120 seconds)
xopc extension install slow-extension --timeout 300000

extension list

bash
xopc extension list

extension remove / uninstall

bash
xopc extension remove <extension-id>
xopc extension uninstall <extension-id>

extension info

bash
xopc extension info <extension-id>

extension create

bash
xopc extension create <extension-id> [options]

Options:
  --name <name>           Extension display name
  --description <desc>    Extension description
  --kind <kind>          Extension type: channel|provider|memory|tool|utility

Troubleshooting

Extension Not Loading

  1. Check if extension is in enabled array
  2. Verify xopc.extension.json manifest is valid
  3. Check logs for loading errors

Installation Failed

  1. Check network connection
  2. Verify package name is correct
  3. Check timeout setting for slow installations

Hook Not Triggering

  1. Verify hook name is correct
  2. Check if hook is registered in register() method
  3. Check logs for hook registration errors

Extension Configuration

Global Configuration

The extensions section in config.json supports the following global options:

json
{
  "extensions": {
    "enabled": {
      "hello": true,
      "echo": false
    },
    "allow": ["hello", "echo", "xopc-feishu"],
    "security": {
      "checkPermissions": true,
      "allowUntrusted": false,
      "trackProvenance": true,
      "allowPromptInjection": false
    },
    "slots": {
      "memory": "memory-lancedb",
      "tts": "elevenlabs"
    }
  }
}
OptionTypeDescription
enabledRecord<string, boolean>Enable/disable specific extensions
allowstring[]Allowlist of permitted extensions
security.checkPermissionsbooleanEnable path safety checks
security.allowUntrustedbooleanAllow loading extensions not in allowlist
security.trackProvenancebooleanTrack extension install source
security.allowPromptInjectionbooleanAllow extensions to inject system prompts
slots.memorystringPreferred memory backend extension
slots.ttsstringPreferred TTS provider extension
slots.imageGenerationstringPreferred image generation extension
slots.webSearchstringPreferred web search extension

Extension-Specific Configuration

Each extension can have its own custom configuration. Any fields not in the global config are treated as extension-specific:

json
{
  "extensions": {
    "feishu": {
      "appId": "cli_xxx",
      "appSecret": "yyy",
      "verificationToken": "zzz"
    },
    "memory-lancedb": {
      "vectorDim": 1536,
      "persistencePath": "~/data/memory"
    }
  }
}

The extension can access its config via api.extensionConfig:

typescript
// In your extension's register() or activate()
export function register(api: ExtensionApi) {
  const feishuConfig = api.extensionConfig as {
    appId: string;
    appSecret: string;
    verificationToken?: string;
  };
  
  console.log('Feishu App ID:', feishuConfig.appId);
}

Slot Configuration

Slots ensure exclusive capabilities have only one active implementation. Configure which extension should claim each slot:

json
{
  "extensions": {
    "slots": {
      "memory": "my-memory-extension",
      "tts": "my-tts-extension"
    }
  }
}

When a slot has a preferred plugin, other extensions requesting that slot will be rejected.

Security

By default, xopc performs security checks on extensions:

  • Path safety (no symlink escape)
  • Ownership validation
  • Hardlink detection
  • Provenance tracking

Set allowPromptInjection: true to allow extensions to modify system prompts via hook results.

Released under the MIT License.