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