README
¶
marchat Plugin System
The marchat plugin system provides a modular, extensible architecture for adding functionality to the chat application. Plugins are external binaries that communicate with marchat via JSON over stdin/stdout.
Architecture Overview
Plugin Communication
- Plugins run as isolated subprocesses
- Communication via JSON over stdin/stdout
- Headless-first design with optional TUI extensions
- Graceful failure - plugins cannot crash the main app
Plugin Lifecycle
- Discovery: Plugins are discovered in the plugin directory
- Loading: Plugin manifest is parsed and validated
- Initialization: Plugin receives configuration and user list
- Runtime: Plugin processes messages and commands
- Shutdown: Plugin receives shutdown signal and exits gracefully
Plugin Structure
Each plugin must have the following structure:
myplugin/
├── plugin.json # Plugin manifest
├── myplugin # Binary executable
└── README.md # Optional documentation
Plugin Manifest (plugin.json)
{
"name": "myplugin",
"version": "1.0.0",
"description": "A description of what this plugin does",
"author": "Your Name",
"license": "MIT",
"repository": "https://github.com/user/myplugin",
"commands": [
{
"name": "mycommand",
"description": "Description of the command",
"usage": ":mycommand <args>",
"admin_only": false
}
],
"permissions": [],
"settings": {},
"min_version": "0.1.0"
}
Plugin SDK
Running SDK tests (nested module)
plugin/sdk has its own go.mod, so the repo root go test ./... command does not execute its tests. From the repository root:
cd plugin/sdk
go test ./...
The sample under plugin/examples/echo is also a small standalone module (cd plugin/examples/echo && go test ./...); it may report [no test files]. Full-suite notes and merged coverage for the main module are in TESTING.md.
Core Interface
type Plugin interface {
Name() string
Init(Config) error
OnMessage(Message) ([]Message, error)
Commands() []PluginCommand
}
Message Type
The sdk.Message struct carries chat context from the hub to plugins and back:
type Message struct {
Sender string `json:"sender"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
Type string `json:"type,omitempty"`
Channel string `json:"channel,omitempty"`
Encrypted bool `json:"encrypted,omitempty"`
MessageID int64 `json:"message_id,omitempty"`
Recipient string `json:"recipient,omitempty"`
Edited bool `json:"edited,omitempty"`
}
| Field | Description |
|---|---|
Sender |
Username of the message author |
Content |
Message text (opaque ciphertext when Encrypted is true) |
CreatedAt |
Timestamp |
Type |
"text", "file", "dm", etc. (matches shared.MessageType values) |
Channel |
Channel the message belongs to (empty = default general) |
Encrypted |
true when content is E2E encrypted; plugins should skip parsing |
MessageID |
Server-assigned ID (useful for reactions, edits) |
Recipient |
Target user for DMs (empty = broadcast) |
Edited |
true if the message was edited after send |
Backwards compatibility: All extended fields use omitempty. Plugins compiled against older SDK versions silently ignore unknown JSON keys and omit them on output; no recompile required.
Message routing rules:
- The hub only forwards messages with
typeset to"text"to plugins. Other types (typing, reactions, etc.) are not delivered. - Host behavior: The server never blocks its hub on plugin stdin. Each running plugin has a bounded outbound queue (64 messages by default in
plugin/host); if a plugin falls behind, new chat fan-out lines may be dropped (logged server-side). Fan-out is best-effort and at most once per plugin per message (no host retry). Plugins should return quickly fromOnMessageand avoid blocking the stdio read loop. - Plugin replies that omit
type(or set it to anything other than"text") are broadcast to clients but are not re-forwarded to other plugins. This prevents accidental infinite loops. - To opt into plugin-to-plugin chaining, set
Type: "text"on outboundsdk.Messageexplicitly. Use with care: the echo plugin, for example, should not do this or it will loop. - Encrypted messages: The hub does not filter encrypted messages before forwarding to plugins. Plugins receive them with
Encrypted: trueand opaqueContent. Plugins that parseContentshould checkmsg.Encryptedand skip or handle accordingly.
Message Processing
Plugins receive messages and can respond with additional messages:
func (p *MyPlugin) OnMessage(msg sdk.Message) ([]sdk.Message, error) {
// Skip encrypted messages the plugin cannot read
if msg.Encrypted {
return nil, nil
}
// Process incoming message
if strings.HasPrefix(msg.Content, "hello") {
response := sdk.Message{
Sender: "MyBot",
Content: "Hello back!",
CreatedAt: time.Now(),
Channel: msg.Channel, // reply in the same channel
}
return []sdk.Message{response}, nil
}
return nil, nil
}
Command Registration
Plugins can register commands that users can invoke:
func (p *MyPlugin) Commands() []sdk.PluginCommand {
return []sdk.PluginCommand{
{
Name: "greet",
Description: "Send a greeting",
Usage: ":greet <name>",
AdminOnly: false,
},
}
}
Plugin Communication Protocol
Request Format
{
"type": "message|command|init|shutdown",
"command": "command_name",
"data": {}
}
Response Format
{
"type": "message|log",
"success": true,
"data": {},
"error": "error message"
}
Message Types
- init: Plugin initialization with config and user list
- message: Incoming chat message
- command: Plugin command execution
- shutdown: Graceful shutdown request
Plugin Development
Getting Started
-
Create plugin directory:
mkdir myplugin cd myplugin -
Create plugin.json:
{ "name": "myplugin", "version": "1.0.0", "description": "My first plugin", "author": "Your Name", "license": "MIT", "commands": [] } -
Create main.go:
package main import ( "log" "github.com/Cod-e-Codes/marchat/plugin/sdk" ) type MyPlugin struct { *sdk.BasePlugin } func NewMyPlugin() *MyPlugin { return &MyPlugin{ BasePlugin: sdk.NewBasePlugin("myplugin"), } } func (p *MyPlugin) Init(config sdk.Config) error { return nil } func (p *MyPlugin) OnMessage(msg sdk.Message) ([]sdk.Message, error) { return nil, nil } func (p *MyPlugin) Commands() []sdk.PluginCommand { return nil } func main() { plugin := NewMyPlugin() if err := sdk.RunStdio(plugin, plugin.handleCommand); err != nil { log.Fatalf("plugin exited: %v", err) } } // handleCommand handles PluginRequest type "command" (chat :commands). // init, message, and shutdown are handled by sdk.HandlePluginRequest via RunStdio. func (p *MyPlugin) handleCommand(command string, args []string) sdk.PluginResponse { _ = args return sdk.PluginResponse{ Type: "command", Success: false, Error: "unknown command", } } -
Build the plugin:
go build -o myplugin main.go -
Install the plugin:
# Copy to plugin directory cp myplugin /path/to/marchat/plugins/myplugin/ cp plugin.json /path/to/marchat/plugins/myplugin/
Plugin Configuration
Plugins receive configuration during initialization:
type Config struct {
PluginDir string // Plugin directory path
DataDir string // Plugin data directory
Settings map[string]string // Plugin settings
}
Plugin Data Storage
Plugins can store data in their data directory:
import (
"encoding/json"
"os"
"path/filepath"
)
func (p *MyPlugin) saveData(data interface{}) error {
dataFile := filepath.Join(p.GetConfig().DataDir, "data.json")
b, err := json.Marshal(data)
if err != nil {
return err
}
return os.WriteFile(dataFile, b, 0644)
}
Plugin Management
Installation
Plugins can be installed via:
-
Chat commands:
:install myplugin -
Plugin store:
:store -
Manual installation:
- Copy plugin files to plugin directory
- Restart marchat or use
:plugin enable myplugin
Plugin Commands
:plugin list- List installed plugins:plugin enable <name>- Enable a plugin:plugin disable <name>- Disable a plugin:plugin uninstall <name>- Uninstall a plugin (admin only):store- Open plugin store:refresh- Refresh plugin store
Plugin Store
The plugin store provides a TUI interface for browsing and installing plugins:
- Browse plugins by category, tags, or search
- View plugin details including description, commands, and metadata
- Install plugins with one-click installation
- Manage installed plugins enable/disable/update
Official Plugins and Licensing
License Validation
Official (paid) plugins require license validation:
- License file:
.licensefile in plugin directory - Cryptographic verification: Ed25519 signature validation
- Offline support: Licenses cached after first validation; the server re-verifies the signature (and that
plugin_namematches the cache key) whenever it reads the cache, and drops invalid cache files
License Management
Use the marchat-license CLI tool:
# Generate key pair
marchat-license -action genkey
# Generate license
marchat-license -action generate \
-plugin myplugin \
-customer CUSTOMER123 \
-expires 2024-12-31 \
-private-key <private-key> \
-output myplugin.license
# Validate license
marchat-license -action validate \
-license myplugin.license \
-public-key <public-key>
# Check license status
marchat-license -action check \
-plugin myplugin \
-public-key <public-key>
Community Plugin Registry
Registry Format
The community registry is a JSON file hosted on GitHub:
[
{
"name": "myplugin",
"version": "1.0.0",
"description": "A community plugin",
"author": "Community Member",
"license": "MIT",
"repository": "https://github.com/user/myplugin",
"download_url": "https://github.com/user/myplugin/releases/latest/download/myplugin.zip",
"checksum": "sha256:...",
"category": "utility",
"tags": ["chat", "utility"],
"commands": [...]
}
]
Submitting Plugins
- Create plugin following the structure above
- Host plugin on GitHub/GitLab with releases
- Submit PR to the community registry
- Include metadata in registry entry
Registry URL
The default registry URL is:
https://raw.githubusercontent.com/Cod-e-Codes/marchat-plugins/main/registry.json
Best Practices
Plugin Development
- Fail gracefully: Never crash the main application
- Use BasePlugin: Extend
sdk.BasePluginfor common functionality - Validate input: Always validate user input and plugin data
- Log appropriately: Use stderr for logging, stdout for responses
- Handle errors: Return meaningful error messages
- Test thoroughly: Test with various inputs and edge cases
Security Considerations
- Input validation: Validate all user input
- Resource limits: Don't consume excessive resources
- File operations: Use plugin data directory for file operations
- Network access: Document any network access requirements
- Permissions: Request only necessary permissions
Performance Guidelines
- Async operations: Use goroutines for long-running operations
- Memory usage: Be mindful of memory consumption
- Response time: Respond quickly to avoid blocking the chat
- Caching: Cache frequently accessed data
- Cleanup: Clean up resources on shutdown
Example Plugins
Echo Plugin
A simple echo plugin that repeats messages:
func (p *EchoPlugin) OnMessage(msg sdk.Message) ([]sdk.Message, error) {
if strings.HasPrefix(msg.Content, "echo:") {
response := sdk.Message{
Sender: "EchoBot",
Content: strings.TrimPrefix(msg.Content, "echo:"),
CreatedAt: time.Now(),
}
return []sdk.Message{response}, nil
}
return nil, nil
}
Weather Plugin
A weather plugin that responds to weather queries:
func (p *WeatherPlugin) OnMessage(msg sdk.Message) ([]sdk.Message, error) {
if strings.HasPrefix(msg.Content, "weather:") {
location := strings.TrimPrefix(msg.Content, "weather:")
weather := p.getWeather(location)
response := sdk.Message{
Sender: "WeatherBot",
Content: fmt.Sprintf("Weather in %s: %s", location, weather),
CreatedAt: time.Now(),
}
return []sdk.Message{response}, nil
}
return nil, nil
}
Troubleshooting
Common Issues
- Plugin not loading: Check plugin.json format and binary permissions
- Plugin not responding: Check JSON communication format
- Permission denied: Ensure plugin binary is executable
- License validation failed: Check license file and public key
- Plugin crashes: Check plugin logs in stderr
Debugging
- Enable debug logging: Set log level to debug
- Check plugin logs: Plugin stderr is logged by marchat
- Test communication: Use test harness for plugin communication
- Validate JSON: Ensure JSON format is correct
- Check permissions: Verify file and directory permissions
Getting Help
- Documentation: Check this README and code comments
- Examples: Review example plugins in
plugin/examples/ - Issues: Report bugs on GitHub
- Discussions: Ask questions in GitHub Discussions
- Community: Join the marchat community
License
The plugin system is part of marchat and is licensed under the MIT License. Individual plugins may have their own licenses.