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
Core Interface
type Plugin interface {
Name() string
Init(Config) error
OnMessage(Message) ([]Message, error)
Commands() []PluginCommand
}
Message Processing
Plugins receive messages and can respond with additional messages:
func (p *MyPlugin) OnMessage(msg sdk.Message) ([]sdk.Message, error) {
// Process incoming message
if strings.HasPrefix(msg.Content, "hello") {
response := sdk.Message{
Sender: "MyBot",
Content: "Hello back!",
CreatedAt: time.Now(),
}
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 ( "encoding/json" "fmt" "os" "time" "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() decoder := json.NewDecoder(os.Stdin) encoder := json.NewEncoder(os.Stdout) for { var req sdk.PluginRequest if err := decoder.Decode(&req); err != nil { break } response := handleRequest(plugin, req) encoder.Encode(response) } } func handleRequest(plugin *MyPlugin, req sdk.PluginRequest) sdk.PluginResponse { // Handle different request types switch req.Type { case "init": // Handle initialization case "message": // Handle incoming message case "command": // Handle command execution case "shutdown": // Handle shutdown } return sdk.PluginResponse{ Type: req.Type, Success: true, } }
-
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:
func (p *MyPlugin) saveData(data interface{}) error {
dataFile := filepath.Join(p.config.DataDir, "data.json")
return os.WriteFile(dataFile, data, 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:
.license
file in plugin directory - Cryptographic verification: Ed25519 signature validation
- Offline support: Licenses cached after first validation
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.BasePlugin
for 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.