Add headless vault REST API server (replaces Obsidian Desktop)
This commit is contained in:
Executable
+125
@@ -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
|
||||||
Executable
+230
@@ -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 <API_KEY>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user