Skip to content

Latest commit

 

History

History
689 lines (529 loc) · 21.2 KB

File metadata and controls

689 lines (529 loc) · 21.2 KB

Axons Plugin Developer Guide

Version: v1.0 | For Axons ≥ 0.8.0

This guide helps developers build an Axons plugin from scratch, covering directory structure, manifest protocol, frontend build, backend development, packaging, and publishing.

English | 简体中文

Official extension repository: axons-extension-packages — includes language packs, model manager, and other complete examples you can reference or contribute to.


Table of Contents


1. Quick Start

A minimal "frontend-only" plugin only needs 3 files:

com.example.hello/
├── manifest.json
├── ui/
│   └── index.js
└── ui/
    └── icon.svg

manifest.json:

{
  "id": "com.example.hello",
  "name": "Hello World",
  "version": "1.0.0",
  "description": "My first Axons plugin",
  "author": "example",
  "icon": "ui/icon.svg",
  "category": "productivity",
  "minAxonsVersion": "0.8.0",
  "permissions": ["panel:create"],
  "frontend": {
    "entry": "ui/index.js",
    "panels": [{
      "id": "hello",
      "title": "Hello",
      "icon": "ui/icon.svg",
      "location": "right",
      "activator": "activityBar"
    }]
  },
  "activationEvents": ["onStartup"]
}

ui/index.js:

export default function HelloPanel({ pluginApi, onClose, panelId }) {
  const el = document.createElement('div');
  el.innerHTML = '<h2>Hello Axons!</h2>';
  return el;  // Note: for React component pattern, see Section 4
}

However, we recommend using React + Vite for development. See below for details.


2. Directory Structure

Recommended complete plugin directory:

com.example.my-plugin/
├── manifest.json          # Plugin manifest (required)
├── install.sh             # Install script (required for backend dependencies)
├── uninstall.sh           # Uninstall script (optional)
├── requirements.txt       # Python dependencies (for Python backends)
├── server.py              # Backend service (optional)
├── .venv/                 # Python virtual environment (created by install.sh)
├── src/                   # Frontend source code
│   ├── index.tsx          # Entry component
│   └── types.ts           # Type definitions
├── ui/                    # Frontend build output + static assets
│   ├── index.js           # Build artifact (generated by Vite)
│   └── icon.svg           # Panel icon
├── package.json           # Frontend dependencies
├── tsconfig.json          # TypeScript configuration
├── vite.config.js         # Vite build configuration
└── .axons-ignore          # Packaging exclusion rules

.axons-ignore

Exclude unnecessary files during packaging:

node_modules/
.venv/
src/
.git/
*.tar.gz
package-lock.json
tsconfig.json
vite.config.js

3. manifest.json Protocol

Complete Fields

{
  // ─── Basic Info ───
  "id": "com.example.my-plugin",    // Reverse-DNS, globally unique
  "name": "My Plugin",              // Display name
  "version": "1.0.0",              // Semantic version
  "description": "Plugin description",
  "author": "author-name",
  "icon": "ui/icon.svg",           // Relative path
  "category": "productivity",      // analysis | visualization | search | productivity
  "minAxonsVersion": "0.8.0",     // Minimum compatible version

  // ─── Permission Declarations ───
  "permissions": [
    "project:read",      // Read project info
    "graph:read",        // Read code graph data
    "model:register",    // Register/unregister LLM models
    "panel:create",      // Create UI panels
    "state:read",        // Read shared state
    "state:write"        // Write shared state
  ],

  // ─── Backend (optional) ───
  "backend": {
    "command": [".venv/bin/python", "server.py"],  // Startup command
    "port": 0,                    // 0 = OS dynamic allocation, or specify a fixed port
    "healthCheck": "/health",     // Health check path
    "readyTimeout": "15s",        // Ready timeout
    "install": {
      "command": ["bash", "install.sh"],
      "timeout": "300s"
    },
    "uninstall": {
      "command": ["bash", "uninstall.sh"]
    }
  },

  // ─── Frontend (optional) ───
  "frontend": {
    "entry": "ui/index.js",       // ES Module entry
    "panels": [{
      "id": "my-panel",           // Panel ID (unique within plugin)
      "title": "My Panel",        // Panel title
      "icon": "ui/icon.svg",      // Panel icon
      "location": "right",        // right | left | center-bottom | modal
      "activator": "activityBar"  // activityBar | footer | node-select | gearMenu | command
    }],
    "commands": [{
      "id": "my-plugin.open",
      "title": "Open My Plugin",
      "shortcut": "Ctrl+Shift+P"
    }]
  },

  // ─── Activation Events ───
  "activationEvents": ["onStartup"]
}

Four Plugin Archetypes

Archetype Backend Frontend Use Case
Full-stack Panel + own API service
Frontend-only Panel that calls Axons APIs only
Backend-only MCP toolsets
Frontend+CLI Frontend directly calling Axons API

4. Frontend Development

4.1 Project Initialization

mkdir com.example.my-plugin && cd com.example.my-plugin
npm init -y
npm install react react-dom
npm install -D vite @vitejs/plugin-react typescript @types/react @types/react-dom

4.2 vite.config.js Configuration (Important)

This is the most common source of issues. Plugins are loaded as ES Modules by Axons, so you must configure externalization and environment variables correctly:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],

  // ⚠️ Must be defined, otherwise the browser throws "process is not defined"
  define: {
    'process.env.NODE_ENV': JSON.stringify('production'),
  },

  build: {
    lib: {
      entry: 'src/index.tsx',
      formats: ['es'],          // Must be ES Module
      fileName: () => 'index.js',
    },
    rollupOptions: {
      // ⚠️ Externalization: React and axons-plugin-ui are provided by the Axons runtime
      // Do NOT bundle them into the plugin artifact, otherwise React multi-instance issues occur (hooks break)
      external: ['react', 'react-dom', 'axons-plugin-ui'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
          'axons-plugin-ui': 'AxonsPluginUI',
        },
      },
    },
    outDir: 'ui',
    emptyOutDir: false,   // Preserve ui/icon.svg and other static files
  },
});

Two key points:

  1. define: { 'process.env.NODE_ENV': ... } — The browser doesn't have a process global variable. Without this line, you'll get process is not defined. Vite replaces process.env.NODE_ENV with a string constant at build time, and development-mode code gets tree-shaken away.

  2. external: ['react', ...] — React etc. are provided by the Axons host via import map. Plugins should not bundle these dependencies. Bundling them causes React multi-instance issues, breaking hooks.

4.3 Component Protocol

The plugin entry component must follow the PluginPanelProps interface:

// src/index.tsx
import React from 'react';

interface PluginPanelProps {
  pluginApi: import('../lib/pluginApi').PluginApi;
  onClose: () => void;
  panelId: string;
}

export default function MyPanel({ pluginApi, onClose, panelId }: PluginPanelProps) {
  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
      {/* Your panel content */}
    </div>
  );
}
  • pluginApi — The single entry point for communicating with the Axons platform, see Section 4.6
  • onClose — Close the current panel
  • panelId — Current panel ID

Note: The panel is already wrapped in a resizable container by Axons (default 384px wide, draggable). Your component only needs to focus on the content area.

4.4 Using axons-plugin-ui Component Library

Axons provides a UI component library that plugins can use directly, with styles consistent with the main interface:

import React, { useState } from 'react';
import { Button, Card, CardHeader, CardBody, Badge, Spinner, Tabs } from 'axons-plugin-ui';

export default function MyPanel({ pluginApi }) {
  const [activeTab, setActiveTab] = useState('tab1');

  return (
    <div style={{ height: '100%', padding: '12px' }}>
      <Tabs
        tabs={[{ id: 'tab1', label: 'Tab One' }, { id: 'tab2', label: 'Tab Two' }]}
        activeTab={activeTab}
        onChange={setActiveTab}
      />
      <Card>
        <CardHeader>Title</CardHeader>
        <CardBody>
          <Badge variant="success">Ready</Badge>
          <Button variant="primary" onClick={() => {}}>Action</Button>
        </CardBody>
      </Card>
    </div>
  );
}

Available components:

Component Description
Button Button, variant: primary/secondary/ghost, size: default/sm
Card / CardHeader / CardBody Card container
Input Text input
Select Dropdown select
Textarea Multi-line text input
Badge Badge, variant: default/success/warning/error/info
Divider Horizontal separator, spacing: default/lg
EmptyState Empty state placeholder (icon + title + description)
Spinner Loading spinner
ProgressBar Progress bar, value 0-1
List / ListItem List with icon, active state, click handler
Tabs Tab switcher (with keyboard navigation)
Modal Modal dialog (with focus trap)
ConfirmDialog Confirmation dialog

TypeScript support: Axons provides type declarations at /plugin-sdk/axons-plugin-ui.d.ts. This file is compile-time only — it never ships in your plugin bundle. The host maintains it as the single source of truth; plugin developers never need to edit it.

To use it during development:

  • Inside the axons repo: add a paths entry in tsconfig.json:
    {
      "compilerOptions": {
        "paths": {
          "axons-plugin-ui": ["../axons/internal/api/static/dist/plugin-sdk/axons-plugin-ui"]
        }
      }
    }
  • Outside the axons repo: copy the type declaration file from the axons repository into your plugin project (re-copy when the host updates axons-plugin-ui):
    cp <axons-repo>/internal/api/static/dist/plugin-sdk/axons-plugin-ui.d.ts src/axons-plugin-ui.d.ts

At runtime, axons-plugin-ui is provided by the host iframe (UMD global + ESM shim) — your plugin's index.js never bundles it.

4.5 Styling & Theming

Axons provides theme support through CSS variables. Plugins should use variables instead of hardcoded colors:

/* Use CSS variables in component styles */
background: var(--axons-color-surface);
color: var(--axons-text-primary);
border: 1px solid var(--axons-border-subtle);
font-family: var(--axons-font-sans);

Common variables:

Variable Purpose
--axons-color-void Deepest background
--axons-color-deep Secondary deep background
--axons-color-surface Panel background
--axons-color-elevated Overlay background
--axons-color-hover Hover background
--axons-border-subtle Subtle border
--axons-border-default Default border
--axons-text-primary Primary text
--axons-text-secondary Secondary text
--axons-text-muted Muted text
--axons-accent Theme accent color
--axons-success / warning / error / info Status colors
--axons-font-sans Sans-serif font
--axons-font-mono Monospace font

4.6 Using pluginApi

pluginApi is the single entry point for plugins to communicate with the Axons platform, automatically handling desktop/web differences:

Request Plugin Backend API

// Auto-selects: desktop connects directly / web goes through Axons proxy
const resp = await pluginApi.fetch('/api/models');
const data = await resp.json();

SSE Real-time Push

const es = pluginApi.createEventSource('/api/events');
es.onmessage = (e) => {
  const data = JSON.parse(e.data);
  // Handle pushed data
};
// Cleanup
es.close();

EventBus Cross-panel Communication

// Subscribe to events (returns unsubscribe function)
const unsubscribe = pluginApi.onEvent('node:selected', (payload) => {
  console.log('Selected node:', payload);
});

// Broadcast events
pluginApi.emitEvent('model:downloaded', { name: 'llama3' });

// Unsubscribe when component unmounts
unsubscribe();

Shared State

// Write state (namespaced by pluginId)
await pluginApi.setState('lastModel', { name: 'llama3', size: '4.7G' });

// Read state
const lastModel = await pluginApi.getState('lastModel');

5. Backend Development

5.1 Environment Variables

Axons injects the following environment variables when starting the plugin backend process:

Variable Description
AXONS_API_URL Axons API address, e.g. http://127.0.0.1:8080
AXONS_PLUGIN_PORT Port the plugin should bind to (when port: 0 in manifest, OS assigns dynamically)
AXONS_PLUGIN_TOKEN Plugin authentication token

Python backend example:

import os

API_URL = os.environ.get('AXONS_API_URL', 'http://127.0.0.1:8080')
PORT = int(os.environ.get('AXONS_PLUGIN_PORT', '0'))
TOKEN = os.environ.get('AXONS_PLUGIN_TOKEN', '')

5.2 Health Check

The healthCheck endpoint declared in manifest.json must return 200:

# server.py (FastAPI example)
from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
async def health():
    return {"status": "ok"}

5.3 CORS (Required for Desktop)

On desktop, the frontend connects directly to the plugin backend, so CORS headers must be returned:

# FastAPI — Global CORS middleware
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

On web, requests go through the Axons proxy, so CORS is not needed (but adding it won't hurt).

5.4 Calling Axons API

The backend uses AXONS_API_URL to call Axons API directly (same-machine HTTP, no cross-origin issues):

import requests

API_URL = os.environ.get('AXONS_API_URL', 'http://127.0.0.1:8080')

# Get code graph
resp = requests.get(f"{API_URL}/v1/graph/{project_id}")
graph = resp.json()

# Semantic search
resp = requests.post(f"{API_URL}/v1/search", json={
    "query": "authentication logic",
    "project_id": project_id
})

6. Install Scripts

install.sh runs once after a user imports the plugin, used to install backend dependencies:

#!/bin/bash
set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# 1. Check runtime environment
if ! command -v python3 &>/dev/null; then
    echo "Error: python3 not found"
    exit 1
fi

# 2. Create virtual environment and install dependencies
python3 -m venv "$SCRIPT_DIR/.venv"
source "$SCRIPT_DIR/.venv/bin/activate"
pip install --quiet -r "$SCRIPT_DIR/requirements.txt"

echo "Installation complete"

Important notes:

  • Python plugins must use .venv. The manifest.json backend.command should point to .venv/bin/python
  • The script must exit with code 0 for success, non-zero for failure
  • Avoid using sudo and do not modify system-level configurations

7. Packaging & Publishing

Packaging

# 1. Build frontend
npx vite build

# 2. Package as .tar.gz (filename format: {id}-{version}.axons-plugin.tar.gz)
cd ..
tar -czf com.example.my-plugin-1.0.0.axons-plugin.tar.gz \
  -C com.example.my-plugin \
  --exclude-from=com.example.my-plugin/.axons-ignore \
  .

Importing

Users upload .axons-plugin.tar.gz files in the Axons Extensions panel, or via API:

curl -X POST http://localhost:8080/v1/plugins/import \
  -F "file=@com.example.my-plugin-1.0.0.axons-plugin.tar.gz"

8. Debugging Tips

Frontend Debugging

  1. Browser DevTools — Open DevTools in the Axons web interface. The Sources panel can find plugin code
  2. console.log — Plugin console.log output goes to the browser console
  3. Vite dev server — You can run npx vite dev separately to develop components, but note that pluginApi is not available in standalone mode

Backend Debugging

  1. Manual start — Set environment variables and run the backend directly:
    export AXONS_API_URL=http://127.0.0.1:8080
    export AXONS_PLUGIN_PORT=18080
    export AXONS_PLUGIN_TOKEN=test
    .venv/bin/python server.py
  2. curl testing — Directly request the plugin backend API
  3. Axons logs — View plugin stdout/stderr output

Common Debugging APIs

# View installed plugins
curl http://localhost:8080/v1/plugins

# View plugin panel registry
curl http://localhost:8080/v1/plugins/registry/panels

# Read shared state
curl http://localhost:8080/v1/plugins/state/com.example.my-plugin:lastModel

# Write shared state
curl -X PUT http://localhost:8080/v1/plugins/state/com.example.my-plugin:key \
  -H "Content-Type: application/json" \
  -d '{"value": "test"}'

9. FAQ

Q: Plugin loads with "Failed to resolve module specifier 'react'"

Cause: The browser cannot resolve bare module specifiers (like import from "react"). Axons resolves this via import map. Ensure your vite.config.js has external configured for react, react-dom, and axons-plugin-ui.

Q: Plugin loads with "process is not defined"

Cause: Build artifacts contain references to process.env.NODE_ENV, but the browser doesn't have a process global variable.

Solution: Add to vite.config.js:

define: {
  'process.env.NODE_ENV': JSON.stringify('production'),
}

Q: React hooks error "Invalid hook call"

Cause: The plugin bundled React, causing two instances alongside the Axons host's React.

Solution: Ensure vite.config.js has external: ['react', 'react-dom']. Do not bundle React into the plugin artifact.

Q: Desktop CORS error when requesting plugin backend

Solution: The plugin backend must add CORS headers to allow cross-origin requests. See the FastAPI example in Section 5.3.

Q: Plugin backend fails to start

Troubleshooting steps:

  1. Set environment variables manually and run backend.command
  2. Check if the port is occupied
  3. Check if the healthCheck endpoint is ready within readyTimeout
  4. Check plugin stdout/stderr in Axons logs

Q: How to apply code changes?

  1. Rebuild frontend: npx vite build
  2. Re-package the plugin: tar -czf ...
  3. Uninstall the old plugin in the Axons Extensions panel, then import the new package
  4. Or via API: first DELETE /v1/plugins/{id} then POST /v1/plugins/import

Q: How to support different panel locations?

Set location in manifest.json panels:

location Description Panel width
right Right panel (default) 384px, draggable
left Left panel Follows left panel container
center-bottom Bottom panel Full width
modal Modal dialog Centered popup

Appendix: Complete Example References

Official Extension Repository

For complete example code and ready-to-use plugin templates, check the official extension repository: axons-extension-packages

Plugin Type Description
com.axons.locale-zh-cn Static Simplified Chinese language pack
com.axons.huggingface Full-stack (Frontend + Backend) HuggingFace GGUF model browser and local LLM management

More Development Docs