Add comprehensive plugin specification and development guide
This commit is contained in:
759
docs/PLUGIN_SPEC.md
Normal file
759
docs/PLUGIN_SPEC.md
Normal file
@@ -0,0 +1,759 @@
|
||||
# 🔌 Plugin Specification - Extension Development Guide
|
||||
|
||||
## Philosophy: Everything is a Plugin
|
||||
|
||||
**Core Principle:** If core features (Chat, Channels, Notes) are implemented as plugins, we prove that ANY feature can be a plugin.
|
||||
|
||||
```
|
||||
Minimal Core (Go) Extensions (Any Language)
|
||||
├── HTTP Router ├── Chat Engine (Python)
|
||||
├── WebSocket Hub ├── Channels (Go)
|
||||
├── Extension Manager ├── RAG Engine (Python)
|
||||
├── Auth/Users ├── Workflows (Go + Python)
|
||||
└── PostgreSQL └── Your Custom Plugin...
|
||||
```
|
||||
|
||||
## Plugin Types
|
||||
|
||||
### 1. Frontend Plugins (UI Only)
|
||||
**Language:** JavaScript
|
||||
**Runs:** In browser
|
||||
**Capabilities:** UI modifications, client-side logic
|
||||
**Modes:** Both managed and unmanaged
|
||||
|
||||
### 2. Backend Plugins (Full Power)
|
||||
**Language:** Python, Go, Node.js, Rust, etc.
|
||||
**Runs:** As separate process
|
||||
**Capabilities:** Tools, models, data processing, external APIs
|
||||
**Modes:** Managed only
|
||||
|
||||
## Frontend Plugin API
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
extensions/frontend/token-counter/
|
||||
├── manifest.json # Plugin metadata
|
||||
├── main.js # Entry point
|
||||
├── styles.css # Optional styles
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### manifest.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "token-counter",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "Display token count for messages",
|
||||
"type": "frontend",
|
||||
"entry": "main.js",
|
||||
"permissions": [
|
||||
"message:read",
|
||||
"ui:inject"
|
||||
],
|
||||
"hooks": [
|
||||
"onMessageSend",
|
||||
"onMessageReceive",
|
||||
"onUIRender"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### main.js Example
|
||||
|
||||
```javascript
|
||||
// Frontend plugin template
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Plugin initialization
|
||||
window.ChatSwitchboard = window.ChatSwitchboard || {};
|
||||
window.ChatSwitchboard.plugins = window.ChatSwitchboard.plugins || [];
|
||||
|
||||
const TokenCounter = {
|
||||
name: 'token-counter',
|
||||
version: '1.0.0',
|
||||
|
||||
// Called when plugin loads
|
||||
init: function() {
|
||||
console.log('Token Counter plugin loaded');
|
||||
this.addUI();
|
||||
},
|
||||
|
||||
// Hook: Before message is sent
|
||||
hooks: {
|
||||
onMessageSend: function(message) {
|
||||
const tokens = this.estimateTokens(message.content);
|
||||
console.log(`Message has ~${tokens} tokens`);
|
||||
|
||||
// Show warning if too long
|
||||
if (tokens > 4000) {
|
||||
window.ChatSwitchboard.showToast(
|
||||
`⚠️ Long message: ${tokens} tokens`,
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
// Can modify message before sending
|
||||
return message;
|
||||
},
|
||||
|
||||
onMessageReceive: function(message) {
|
||||
// Process incoming messages
|
||||
return message;
|
||||
},
|
||||
|
||||
onUIRender: function() {
|
||||
// Inject UI elements
|
||||
this.updateTokenDisplay();
|
||||
}
|
||||
},
|
||||
|
||||
// Add token counter to UI
|
||||
addUI: function() {
|
||||
const inputArea = document.querySelector('.input-area');
|
||||
const counter = document.createElement('div');
|
||||
counter.id = 'token-counter';
|
||||
counter.className = 'plugin-token-counter';
|
||||
counter.textContent = '0 tokens';
|
||||
inputArea.appendChild(counter);
|
||||
|
||||
// Update on input
|
||||
const textarea = document.getElementById('messageInput');
|
||||
textarea.addEventListener('input', () => {
|
||||
const tokens = this.estimateTokens(textarea.value);
|
||||
counter.textContent = `${tokens} tokens`;
|
||||
});
|
||||
},
|
||||
|
||||
// Token estimation (rough approximation)
|
||||
estimateTokens: function(text) {
|
||||
return Math.ceil(text.length / 4);
|
||||
},
|
||||
|
||||
updateTokenDisplay: function() {
|
||||
const counter = document.getElementById('token-counter');
|
||||
const input = document.getElementById('messageInput');
|
||||
if (counter && input) {
|
||||
const tokens = this.estimateTokens(input.value);
|
||||
counter.textContent = `${tokens} tokens`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Register plugin
|
||||
window.ChatSwitchboard.plugins.push(TokenCounter);
|
||||
|
||||
// Auto-init when DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => TokenCounter.init());
|
||||
} else {
|
||||
TokenCounter.init();
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
### Available Hooks
|
||||
|
||||
```javascript
|
||||
// Message lifecycle
|
||||
onMessageSend(message) // Before sending to API
|
||||
onMessageReceive(message) // After receiving from API
|
||||
onMessageRender(element, message) // When rendering to DOM
|
||||
|
||||
// UI lifecycle
|
||||
onUIRender() // After UI updates
|
||||
onChatSwitch(chatId) // When switching chats
|
||||
onModelChange(model) // When model changes
|
||||
|
||||
// Extension points
|
||||
onToolCall(tool, params) // Before calling tool
|
||||
onToolResult(tool, result) // After tool returns
|
||||
|
||||
// Settings
|
||||
onSettingsOpen() // Settings modal opened
|
||||
onSettingsSave(settings) // Settings saved
|
||||
|
||||
// Files
|
||||
onFileUpload(file) // File uploaded
|
||||
onFileSelect(file) // File selected for chat
|
||||
```
|
||||
|
||||
### Plugin API Methods
|
||||
|
||||
```javascript
|
||||
// Toast notifications
|
||||
ChatSwitchboard.showToast(message, type); // type: success, error, warning, info
|
||||
|
||||
// Storage (scoped to plugin)
|
||||
ChatSwitchboard.storage.set(key, value);
|
||||
ChatSwitchboard.storage.get(key);
|
||||
ChatSwitchboard.storage.remove(key);
|
||||
|
||||
// UI manipulation
|
||||
ChatSwitchboard.ui.addButton(location, config);
|
||||
ChatSwitchboard.ui.addPanel(config);
|
||||
ChatSwitchboard.ui.addMenuItem(config);
|
||||
|
||||
// State access (read-only)
|
||||
ChatSwitchboard.state.getCurrentChat();
|
||||
ChatSwitchboard.state.getCurrentModel();
|
||||
ChatSwitchboard.state.getSettings();
|
||||
|
||||
// Events
|
||||
ChatSwitchboard.events.on(event, callback);
|
||||
ChatSwitchboard.events.off(event, callback);
|
||||
ChatSwitchboard.events.emit(event, data);
|
||||
```
|
||||
|
||||
## Backend Plugin API
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
extensions/backend/web-search/
|
||||
├── extension.json # Manifest
|
||||
├── main.py # Entry point (or main.go, server.js, etc)
|
||||
├── requirements.txt # Dependencies (Python)
|
||||
├── config.yaml # Configuration
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### extension.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "web-search",
|
||||
"version": "1.0.0",
|
||||
"author": "Chat Switchboard Team",
|
||||
"description": "Search the web using DuckDuckGo API",
|
||||
"type": "backend",
|
||||
"runtime": "python",
|
||||
"entry": "main.py",
|
||||
"port": 9001,
|
||||
"capabilities": ["tool"],
|
||||
"dependencies": {
|
||||
"python": ">=3.9",
|
||||
"packages": ["fastapi", "uvicorn", "duckduckgo-search"]
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"name": "search_web",
|
||||
"description": "Search the web for information",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query"
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum results to return",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
},
|
||||
"returns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
"snippet": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"max_requests_per_minute": 30,
|
||||
"timeout_seconds": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python Plugin Template
|
||||
|
||||
```python
|
||||
# extensions/backend/web-search/main.py
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from duckduckgo_search import DDGS
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
app = FastAPI(title="Web Search Plugin")
|
||||
|
||||
# Load config
|
||||
PORT = int(os.getenv("PLUGIN_PORT", 9001))
|
||||
MAX_RESULTS = int(os.getenv("MAX_RESULTS", 5))
|
||||
|
||||
# Request/response models
|
||||
class SearchRequest(BaseModel):
|
||||
query: str
|
||||
max_results: int = 5
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
title: str
|
||||
url: str
|
||||
snippet: str
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
results: list[SearchResult]
|
||||
|
||||
# Tool endpoint
|
||||
@app.post("/tools/search_web", response_model=SearchResponse)
|
||||
async def search_web(request: SearchRequest):
|
||||
"""Search the web using DuckDuckGo"""
|
||||
try:
|
||||
ddgs = DDGS()
|
||||
results = ddgs.text(
|
||||
request.query,
|
||||
max_results=min(request.max_results, MAX_RESULTS)
|
||||
)
|
||||
|
||||
return SearchResponse(
|
||||
results=[
|
||||
SearchResult(
|
||||
title=r.get("title", ""),
|
||||
url=r.get("href", ""),
|
||||
snippet=r.get("body", "")
|
||||
)
|
||||
for r in results
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Health check
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "plugin": "web-search"}
|
||||
|
||||
# Plugin info
|
||||
@app.get("/info")
|
||||
async def info():
|
||||
return {
|
||||
"name": "web-search",
|
||||
"version": "1.0.0",
|
||||
"tools": ["search_web"]
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=PORT)
|
||||
```
|
||||
|
||||
### Go Plugin Template
|
||||
|
||||
```go
|
||||
// extensions/backend/file-ops/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type ReadFileRequest struct {
|
||||
Path string `json:"path" binding:"required"`
|
||||
}
|
||||
|
||||
type ReadFileResponse struct {
|
||||
Content string `json:"content"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// Tool endpoint
|
||||
r.POST("/tools/read_file", func(c *gin.Context) {
|
||||
var req ReadFileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
info, _ := os.Stat(req.Path)
|
||||
|
||||
c.JSON(200, ReadFileResponse{
|
||||
Content: string(content),
|
||||
Size: info.Size(),
|
||||
})
|
||||
})
|
||||
|
||||
// Health check
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Start server
|
||||
port := os.Getenv("PLUGIN_PORT")
|
||||
if port == "" {
|
||||
port = "9002"
|
||||
}
|
||||
r.Run(":" + port)
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Manager (Backend Core)
|
||||
|
||||
### Go Extension Manager
|
||||
|
||||
```go
|
||||
// server/extensions/manager.go
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Runtime string `json:"runtime"`
|
||||
Entry string `json:"entry"`
|
||||
Port int `json:"port"`
|
||||
Tools []Tool `json:"tools"`
|
||||
Process *os.Process
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
extensions map[string]*Extension
|
||||
tools map[string]*Tool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
extensions: make(map[string]*Extension),
|
||||
tools: make(map[string]*Tool),
|
||||
}
|
||||
}
|
||||
|
||||
// Load all extensions from directory
|
||||
func (m *Manager) LoadExtensions(dir string) error {
|
||||
entries, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
extPath := filepath.Join(dir, entry.Name())
|
||||
if err := m.LoadExtension(extPath); err != nil {
|
||||
log.Printf("Failed to load extension %s: %v", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load single extension
|
||||
func (m *Manager) LoadExtension(path string) error {
|
||||
// Read manifest
|
||||
manifestPath := filepath.Join(path, "extension.json")
|
||||
data, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ext Extension
|
||||
if err := json.Unmarshal(data, &ext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start extension process
|
||||
if err := m.startExtension(&ext, path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Register tools
|
||||
for i := range ext.Tools {
|
||||
ext.Tools[i].ExtensionName = ext.Name
|
||||
m.tools[ext.Tools[i].Name] = &ext.Tools[i]
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.extensions[ext.Name] = &ext
|
||||
m.mu.Unlock()
|
||||
|
||||
log.Printf("✅ Loaded extension: %s v%s", ext.Name, ext.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start extension process
|
||||
func (m *Manager) startExtension(ext *Extension, path string) error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch ext.Runtime {
|
||||
case "python":
|
||||
cmd = exec.Command("python3", ext.Entry)
|
||||
case "go":
|
||||
cmd = exec.Command("go", "run", ext.Entry)
|
||||
case "node":
|
||||
cmd = exec.Command("node", ext.Entry)
|
||||
default:
|
||||
return fmt.Errorf("unsupported runtime: %s", ext.Runtime)
|
||||
}
|
||||
|
||||
cmd.Dir = path
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PLUGIN_PORT=%d", ext.Port))
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ext.Process = cmd.Process
|
||||
ext.BaseURL = fmt.Sprintf("http://localhost:%d", ext.Port)
|
||||
|
||||
// Wait for extension to be ready
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Call tool
|
||||
func (m *Manager) CallTool(name string, params map[string]interface{}) (interface{}, error) {
|
||||
m.mu.RLock()
|
||||
tool, exists := m.tools[name]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("tool not found: %s", name)
|
||||
}
|
||||
|
||||
ext := m.extensions[tool.ExtensionName]
|
||||
url := fmt.Sprintf("%s/tools/%s", ext.BaseURL, name)
|
||||
|
||||
// HTTP POST to extension
|
||||
body, _ := json.Marshal(params)
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// List all tools
|
||||
func (m *Manager) ListTools() []Tool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
tools := make([]Tool, 0, len(m.tools))
|
||||
for _, tool := range m.tools {
|
||||
tools = append(tools, *tool)
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
// Shutdown all extensions
|
||||
func (m *Manager) Shutdown() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for _, ext := range m.extensions {
|
||||
if ext.Process != nil {
|
||||
ext.Process.Kill()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extension Discovery
|
||||
|
||||
```go
|
||||
// server/main.go
|
||||
func main() {
|
||||
// ... existing setup ...
|
||||
|
||||
// Load extensions
|
||||
extManager := extensions.NewManager()
|
||||
if err := extManager.LoadExtensions("./extensions/backend"); err != nil {
|
||||
log.Fatal("Failed to load extensions:", err)
|
||||
}
|
||||
defer extManager.Shutdown()
|
||||
|
||||
// Expose extension tools via API
|
||||
r.GET("/api/tools", func(c *gin.Context) {
|
||||
tools := extManager.ListTools()
|
||||
c.JSON(200, tools)
|
||||
})
|
||||
|
||||
r.POST("/api/tools/:name", func(c *gin.Context) {
|
||||
toolName := c.Param("name")
|
||||
var params map[string]interface{}
|
||||
c.BindJSON(¶ms)
|
||||
|
||||
result, err := extManager.CallTool(toolName, params)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, result)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Development Workflow
|
||||
|
||||
### 1. Create from Template
|
||||
|
||||
```bash
|
||||
# Frontend plugin
|
||||
cp -r extensions/frontend/_template extensions/frontend/my-plugin
|
||||
cd extensions/frontend/my-plugin
|
||||
# Edit manifest.json, main.js
|
||||
|
||||
# Backend plugin (Python)
|
||||
cp -r extensions/backend/_template-python extensions/backend/my-plugin
|
||||
cd extensions/backend/my-plugin
|
||||
# Edit extension.json, main.py
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Test Locally
|
||||
|
||||
```bash
|
||||
# Backend plugin
|
||||
python main.py # Runs on port from extension.json
|
||||
|
||||
# Test tool
|
||||
curl -X POST http://localhost:9001/tools/my_tool \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"param": "value"}'
|
||||
```
|
||||
|
||||
### 3. Install in Chat Switchboard
|
||||
|
||||
```bash
|
||||
# Move to extensions directory
|
||||
mv my-plugin ../chat-switchboard/extensions/backend/
|
||||
|
||||
# Restart backend
|
||||
cd ../chat-switchboard
|
||||
docker-compose restart backend
|
||||
```
|
||||
|
||||
### 4. Publish to Marketplace
|
||||
|
||||
```bash
|
||||
# Package extension
|
||||
tar -czf my-plugin-1.0.0.tar.gz my-plugin/
|
||||
|
||||
# Upload to marketplace
|
||||
curl -X POST https://marketplace.chatswitch.io/api/publish \
|
||||
-F "file=@my-plugin-1.0.0.tar.gz" \
|
||||
-F "category=tools" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## Security & Sandboxing
|
||||
|
||||
### Backend Plugin Isolation
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml - Run plugins in containers
|
||||
services:
|
||||
plugin-web-search:
|
||||
build: ./extensions/backend/web-search
|
||||
ports:
|
||||
- "9001:9001"
|
||||
environment:
|
||||
- PLUGIN_PORT=9001
|
||||
networks:
|
||||
- extension-network
|
||||
restart: always
|
||||
```
|
||||
|
||||
### Permission System
|
||||
|
||||
```json
|
||||
// Plugins declare required permissions
|
||||
{
|
||||
"permissions": [
|
||||
"network:outbound", // Make external HTTP requests
|
||||
"storage:read", // Read from DB
|
||||
"storage:write", // Write to DB
|
||||
"file:read", // Read files
|
||||
"tool:call:*" // Call other tools
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Official Plugin List
|
||||
|
||||
### Bundled with Core
|
||||
|
||||
1. **chat-engine** (Python) - LLM conversation handling
|
||||
2. **channels** (Go) - Multi-user chat rooms
|
||||
3. **rag-engine** (Python) - Vector search, embeddings
|
||||
4. **workflows** (Go/Python) - Workflow orchestration
|
||||
5. **notes** (Go) - Note management
|
||||
|
||||
### Official Extensions
|
||||
|
||||
6. **web-search** (Python) - DuckDuckGo search
|
||||
7. **code-runner** (Python) - Sandboxed code execution
|
||||
8. **image-gen** (Python) - DALL-E/Stable Diffusion
|
||||
9. **file-ops** (Go) - File system operations
|
||||
10. **calculator** (Python) - Math evaluation
|
||||
|
||||
### Community Extensions (Example Ideas)
|
||||
|
||||
- **slack-integration** - Post to Slack channels
|
||||
- **github-integration** - Create issues, PRs
|
||||
- **email-sender** - Send emails via SMTP
|
||||
- **pdf-parser** - Extract text from PDFs
|
||||
- **scraper** - Web scraping tool
|
||||
- **translate** - Multi-language translation
|
||||
- **tts** - Text-to-speech
|
||||
- **stt** - Speech-to-text
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Read `docs/ARCHITECTURE.md` for system overview
|
||||
2. Check `extensions/_template-python/` for starter code
|
||||
3. Browse `extensions/backend/` for examples
|
||||
4. Join developer Discord for help
|
||||
|
||||
**Build something awesome! 🚀**
|
||||
Reference in New Issue
Block a user