28 KiB
Paarrot Plugin API Documentation
Complete reference for developing plugins for Paarrot/Cinny Desktop.
Table of Contents
Getting Started
Plugin Location
Plugins are installed in:
- Windows:
%APPDATA%\paarrot\plugins\<plugin-name>\ - Linux:
~/.config/Paarrot/plugins/<plugin-name>/ - macOS:
~/Library/Application Support/Paarrot/plugins/<plugin-name>/
Required Files
index.js- Main plugin file (required)plugin-metadata.json- Plugin metadata (required)README.md- Documentation (recommended)
plugin-metadata.json
{
"name": "My Awesome Plugin",
"version": "1.0.0",
"description": "Does something cool",
"author": "Your Name",
"homepage": "https://github.com/username/plugin",
"thumbnail": "https://example.com/icon.png",
"tags": ["utility", "messaging", "fun"]
}
Plugin Structure
Basic Plugin
module.exports = {
name: "My Plugin",
version: "1.0.0",
dependencies: {
// "other-plugin": "^1.0.0"
},
/**
* Called when plugin loads
* @param {PluginContext} ctx - Plugin context
*/
onLoad: async (ctx) => {
ctx.log('Plugin loaded!');
// Initialize your plugin here
ctx.commands.register({
name: "hello",
description: "Say hello",
run: () => "Hello, world!"
});
},
/**
* Called when plugin unloads
*/
onUnload: async () => {
console.log('Plugin unloaded');
},
/**
* API for other plugins
*/
exports: null
};
Plugin Context API
The PluginContext provides access to all plugin APIs:
interface PluginContext {
pluginId: string;
matrixClient: MatrixClient;
React: typeof import('react');
lucide: typeof import('lucide-react');
commands: CommandsAPI;
messages: MessagesAPI;
ui: UIAPI;
settings: SettingsAPI;
matrix: MatrixEventsAPI;
timers: TimersAPI;
notify: NotificationFunction;
log: LogFunction;
warn: LogFunction;
error: LogFunction;
require: RequireFunction;
}
Commands
Register slash commands that users can type in chat.
Register a Command
ctx.commands.register({
name: "mycommand",
description: "Does something cool",
args: ["arg1", "arg2"], // Optional
run: (args) => {
// Return string to send as message
return `Got args: ${args.arg1}, ${args.arg2}`;
}
});
Examples
Simple command:
ctx.commands.register({
name: "shrug",
description: "Send a shrug emoji",
run: () => "¯\\_(ツ)_/¯"
});
// Usage: /shrug
Command with arguments:
ctx.commands.register({
name: "greet",
description: "Greet someone",
args: ["name"],
run: ({ name }) => `Hello, ${name || 'stranger'}!`
});
// Usage: /greet Alice
Advanced command:
ctx.commands.register({
name: "calc",
description: "Simple calculator",
args: ["expression"],
run: ({ expression }) => {
try {
const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, '');
const result = Function(`'use strict'; return (${sanitized})`)();
return `${expression} = ${result}`;
} catch (err) {
return "Invalid expression";
}
}
});
// Usage: /calc 2 + 2 * 3
API Reference
interface CommandsAPI {
register(command: PluginCommand): void;
unregister(name: string): void;
execute(name: string, args: Record<string, any>): Promise<string | void>;
}
interface PluginCommand {
name: string;
description: string;
args?: string[];
run: (args: Record<string, any>) => string | void | Promise<string | void>;
}
Message Interceptors
Intercept and modify messages before they're sent or after they're received.
Before Send
Modify outgoing messages before they're sent to the server.
ctx.messages.onBeforeSend((msg) => {
// Modify msg.content
if (msg.content.includes('secret')) {
msg.content = msg.content.replace(/secret/g, '█████');
}
// Add prefix
msg.content = `[Bot] ${msg.content}`;
});
On Receive
Process incoming messages (read-only monitoring).
ctx.messages.onReceive((msg) => {
// Monitor for keywords
if (msg.content.includes('important')) {
ctx.notify({
title: 'Important Message',
body: msg.content,
type: 'info'
});
}
});
Real-World Examples
Auto-expand abbreviations:
ctx.messages.onBeforeSend((msg) => {
const expansions = {
'brb': 'be right back',
'omw': 'on my way',
'gtg': 'got to go',
'afk': 'away from keyboard'
};
Object.entries(expansions).forEach(([abbr, full]) => {
const regex = new RegExp(`\\b${abbr}\\b`, 'gi');
msg.content = msg.content.replace(regex, full);
});
});
Add auto-shrug to questions:
ctx.messages.onBeforeSend((msg) => {
if (msg.content.trim().endsWith('?')) {
msg.content += ' ¯\\_(ツ)_/¯';
}
});
Track mentions:
ctx.messages.onReceive((msg) => {
const userId = ctx.matrixClient.getUserId();
if (msg.content.includes(userId)) {
ctx.log('You were mentioned!');
ctx.notify('Someone mentioned you!');
}
});
API Reference
interface MessagesAPI {
onBeforeSend(handler: (msg: Message) => void): void;
onReceive(handler: (msg: Message) => void): void;
}
interface Message {
content: string;
roomId?: string;
sender?: string;
eventId?: string;
}
Custom Renderers
Register custom UI renderers for messages, images, and other content types.
// Custom message renderer
ctx.ui.registerRenderer("message", (msg, defaultRenderer) => {
// Custom logic
if (msg.sender?.includes('bot')) {
// Return custom React component
return ctx.React.createElement('div', {
style: { background: 'yellow' }
}, msg.content);
}
// Fall back to default
return defaultRenderer?.();
});
// Custom image renderer
ctx.ui.registerRenderer("image", (data, defaultRenderer) => {
// Add custom effects, borders, etc.
return defaultRenderer?.();
});
API Reference
interface UIAPI {
registerButton(button: UIButtonDefinition): void;
unregisterButton(id: string): void;
registerRenderer(
type: string,
renderer: (data: any, defaultRenderer?: () => ReactNode) => ReactNode
): void;
}
type CustomRenderer = (
data: any,
defaultRenderer?: () => ReactNode
) => ReactNode;
UI: Button Registration
Inject buttons into various parts of the Paarrot UI. Buttons render in two visual styles depending on location — nav list rows (icon + label, full width) or icon buttons (compact, toolbar-style).
See PLUGIN_BUTTON_API.md for the complete reference including positioning and grouping examples.
Register a Button
ctx.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Tool',
icon: '🔧',
onClick: () => ctx.log('clicked!')
});
UI Locations — Nav List Rows
| Location | Where it appears |
|---|---|
channel-list |
Space channel list, below Lobby and Message Search |
home-section |
Home panel, inside the Rooms category above the room list |
direct-messages |
DMs panel, below "Create Chat" and above the CHATS dropdown |
UI Locations — Icon Buttons
| Location | Where it appears |
|---|---|
text-composer-toolbar |
Message composer toolbar (alongside emoji, sticker buttons) |
composer-actions |
Left side of the composer, beside the + attach button |
room-header |
Top room header bar, before the ⋮ menu |
room-menu |
Room ⋮ dropdown menu |
message-actions |
Message hover action bar |
user-menu |
Right-click popup on the user avatar |
search-notification-section |
Notifications page header (right side) |
sidebar-actions |
Left sidebar — above Explore Servers icon, and above Search icon in the sticky bottom section |
Positioning
ctx.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Tool',
icon: '🔧',
position: {
after: 'emoji-picker-button',
group: 'my-tools',
order: 1
},
onClick: () => ctx.log('clicked!')
});
Unregister
ctx.ui.unregisterButton('my-button');
// All buttons are automatically unregistered on plugin unload
API Reference
type UILocation =
| 'channel-list' | 'direct-messages' | 'home-section'
| 'text-composer-toolbar' | 'composer-actions'
| 'room-header' | 'room-menu' | 'message-actions'
| 'user-menu' | 'search-notification-section' | 'sidebar-actions';
interface UIButtonDefinition {
id: string;
location: UILocation;
label: string;
icon?: string;
/** Lucide icon name, e.g. `ImagePlay`. Also supported via `icon: 'lucide:ImagePlay'`. */
lucideIcon?: string;
position?: UIButtonPosition;
onClick?: () => void | Promise<void>;
}
interface UIButtonPosition {
before?: string;
after?: string;
group?: string;
order?: number;
}
Settings
Define settings that users can configure in the UI.
Define Settings Schema
ctx.settings.define({
// Boolean toggle
enableFeature: {
type: "boolean",
label: "Enable Feature",
description: "Toggle this feature on/off",
default: true
},
// String input
apiKey: {
type: "string",
label: "API Key",
description: "Your API key",
default: ""
},
// Number input
maxRetries: {
type: "number",
label: "Max Retries",
description: "Maximum retry attempts",
default: 3
},
// Select dropdown
theme: {
type: "select",
label: "Theme",
description: "Choose a theme",
options: [
{ value: "dark", label: "Dark Mode" },
{ value: "light", label: "Light Mode" }
],
default: "dark"
},
// Color picker
accentColor: {
type: "color",
label: "Accent Color",
description: "Choose your accent color",
default: "#6366f1"
}
});
Get/Set Settings
// Get a setting
const theme = ctx.settings.get('theme');
const enabled = ctx.settings.get('enableFeature');
// Set a setting (usually done by user in UI)
ctx.settings.set('theme', 'dark');
API Reference
interface SettingsAPI {
define(schema: SettingsSchema): void;
get<T = any>(key: string): T | undefined;
set(key: string, value: any): void;
}
interface SettingDefinition {
type: 'string' | 'number' | 'boolean' | 'select' | 'color';
label?: string;
description?: string;
default?: any;
options?: Array<{ value: any; label: string }>;
}
type SettingsSchema = Record<string, SettingDefinition>;
Themes
Register custom themes that appear in the theme selector dropdown. Themes use CSS that's injected once when the plugin loads.
Register a Theme
ctx.themes.register({
id: "my-theme",
name: "My Custom Theme",
kind: "dark", // "light" or "dark"
css: `
body.plugin-my-plugin-my-theme {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
}
.plugin-my-plugin-my-theme #root {
background: linear-gradient(180deg, rgba(22, 33, 62, 0.5) 0%, rgba(15, 52, 96, 0.3) 100%);
}
.plugin-my-plugin-my-theme {
--tc-link: hsl(213, 100%, 80%);
}
`
});
How It Works
- CSS is injected as a
<style>tag when plugin loads - Theme appears in Settings → General → Theme dropdown
- When selected, the class
.plugin-{pluginId}-{themeId}is applied to<body> - Your CSS rules match and style the app
Theme Class Name
Your theme CSS selector will be: .plugin-{pluginId}-{themeId}
Example: plugin ID my-plugin + theme ID dark-blue = .plugin-my-plugin-dark-blue
CSS Custom Properties
Available CSS variables you can override:
--tc-link- Link color--mx-uc-1through--mx-uc-8- Matrix user ID colors (for different users)--font-emoji- Emoji font family--font-secondary- Secondary font family
Styling Elements
Common selectors to style:
body.plugin-{pluginId}-{themeId}- Body background.plugin-{pluginId}-{themeId} #root- Main app container.plugin-{pluginId}-{themeId} [role="navigation"]- Navigation sidebar.plugin-{pluginId}-{themeId} aside- Side panels.plugin-{pluginId}-{themeId} main- Main content area.plugin-{pluginId}-{themeId} header- Headers.plugin-{pluginId}-{themeId} button- Buttons
Examples
Dark Theme with Gradients:
ctx.themes.register({
id: "midnight",
name: "Midnight Blue 🌙",
kind: "dark",
css: `
body.plugin-my-plugin-midnight {
background: linear-gradient(135deg, #0a192f 0%, #112240 50%, #1a365d 100%);
}
.plugin-my-plugin-midnight #root {
background: linear-gradient(180deg, rgba(17, 34, 64, 0.5) 0%, rgba(26, 54, 93, 0.3) 100%);
}
.plugin-my-plugin-midnight [role="navigation"],
.plugin-my-plugin-midnight aside {
background: linear-gradient(180deg, rgba(10, 25, 47, 0.8) 0%, rgba(17, 34, 64, 0.9) 100%);
backdrop-filter: blur(8px);
}
.plugin-my-plugin-midnight main {
background: linear-gradient(135deg, rgba(17, 34, 64, 0.4) 0%, rgba(26, 54, 93, 0.3) 100%);
}
.plugin-my-plugin-midnight header {
background: linear-gradient(90deg, rgba(10, 25, 47, 0.9) 0%, rgba(17, 34, 64, 0.8) 100%);
}
.plugin-my-plugin-midnight {
--tc-link: hsl(213, 100%, 80%);
}
`
});
Light Theme:
ctx.themes.register({
id: "sunrise",
name: "Sunrise ☀️",
kind: "light",
css: `
body.plugin-my-plugin-sunrise {
background: linear-gradient(135deg, #fff4e6 0%, #ffe0b2 50%, #ffcc80 100%);
}
.plugin-my-plugin-sunrise #root {
background: linear-gradient(180deg, rgba(255, 224, 178, 0.5) 0%, rgba(255, 204, 128, 0.3) 100%);
}
.plugin-my-plugin-sunrise [role="navigation"],
.plugin-my-plugin-sunrise aside {
background: linear-gradient(180deg, rgba(255, 244, 230, 0.9) 0%, rgba(255, 224, 178, 0.95) 100%);
backdrop-filter: blur(8px);
}
.plugin-my-plugin-sunrise button:hover {
filter: brightness(0.95);
}
.plugin-my-plugin-sunrise {
--tc-link: hsl(30, 100%, 40%);
}
`
});
Minimal Theme:
ctx.themes.register({
id: "forest",
name: "Forest Green 🌲",
kind: "dark",
css: `
body.plugin-my-plugin-forest {
background: #1a2f23;
}
.plugin-my-plugin-forest {
--tc-link: hsl(140, 60%, 70%);
--mx-uc-1: hsl(140, 60%, 65%);
--mx-uc-2: hsl(150, 60%, 70%);
--mx-uc-3: hsl(130, 60%, 65%);
--mx-uc-4: hsl(160, 60%, 70%);
--mx-uc-5: hsl(120, 60%, 65%);
--mx-uc-6: hsl(145, 60%, 60%);
--mx-uc-7: hsl(135, 60%, 70%);
--mx-uc-8: hsl(155, 60%, 70%);
}
`
});
Unregister Theme
ctx.themes.unregister("my-theme");
API Reference
interface ThemesAPI {
register(theme: PluginTheme): void;
unregister(themeId: string): void;
}
interface PluginTheme {
id: string;
name: string;
kind: 'light' | 'dark';
/** CSS string that will be injected when theme is active */
css: string;
}
Notes:
- CSS is injected once when the plugin loads (not per theme change)
- Theme IDs are automatically prefixed with
plugin-{pluginId}-to avoid conflicts - Theme class names follow the pattern:
.plugin-{pluginId}-{themeId} - Themes appear in Settings → General → Theme selector
- CSS stays in the page until plugin is unloaded
- Themes work exactly like built-in themes (Silver, Dark, Mocha, etc.)
- Use gradients, backdrop-filters, and transitions for polished themes
Matrix Events
Hook into raw Matrix events for advanced functionality.
Listen to Events
// Room messages
ctx.matrix.on("m.room.message", (event) => {
const content = event.getContent();
ctx.log('Message:', content.body);
});
// Membership changes
ctx.matrix.on("m.room.member", (event) => {
const content = event.getContent();
ctx.log('Membership:', content.membership);
});
// Timeline events
ctx.matrix.on("Room.timeline", (event) => {
if (event.getType() === 'm.room.message') {
const sender = event.getSender();
ctx.log('Timeline event from:', sender);
}
});
// Sync state
ctx.matrix.on("sync", (state, prevState) => {
ctx.log('Sync state:', state);
});
Remove Event Handlers
const handler = (event) => {
ctx.log('Event received');
};
ctx.matrix.on("m.room.message", handler);
// Later...
ctx.matrix.off("m.room.message", handler);
API Reference
interface MatrixEventsAPI {
on(eventType: string, handler: (event: MatrixEvent) => void): void;
off(eventType: string, handler: (event: MatrixEvent) => void): void;
}
Common Event Types:
m.room.message- Room messagesm.room.member- Membership changesm.room.name- Room name changesm.room.topic- Room topic changesRoom.timeline- Timeline eventssync- Sync state changes
Background Tasks
Run periodic tasks or delayed actions.
Set Interval
// Run every 30 seconds
const intervalId = ctx.timers.setInterval(() => {
ctx.log('Periodic task running');
// Do something periodically
const stats = {
timestamp: Date.now(),
setting: ctx.settings.get('theme')
};
ctx.log('Stats:', stats);
}, 30000);
// Clear interval
ctx.timers.clearInterval(intervalId);
Set Timeout
// Run once after delay
const timeoutId = ctx.timers.setTimeout(() => {
ctx.notify({
title: 'Reminder',
body: 'This is a delayed notification',
type: 'info'
});
}, 10000); // 10 seconds
// Clear timeout
ctx.timers.clearTimeout(timeoutId);
API Reference
interface TimersAPI {
setInterval(callback: () => void, ms: number): number;
setTimeout(callback: () => void, ms: number): number;
clearInterval(id: number): void;
clearTimeout(id: number): void;
}
Note: Timers are automatically cleaned up when the plugin is unloaded.
Notifications
Show system notifications to the user.
Simple Notification
ctx.notify('Hello, world!');
Detailed Notification
ctx.notify({
title: 'Plugin Alert',
body: 'Something important happened!',
type: 'info', // 'info' | 'success' | 'error' | 'warning'
duration: 5000 // milliseconds (optional)
});
Examples
// Success notification
ctx.notify({
title: 'Success',
body: 'Operation completed successfully!',
type: 'success'
});
// Error notification
ctx.notify({
title: 'Error',
body: 'Something went wrong',
type: 'error'
});
// Warning
ctx.notify({
title: 'Warning',
body: 'This action cannot be undone',
type: 'warning'
});
API Reference
type NotificationFunction = (
options: NotificationOptions | string
) => void;
interface NotificationOptions {
title: string;
body: string;
type?: 'info' | 'success' | 'error' | 'warning';
duration?: number;
}
Logging
Log messages to the console and plugin logs panel.
// Info logs
ctx.log('Plugin started');
ctx.log('User count:', 42);
ctx.log('Complex object:', { foo: 'bar', nested: { value: 123 } });
// Warning logs
ctx.warn('This is a warning');
ctx.warn('Deprecated feature used');
// Error logs
ctx.error('Something went wrong');
ctx.error('Error details:', errorObject);
API Reference
type LogFunction = (...args: any[]) => void;
interface PluginContext {
log: LogFunction; // Info level
warn: LogFunction; // Warning level
error: LogFunction; // Error level
}
Note: All logs are visible in:
- Browser console (F12)
- Plugin logs panel (Settings → Plugins → Logs) - coming soon
Plugin Exports
Export functions and data for other plugins to use.
Define Exports
module.exports = {
// ... onLoad, onUnload ...
exports: {
version: "1.0.0",
utils: {
greet: (name) => `Hello, ${name}!`,
processText: (text) => text.toUpperCase(),
getStats: () => ({
version: "1.0.0",
features: ['commands', 'settings']
})
}
}
};
Use Another Plugin's Exports
// In your plugin
const otherPlugin = ctx.require('other-plugin-id');
if (otherPlugin) {
const greeting = otherPlugin.utils.greet('World');
ctx.log(greeting); // "Hello, World!"
const processed = otherPlugin.utils.processText('hello');
ctx.log(processed); // "HELLO"
const stats = otherPlugin.utils.getStats();
ctx.log('Stats:', stats);
}
API Reference
type RequireFunction = (pluginId: string) => any;
interface Plugin {
name: string;
version: string;
dependencies?: Record<string, string>;
onLoad: (ctx: PluginContext) => Promise<void>;
onUnload?: () => Promise<void>;
exports?: any;
}
Matrix Client
Direct access to the Matrix.js SDK for advanced operations.
// Get user info
const userId = ctx.matrixClient.getUserId();
const displayName = ctx.matrixClient.getUser(userId)?.displayName;
// Get rooms
const rooms = ctx.matrixClient.getRooms();
ctx.log(`You're in ${rooms.length} rooms`);
// Get specific room
const room = ctx.matrixClient.getRoom(roomId);
if (room) {
const name = room.name;
const members = room.getMembers();
ctx.log(`Room: ${name}, Members: ${members.length}`);
}
// Sync state
const syncState = ctx.matrixClient.getSyncState();
ctx.log('Sync state:', syncState);
// Send custom event
await ctx.matrixClient.sendEvent(roomId, 'm.room.message', {
msgtype: 'm.text',
body: 'Hello from plugin!'
});
API Reference
interface PluginContext {
matrixClient: MatrixClient; // Full Matrix.js SDK client
}
For full Matrix.js SDK documentation, see: https://matrix-org.github.io/matrix-js-sdk/
Complete Example
Here's a complete plugin that demonstrates all features:
/**
* Example Plugin - All Features
*/
module.exports = {
name: "Example Plugin",
version: "1.0.0",
dependencies: {},
onLoad: async (ctx) => {
ctx.log('🚀 Plugin loading...');
// === COMMANDS ===
ctx.commands.register({
name: "hello",
description: "Say hello",
run: () => "Hello, world!"
});
ctx.commands.register({
name: "greet",
description: "Greet someone",
args: ["name"],
run: ({ name }) => `Hello, ${name}!`
});
// === INTERCEPTORS ===
ctx.messages.onBeforeSend((msg) => {
// Auto-expand abbreviations
msg.content = msg.content.replace(/\bbrb\b/gi, 'be right back');
// Add emoji if enabled
if (ctx.settings.get('addEmoji')) {
msg.content += ' ✨';
}
});
ctx.messages.onReceive((msg) => {
// Track keywords
const keywords = ctx.settings.get('keywords') || '';
keywords.split(',').forEach(keyword => {
if (msg.content.includes(keyword.trim())) {
ctx.notify(`Keyword detected: ${keyword}`);
}
});
});
// === SETTINGS ===
ctx.settings.define({
addEmoji: {
type: "boolean",
label: "Add Emoji",
description: "Add sparkles to messages",
default: false
},
keywords: {
type: "string",
label: "Track Keywords",
description: "Comma-separated keywords to track",
default: "important,urgent"
},
theme: {
type: "select",
label: "Theme",
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" }
],
default: "dark"
}
});
// === THEMES ===
ctx.themes.register({
id: "example-theme",
name: "Example Theme",
kind: "dark",
colors: {
background: "#1a1a2e",
surface: "#16213e",
primary: "#0f3460",
secondary: "#533483",
onBackground: "#ffffff",
onSurface: "#e0e0e0"
}
});
// === MATRIX EVENTS ===
ctx.matrix.on("m.room.message", (event) => {
const content = event.getContent();
if (content.body?.includes('example')) {
ctx.log('Keyword found in message');
}
});
// === BACKGROUND TASKS ===
ctx.timers.setInterval(() => {
ctx.log('Periodic check');
}, 60000); // Every minute
// === NOTIFICATIONS ===
ctx.notify({
title: 'Example Plugin',
body: 'Plugin loaded successfully!',
type: 'success'
});
// === EXPORTS ===
this.exports = {
version: "1.0.0",
greet: (name) => `Hello, ${name}!`
};
ctx.log('✅ Plugin loaded');
},
onUnload: async () => {
console.log('Plugin unloaded');
},
exports: null
};
Best Practices
Error Handling
Always wrap plugin code in try-catch:
onLoad: async (ctx) => {
try {
// Your code here
} catch (error) {
ctx.error('Plugin error:', error);
ctx.notify({
title: 'Plugin Error',
body: error.message,
type: 'error'
});
}
}
Cleanup
Clean up resources in onUnload:
onUnload: async () => {
// Timers are auto-cleared
// Event handlers are auto-removed
// Settings are persisted automatically
console.log('Plugin unloaded');
}
Settings Validation
Validate settings before use:
const maxRetries = ctx.settings.get('maxRetries') || 3;
const keywords = (ctx.settings.get('keywords') || '').split(',').filter(k => k.trim());
Logging
Use appropriate log levels:
ctx.log('Info message'); // Normal operation
ctx.warn('Warning message'); // Potential issues
ctx.error('Error message'); // Actual errors
Performance
- Use intervals sparingly (30s+ recommended)
- Avoid heavy processing in message interceptors
- Cache expensive computations
- Clean up timers when not needed
Troubleshooting
Plugin Won't Load
- Check console for errors (F12)
- Verify
plugin-metadata.jsonexists - Check for JavaScript syntax errors
- Ensure
onLoadfunction exists
Commands Not Working
- Ensure plugin is enabled
- Check console for "Commands registered" message
- Verify command name doesn't conflict
- Try
/testcommand first
Settings Not Showing
- Check settings schema is defined
- Verify setting types are valid
- Look for JavaScript errors in console
Interceptors Not Working
- Check plugin is enabled
- Verify interceptors are registered in
onLoad - Check console logs for interceptor messages
Publishing Your Plugin
To publish your plugin to the Paarrot Plugin Directory:
- Create a GitHub repository
- Add these files:
index.jsplugin-metadata.jsonREADME.md
- Create a ZIP file:
your-plugin.zip - Submit to: https://github.com/Paarrot/Plugin-Directory
Support
- Documentation: https://github.com/Paarrot/cinny-desktop/blob/main/PLUGIN_API.md
- Examples: https://github.com/Paarrot/cinny-desktop/tree/main/example-plugin
- Issues: https://github.com/Paarrot/cinny-desktop/issues
- Community: https://matrix.to/#/#paarrot:matrix.org
License
MIT License - See LICENSE file for details