The proj plugin system allows you to extend the functionality of the project navigator with custom actions, language detectors, and more. Plugins communicate with the main application via JSON-RPC 2.0 over stdin/stdout.
Plugins use JSON-RPC 2.0 for communication:
- Transport: stdin/stdout
- Format: Newline-delimited JSON
- Methods:
init,actions,executeAction,languages,shutdown
- Discovery: Plugin directories are scanned in
~/.config/proj/plugins/ - Loading: Manifest (
plugin.json) is read - Initialization: Plugin executable is started
- Init Call:
initmethod is called with configuration - Operation: Plugin responds to method calls
- Shutdown:
shutdownmethod is called, process exits
~/.config/proj/plugins/
└── my-plugin/
├── plugin.json # Manifest
└── my-plugin # Executable (any language)
For development, plugins can also be placed in the project's plugins/ directory.
Create a plugin.json file:
{
"name": "my-plugin",
"version": "1.0.0",
"description": "My custom plugin",
"executable": "my-plugin",
"capabilities": ["actions"],
"config": {}
}Fields:
name(required): Unique plugin identifierversion(required): Semantic versiondescription(optional): Human-readable descriptionexecutable(required): Name of the executable filecapabilities(required): Array of capabilities (actions,languages)config(optional): Default configuration
Your plugin must implement these methods:
Params:
{
"config": {
"key": "value"
}
}Response:
{
"success": true
}Params:
{
"name": "project-name",
"path": "/path/to/project",
"language": "Go",
"gitBranch": "main",
"gitDirty": false,
"isGitRepo": true
}Response:
[
{
"id": "my-action",
"label": "My Action",
"description": "Does something useful",
"icon": "🚀",
"priority": 100
}
]Params:
{
"action": "my-action",
"project": {
"name": "project-name",
"path": "/path/to/project",
"language": "Go",
"gitBranch": "main",
"gitDirty": false,
"isGitRepo": true
}
}Response:
{
"success": true,
"message": "Action completed successfully",
"cdPath": "",
"execCmd": []
}Response Fields:
success: Whether the action succeededmessage: Message to display to usercdPath: (Optional) Path to change to and exitexecCmd: (Optional) Command to exec and replace shell
Params: None
Response:
{
"success": true
}See plugins/example/main.go for a complete working example.
Edit ~/.config/proj/config.json:
{
"plugins": {
"enabled": ["my-plugin"],
"config": {
"my-plugin": {
"key": "value"
}
}
}
}Plugin-specific configuration is passed during the init call. Access it from the config parameter.
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
)
type RPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
ID int `json:"id"`
}
type RPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
ID int `json:"id"`
}
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
var req RPCRequest
json.Unmarshal(scanner.Bytes(), &req)
resp := handleRequest(&req)
respData, _ := json.Marshal(resp)
fmt.Println(string(respData))
}
}#!/usr/bin/env python3
import sys
import json
def handle_request(req):
method = req.get('method')
params = req.get('params', {})
if method == 'init':
return {'success': True}
elif method == 'actions':
return [{
'id': 'my-action',
'label': 'My Action',
'description': 'Does something',
'icon': '🚀',
'priority': 100
}]
elif method == 'executeAction':
return {
'success': True,
'message': 'Action executed'
}
return None
for line in sys.stdin:
req = json.loads(line)
result = handle_request(req)
resp = {
'jsonrpc': '2.0',
'result': result,
'id': req.get('id')
}
print(json.dumps(resp))
sys.stdout.flush()#!/usr/bin/env node
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function handleRequest(req) {
const { method, params } = req;
if (method === 'init') {
return { success: true };
} else if (method === 'actions') {
return [{
id: 'my-action',
label: 'My Action',
description: 'Does something',
icon: '🚀',
priority: 100
}];
} else if (method === 'executeAction') {
return {
success: true,
message: 'Action executed'
};
}
return null;
}
rl.on('line', (line) => {
const req = JSON.parse(line);
const result = handleRequest(req);
const resp = {
jsonrpc: '2.0',
result,
id: req.id
};
console.log(JSON.stringify(resp));
});- Place plugin in
~/.config/proj/plugins/my-plugin/ - Enable in config:
proj --config - Run
projand verify actions appear - Test plugin executable directly:
echo '{"jsonrpc":"2.0","method":"init","params":{"config":{}},"id":1}' | ./my-pluginCreate test projects and verify plugin actions appear correctly in the TUI.
- Error Handling: Always return proper JSON-RPC errors
- Performance: Keep action detection fast (<100ms)
- Resource Cleanup: Implement shutdown properly
- Logging: Write debug info to stderr, not stdout
- Versioning: Use semantic versioning
- Security: Validate all inputs
- Documentation: Document configuration options
Plugins with the actions capability can:
- Provide custom actions for projects
- Execute commands
- Trigger directory changes
- Replace the shell with a new command
Plugins with the languages capability can:
- Detect custom programming languages
- Provide language-specific metadata
- Check
plugin.jsonis valid JSON - Verify executable has execute permissions:
chmod +x my-plugin - Enable plugin in config:
"enabled": ["my-plugin"] - Check stderr for error messages
- Verify plugin returns actions in
actionsmethod - Check action structure matches expected format
- Ensure
actionscapability is declared
- Check stderr for error messages
- Test plugin executable directly
- Verify JSON-RPC responses are valid
- Check for resource leaks (file descriptors, etc.)
{
name: string
path: string
language: string
gitBranch: string
gitDirty: boolean
isGitRepo: boolean
}{
id: string
label: string
description: string
icon: string
priority: number
}{
success: boolean
message: string
cdPath?: string
execCmd?: string[]
}See plugins/example/ for a complete working example that demonstrates:
- JSON-RPC request handling
- Action detection
- Action execution
- Configuration handling
- Proper shutdown