Skip to content

Latest commit

 

History

History
603 lines (468 loc) · 16.9 KB

File metadata and controls

603 lines (468 loc) · 16.9 KB

Plugin API Full Reference — XunCode

Complete guide for building and publishing XunCode plugins. This document covers the full lifecycle, every ctx.* method, file-system access, terminal integration, examples, limitations, and publishing rules.

Plugin API version: 1.1.0 — corresponds to XunCode app version ≥ 1.1.0


Table of Contents

  1. Lifecycle Overview
  2. Plugin Manifest
  3. ctx.editor
  4. ctx.ui
  5. ctx.hooks
  6. ctx.storage
  7. ctx.http
  8. ctx.terminal
  9. ctx.fs
  10. Publishing & Review
  11. Full Examples
  12. Limitations & Sandbox
  13. Changelog

Lifecycle Overview

App starts
  └─ Plugin JS loaded from URL
       └─ VscodePlugin.register() called
            └─ activate(ctx) called
                 ├─ Register commands, hooks, UI items
                 └─ Plugin is running

User removes plugin
  └─ deactivate() called
       └─ Clean up timers, listeners, status-bar items, etc.

Best practices:

  • Keep activate() fast — don't do heavy work synchronously.
  • Always implement deactivate() if you set up timers, intervals, or global listeners.
  • Debounce onDidChangeContent — it fires on every keystroke.
  • Use ctx.storage for persistence, not localStorage.

Plugin Manifest

Fields passed to VscodePlugin.register():

Field Type Required Description
id string Unique ID in org.name format, e.g. acme.formatter
name string Display name shown in marketplace
version string Semver string, e.g. 1.0.0
description string Short description (1–2 sentences)
author string Author name or GitHub handle
activate(ctx) function Entry point — called when plugin loads
deactivate() function Called when plugin is removed

ctx.editor

Interact with the Monaco Editor instance.

ctx.editor.getValue() → string

Returns the full text content of the current editor buffer.

const code = ctx.editor.getValue();
console.log('File has', code.split('\n').length, 'lines');

ctx.editor.setValue(text: string)

Replaces the entire editor content. Adds to undo history.

ctx.editor.setValue('// rewritten\n' + ctx.editor.getValue());

ctx.editor.getSelection() → string

Returns the currently selected text, or empty string if nothing selected.

ctx.editor.replaceSelection(text: string)

Replaces the current selection with text. If nothing is selected, inserts at cursor.

const sel = ctx.editor.getSelection();
if (sel) ctx.editor.replaceSelection(sel.toUpperCase());

ctx.editor.getCursorPosition() → { lineNumber, column }

Returns the current cursor position.

const pos = ctx.editor.getCursorPosition();
ctx.ui.showMessage(`Line ${pos.lineNumber}, Col ${pos.column}`);

ctx.editor.getLanguage() → string

Returns the current language ID, e.g. "javascript", "python", "dart".

All supported language IDs: javascript typescript python kotlin java dart go rust c cpp csharp html css scss json yaml markdown shell xml php ruby swift sql r lua plaintext

ctx.editor.setLanguage(lang: string)

Changes the syntax highlighting language.

ctx.editor.setLanguage('typescript');

ctx.editor.addCommand(cmd: object)

Registers a command that appears in the editor command palette.

ctx.editor.addCommand({
  id: 'my-plugin.run',        // must be unique
  label: 'My Plugin: Run',    // shown in command palette
  keybinding: null,           // optional: Monaco KeyCode name
  run() {
    // called when command is triggered
  }
});

ctx.editor.onDidChangeContent(fn: function)

Fires fn(newContent: string) on every content change (every keystroke). Use sparingly.

ctx.editor.onDidChangeContent((content) => {
  // debounce this in production!
});

ctx.editor.format()

Triggers Monaco's built-in document formatter.


ctx.ui

Show messages and interact with the app UI.

ctx.ui.showMessage(text: string)

Shows a snackbar notification (dark, bottom of screen). Auto-dismisses after 3s.

ctx.ui.showError(text: string)

Shows an error snackbar (red). Auto-dismisses after 4s.

ctx.ui.addStatusBarItem(item: object)

Adds a clickable item to the bottom status bar.

ctx.ui.addStatusBarItem({
  id: 'my-plugin.status',
  text: 'My Plugin',
  tooltip: 'Click to run',
  onClick() {
    ctx.ui.showMessage('Status bar clicked!');
  }
});

Note: Status bar items are removed automatically when the plugin is deactivated.


ctx.hooks

React to editor and app lifecycle events.

ctx.hooks.onFileSave(fn: function)

Called with fn(uri: string, content: string) whenever the user saves.

ctx.hooks.onFileSave((uri, content) => {
  console.log('Saved:', uri, '—', content.length, 'chars');
});

ctx.hooks.onFileOpen(fn: function)

Called with fn(uri: string, content: string, filename: string) when a file is opened.

ctx.hooks.onFileOpen((uri, content, filename) => {
  if (filename.endsWith('.py')) {
    ctx.ui.showMessage('Python file opened');
  }
});

ctx.hooks.onEditorReady(fn: function)

Called once when Monaco has fully initialized. Use for setup that requires the editor to be ready.

ctx.hooks.onEditorReady(() => {
  ctx.editor.setValue('// Welcome!\n');
});

ctx.storage

Persistent key-value storage scoped to your plugin ID. Backed by localStorage — survives app restarts.

ctx.storage.get(key: string) → string | null

ctx.storage.set(key: string, value: string)

ctx.storage.remove(key: string)

// Store and retrieve settings
let apiKey = ctx.storage.get('api-key');
if (!apiKey) {
  apiKey = prompt('Enter API key:');
  ctx.storage.set('api-key', apiKey);
}

// Store JSON
ctx.storage.set('config', JSON.stringify({ theme: 'dark', size: 14 }));
const config = JSON.parse(ctx.storage.get('config') || '{}');

Tip: Prefix your keys with your plugin ID to avoid collisions: my-plugin.api-key


ctx.http

Make HTTP requests from your plugin. All requests automatically route through Tor SOCKS5 proxy when the user has Tor enabled in settings.

ctx.http.get(url: string, headers?: object) → Promise<string>

ctx.http.post(url: string, body: string, headers?: object) → Promise<string>

// GET request
const raw = await ctx.http.get('https://api.github.com/repos/owner/repo');
const repo = JSON.parse(raw);

// POST with JSON body and auth header
const response = await ctx.http.post(
  'https://api.anthropic.com/v1/messages',
  JSON.stringify({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'Hello' }]
  }),
  {
    'x-api-key': ctx.storage.get('anthropic-key'),
    'anthropic-version': '2023-06-01',
    'content-type': 'application/json'
  }
);
const data = JSON.parse(response);

Note: The target server must allow CORS (Access-Control-Allow-Origin: *) or requests will fail.


ctx.terminal

Interact with the built-in terminal (proot + Alpine). Requires the user to have Alpine installed.

Warning: Terminal API is asynchronous. All methods return Promise<void>.

ctx.terminal.write(text: string)

Send text to the active terminal. Supports ANSI escape codes.

await ctx.terminal.write('ls -la\n');

ctx.terminal.clear()

Clear the terminal screen (sends \x1Bc).

await ctx.terminal.clear();

ctx.terminal.onOutput(fn: function)

Listen to terminal output. fn(data: string) receives raw output chunks.

ctx.terminal.onOutput((data) => {
  console.log('term:', data);
});

Note: Only one listener per plugin is supported. Registering a new listener replaces the previous one.


ctx.fs

Limited file-system access through the app's sandbox. All paths are relative to the app project directory (/sdcard/XunCode/Projects/ inside proot, or the external app directory on Android).

ctx.fs.read(path: string) → Promise<string>

Read a file as UTF-8 text. Returns empty string if the file does not exist.

const content = await ctx.fs.read('README.md');
console.log(content);

ctx.fs.write(path: string, content: string) → Promise<void>

Write text to a file. Creates parent directories automatically.

await ctx.fs.write('notes.txt', 'Hello from plugin!\n');

ctx.fs.readdir(path: string) → Promise<string[]>

List files and directories at the given path. Returns an array of names (not full paths). Returns [] if the directory does not exist.

const files = await ctx.fs.readdir('src');
console.log('Files:', files.join(', '));

Limitations: ctx.fs cannot access files outside the app project directory. No rename, delete, or mkdir — use ctx.terminal.write() with shell commands if needed.


Publishing & Review

Once your plugin is hosted and tested:

  1. Go to xuncode-market.vercel.app/submit (or your custom marketplace URL)
  2. Fill in the submission form:
    • Plugin name, description, version
    • Author name / GitHub handle
    • Category: AI / Assistant | Formatter | Language Support | Theme | Git | Utility | Terminal
    • Tags (comma-separated)
    • Install URL (the raw JS URL)
    • GitHub repo URL (optional but recommended)
    • Contact email (not shown publicly)
  3. Submit — the maintainer reviews your plugin
  4. On approval: plugin goes live in the in-app Marketplace
  5. On rejection: you receive an email with the reason

Review criteria:

  • Plugin must work as described
  • No malicious code (network requests to unknown endpoints, data exfiltration)
  • Must use VscodePlugin.register() correctly
  • Description must be accurate
  • File size under 500KB
  • ctx.fs must not attempt to escape the project directory

Full Examples

Word Counter

VscodePlugin.register({
  id: 'example.word-counter',
  name: 'Word Counter',
  version: '1.0.0',
  description: 'Counts words and shows stats',
  author: 'example',

  activate(ctx) {
    function getStats() {
      const text = ctx.editor.getValue();
      const words = text.trim().split(/\s+/).filter(Boolean).length;
      const chars = text.length;
      const lines = text.split('\n').length;
      return { words, chars, lines };
    }

    ctx.editor.addCommand({
      id: 'word-counter.show',
      label: 'Word Counter: Show Stats',
      run() {
        const s = getStats();
        ctx.ui.showMessage(`${s.words} words · ${s.chars} chars · ${s.lines} lines`);
      }
    });

    ctx.hooks.onFileSave(() => {
      const s = getStats();
      ctx.ui.showMessage(`Saved — ${s.words} words`);
    });
  }
});

Auto Header

VscodePlugin.register({
  id: 'example.auto-header',
  name: 'Auto Header',
  version: '1.0.0',
  description: 'Inserts a file header comment on save',
  author: 'example',

  activate(ctx) {
    ctx.editor.addCommand({
      id: 'auto-header.insert',
      label: 'Auto Header: Insert',
      run() {
        const lang = ctx.editor.getLanguage();
        const date = new Date().toISOString().split('T')[0];
        const author = ctx.storage.get('auto-header.author') || 'Unknown';
        const isPy = lang === 'python' || lang === 'r';
        const prefix = isPy ? '#' : '//';
        const header = `${prefix} Author: ${author}\n${prefix} Date: ${date}\n${prefix} Language: ${lang}\n\n`;
        ctx.editor.setValue(header + ctx.editor.getValue());
      }
    });
  }
});

AI Code Review (uses Anthropic API)

VscodePlugin.register({
  id: 'example.ai-review',
  name: 'AI Code Review',
  version: '1.0.0',
  description: 'Reviews selected code using Claude',
  author: 'example',

  activate(ctx) {
    ctx.editor.addCommand({
      id: 'ai-review.review',
      label: 'AI: Review Selected Code',
      async run() {
        let key = ctx.storage.get('ai-review.key');
        if (!key) {
          key = prompt('Anthropic API key:');
          if (!key) return;
          ctx.storage.set('ai-review.key', key);
        }

        const code = ctx.editor.getSelection() || ctx.editor.getValue();
        const lang = ctx.editor.getLanguage();
        ctx.ui.showMessage('Reviewing…');

        try {
          const raw = await ctx.http.post(
            'https://api.anthropic.com/v1/messages',
            JSON.stringify({
              model: 'claude-sonnet-4-6',
              max_tokens: 1024,
              messages: [{
                role: 'user',
                content: `Review this ${lang} code for bugs, style issues, and improvements. Be concise:\n\n\`\`\`${lang}\n${code}\n\`\`\``
              }]
            }),
            {
              'x-api-key': key,
              'anthropic-version': '2023-06-01',
              'content-type': 'application/json'
            }
          );
          const result = JSON.parse(raw);
          const review = result.content[0].text;
          // Insert review as a comment block above the code
          const lines = review.split('\n').map(l => `// ${l}`).join('\n');
          ctx.editor.setValue(lines + '\n\n' + ctx.editor.getValue());
          ctx.ui.showMessage('Review inserted above your code');
        } catch (e) {
          ctx.ui.showError('Error: ' + e.message);
        }
      }
    });
  }
});

Terminal File Explorer

VscodePlugin.register({
  id: 'example.term-explorer',
  name: 'Terminal Explorer',
  version: '1.0.0',
  description: 'List files via terminal and open them in editor',
  author: 'example',

  activate(ctx) {
    ctx.editor.addCommand({
      id: 'term-explorer.list',
      label: 'Terminal Explorer: List Files',
      async run() {
        await ctx.terminal.clear();
        await ctx.terminal.write('ls -la /home/user\n');
      }
    });

    ctx.terminal.onOutput((data) => {
      // Parse terminal output and offer quick-pick for file names
      const files = data.split(/\s+/).filter(s => s.includes('.'));
      if (files.length > 0) {
        ctx.ui.showMessage(`Found ${files.length} files in terminal output`);
      }
    });
  }
});

Limitations & Sandbox

Plugins run inside the Monaco WebView sandbox. The following are not available:

Not available Reason
require() / import No Node.js, no module system
fs, path, os No direct filesystem access (use ctx.fs instead)
fetch() directly Use ctx.http instead (Tor-aware)
DOM manipulation outside editor Sandboxed WebView
Native Android APIs Use the Flutter bridge via ctx.http
localStorage directly Use ctx.storage (scoped to plugin)
Plugins > 500KB Hard limit enforced by marketplace

What IS available:

  • Full ES2020+ JavaScript
  • console.log/warn/error (visible in Developer Mode)
  • JSON, Math, Date, Promise, async/await
  • setTimeout, setInterval, clearTimeout, clearInterval
  • All ctx.* APIs listed in this document

Hosting Your Plugin

Your plugin must be hosted at a publicly accessible URL that serves the JS file with CORS headers.

Option 1: GitHub Raw (easiest)

  1. Create a public GitHub repo
  2. Add your plugin.js file
  3. Use the raw URL: https://raw.githubusercontent.com/user/repo/main/plugin.js

GitHub raw URLs already have CORS headers — no extra config needed.

Option 2: Vercel (recommended for production)

  1. Create a Next.js or static project
  2. Put your plugin in public/plugin.js
  3. Add vercel.json for CORS:
{
  "headers": [
    {
      "source": "/plugin.js",
      "headers": [
        { "key": "Access-Control-Allow-Origin", "value": "*" }
      ]
    }
  ]
}
  1. Deploy: vercel --prod
  2. Your plugin URL: https://your-project.vercel.app/plugin.js

Option 3: jsDelivr CDN

If your plugin is on GitHub, jsDelivr serves it with CORS automatically: https://cdn.jsdelivr.net/gh/user/repo@main/plugin.js


Support & Community

  • GitHub: @H4F8 — open issues and PRs here
  • Dev channel: t.me/XunKal1Dev — direct line to the maintainer
  • Community channel: t.me/GodPassTGK — announcements and discussion

Changelog

v1.1.0

  • Added ctx.terminal API (write, clear, onOutput)
  • Added ctx.fs API (read, write, readdir)
  • Improved terminal batching and debounce on Android 10+
  • AXS binary now loads from libaxs.so in nativeLibraryDir (noexec bypass)
  • WebSocket handshake stabilized via WebSocket.connect()

v1.0.0

  • Initial plugin API release
  • ctx.editor: getValue, setValue, getSelection, replaceSelection, getCursorPosition, getLanguage, setLanguage, addCommand, onDidChangeContent, format
  • ctx.ui: showMessage, showError, addStatusBarItem
  • ctx.hooks: onFileSave, onFileOpen, onEditorReady
  • ctx.storage: get, set, remove
  • ctx.http: get, post (Tor-aware)