diff --git a/enable-plugin-headless.sh b/enable-plugin-headless.sh new file mode 100755 index 0000000..16309e3 --- /dev/null +++ b/enable-plugin-headless.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# Enable Local REST API plugin headless (no GUI needed) +# Force-loads plugin by restarting Obsidian and verifying + +set -e + +VAULT="/workspace/second-brain" +PLUGIN_ID="obsidian-local-rest-api" +PLUGIN_DIR="$VAULT/.obsidian/plugins/$PLUGIN_ID" + +echo "šŸ”Œ Enabling Local REST API plugin (headless)..." + +# 1. Verify plugin files exist +if [ ! -f "$PLUGIN_DIR/manifest.json" ]; then + echo "āŒ Plugin files not found in $PLUGIN_DIR" + exit 1 +fi +echo "āœ“ Plugin files verified" + +# 2. Ensure plugin is in community-plugins.json +PLUGINS_FILE="$VAULT/.obsidian/community-plugins.json" +if [ ! -f "$PLUGINS_FILE" ]; then + echo '["obsidian-local-rest-api"]' > "$PLUGINS_FILE" + echo "āœ“ community-plugins.json created" +else + # Add plugin if not already there + python3 << PYTHON +import json +with open('$PLUGINS_FILE', 'r') as f: + plugins = json.load(f) +if '$PLUGIN_ID' not in plugins: + plugins.append('$PLUGIN_ID') + with open('$PLUGINS_FILE', 'w') as f: + json.dump(plugins, f, indent=2) + print("āœ“ Plugin added to community-plugins.json") +else: + print("āœ“ Plugin already in community-plugins.json") +PYTHON +fi + +# 3. Create workspace config if missing +mkdir -p "$VAULT/.obsidian" +WORKSPACE_FILE="$VAULT/.obsidian/.obsidian.vimrc" +if [ ! -f "$VAULT/.obsidian/workspace.json" ]; then + cat > "$VAULT/.obsidian/workspace.json" << 'EOF' +{ + "main": { + "id": "root", + "type": "split", + "children": [ + { + "id": "markdown-source-editor", + "type": "leaf", + "state": { + "type": "markdown", + "state": "#" + } + } + ] + }, + "right": { + "id": "right-sidebar", + "type": "sidebar", + "children": [] + }, + "left": { + "id": "left-sidebar", + "type": "sidebar", + "children": [] + }, + "active": "markdown-source-editor" +} +EOF + echo "āœ“ workspace.json created" +fi + +# 4. Ensure plugin settings directory exists +PLUGIN_DATA_DIR="$VAULT/.obsidian/plugins/$PLUGIN_ID" +mkdir -p "$PLUGIN_DATA_DIR" + +# 5. Create plugin data.json with settings +cat > "$PLUGIN_DATA_DIR/data.json" << 'EOF' +{ + "api_key": "generated-at-plugin-load", + "allow_origins": "*", + "port": 27123 +} +EOF +echo "āœ“ Plugin config created" + +# 6. Kill existing Obsidian processes +echo "Restarting Obsidian..." +pkill -f "obsidian" || true +sleep 2 + +# 7. Restart Obsidian via systemd +sudo systemctl restart obsidian.service 2>/dev/null || { + echo "āš ļø systemctl restart failed, starting manual" + DISPLAY=:99 /home/intense/.local/bin/obsidian --no-sandbox /workspace/second-brain/ > /dev/null 2>&1 & +} + +echo "āœ“ Obsidian restarted" + +# 8. Wait for Obsidian to fully start +echo "Waiting for Obsidian to load plugin (30 seconds)..." +for i in {1..30}; do + if netstat -tlnp 2>/dev/null | grep -q 27123 || ss -tlnp 2>/dev/null | grep -q 27123; then + echo "āœ… Plugin loaded! API listening on port 27123" + break + fi + echo -n "." + sleep 1 +done + +# 9. Test MCP connection +echo "" +echo "Testing MCP connection..." +if timeout 5 /workspace/second-brain/start-mcp.sh 2>&1 | grep -q "Obsidian Local REST API is reachable"; then + echo "āœ… SUCCESS! MCP server can connect to Local REST API" + echo "" + echo "Next: Your vault is ready for MCP integration!" +else + echo "āš ļø Plugin may not have started yet. Wait a few more seconds and try:" + echo " cd /workspace/second-brain && ./start-mcp.sh" +fi diff --git a/vault-server.js b/vault-server.js new file mode 100755 index 0000000..cf9db32 --- /dev/null +++ b/vault-server.js @@ -0,0 +1,230 @@ +#!/usr/bin/env node +/** + * Obsidian Vault Headless REST API Server + * Reads vault files directly without requiring Obsidian Desktop app + * Provides REST API compatible with obsidian-mcp-server expectations + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +// Simple HTTP server (no dependencies needed) +const http = require('http'); + +const VAULT_PATH = process.env.VAULT_PATH || '/workspace/second-brain'; +const PORT = process.env.PORT || 27123; +const API_KEY = process.env.OBSIDIAN_API_KEY || 'default-api-key-set-env-var'; + +console.log(`šŸ“š Obsidian Vault Server`); +console.log(`Vault: ${VAULT_PATH}`); +console.log(`Port: ${PORT}`); +console.log(`API Key: ${API_KEY.slice(0, 8)}...`); + +// Authenticate requests +const authenticateRequest = (req) => { + const auth = req.headers['authorization']; + if (!auth) return false; + + const token = auth.replace('Bearer ', ''); + return token === API_KEY; +}; + +// Read file safely +const readFile = (filePath) => { + const fullPath = path.join(VAULT_PATH, filePath); + + // Prevent directory traversal + if (!fullPath.startsWith(VAULT_PATH)) { + return null; + } + + try { + return fs.readFileSync(fullPath, 'utf8'); + } catch (e) { + return null; + } +}; + +// List files in directory +const listDir = (dirPath) => { + const fullPath = path.join(VAULT_PATH, dirPath); + + if (!fullPath.startsWith(VAULT_PATH)) { + return []; + } + + try { + const entries = fs.readdirSync(fullPath, { withFileTypes: true }); + return entries + .filter(e => !e.name.startsWith('.')) + .map(e => ({ + name: e.name, + isDirectory: e.isDirectory() + })); + } catch (e) { + return []; + } +}; + +// Search files +const searchVault = (query) => { + const results = []; + + const search = (dir) => { + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.name.startsWith('.')) continue; + + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + search(fullPath); + } else if (entry.name.endsWith('.md')) { + try { + const content = fs.readFileSync(fullPath, 'utf8'); + if (content.includes(query)) { + const relativePath = path.relative(VAULT_PATH, fullPath); + results.push({ + file: relativePath, + match: content.substring(0, 200) + }); + } + } catch (e) { + // Skip unreadable files + } + } + } + } catch (e) { + // Skip inaccessible directories + } + }; + + search(VAULT_PATH); + return results; +}; + +// Simple JSON response +const jsonResponse = (res, statusCode, data) => { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data, null, 2)); +}; + +// Create server +const server = http.createServer((req, res) => { + // CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // Authenticate + if (!authenticateRequest(req)) { + jsonResponse(res, 401, { error: 'Unauthorized' }); + return; + } + + const url = new URL(req.url, `http://${req.headers.host}`); + const pathname = url.pathname; + + // Routes + if (pathname === '/' && req.method === 'GET') { + // Health check + jsonResponse(res, 200, { + app: 'obsidian-vault-server', + vault: VAULT_PATH, + status: 'running' + }); + return; + } + + if (pathname.startsWith('/vault/read') && req.method === 'GET') { + // Read file + const filePath = url.searchParams.get('path'); + if (!filePath) { + jsonResponse(res, 400, { error: 'Missing path parameter' }); + return; + } + + const content = readFile(filePath); + if (content === null) { + jsonResponse(res, 404, { error: 'File not found' }); + return; + } + + jsonResponse(res, 200, { + path: filePath, + content: content, + stat: { + ctime: Date.now(), + mtime: Date.now(), + size: content.length + } + }); + return; + } + + if (pathname.startsWith('/vault/search') && req.method === 'GET') { + // Search vault + const query = url.searchParams.get('query'); + if (!query) { + jsonResponse(res, 400, { error: 'Missing query parameter' }); + return; + } + + const results = searchVault(query); + jsonResponse(res, 200, { + query: query, + results: results, + count: results.length + }); + return; + } + + if (pathname.startsWith('/vault/list') && req.method === 'GET') { + // List directory + const dirPath = url.searchParams.get('path') || '/'; + const entries = listDir(dirPath); + + jsonResponse(res, 200, { + path: dirPath, + entries: entries, + count: entries.length + }); + return; + } + + // 404 + jsonResponse(res, 404, { error: 'Not found' }); +}); + +server.listen(PORT, () => { + console.log(`āœ… Vault server listening on http://127.0.0.1:${PORT}`); + console.log(`\nEndpoints:`); + console.log(` GET / - Health check`); + console.log(` GET /vault/read?path=file.md - Read file`); + console.log(` GET /vault/list?path=/ - List directory`); + console.log(` GET /vault/search?query=text - Search vault`); + console.log(`\nAll requests require: Authorization: Bearer `); +}); + +server.on('error', (err) => { + console.error('āŒ Server error:', err); + process.exit(1); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\nšŸ‘‹ Shutting down...'); + server.close(() => { + console.log('Goodbye!'); + process.exit(0); + }); +}); diff --git a/vault-server.service b/vault-server.service new file mode 100644 index 0000000..832836b --- /dev/null +++ b/vault-server.service @@ -0,0 +1,33 @@ +[Unit] +Description=Obsidian Vault Headless REST API Server +After=network.target +Documentation=file:///workspace/second-brain/README.md + +[Service] +Type=simple +User=intense +WorkingDirectory=/workspace/second-brain + +# Load environment +EnvironmentFile=/workspace/second-brain/.env + +# Run vault server +ExecStart=/usr/bin/env node /workspace/second-brain/vault-server.js + +# Restart policy +Restart=always +RestartSec=10 +StartLimitInterval=300 +StartLimitBurst=5 + +# Resource limits +MemoryLimit=256M +CPUQuota=30% + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=vault-server + +[Install] +WantedBy=multi-user.target