Persistent semantic memory HTTP server for AI agents. No cloud, no API keys. Local vector index with Ollama/TF-IDF embeddings. Endpoints: /store, /recall, /forget, /list, /health
110 lines
4.4 KiB
JavaScript
110 lines
4.4 KiB
JavaScript
'use strict';
|
|
|
|
// Load persisted config from ~/.memory-bridge/.env (populated by auth.init on first run)
|
|
const path = require('path');
|
|
const os = require('os');
|
|
require('dotenv').config({ path: path.join(os.homedir(), '.memory-bridge', '.env') });
|
|
|
|
const express = require('express');
|
|
const auth = require('./auth');
|
|
const embedder = require('./embed');
|
|
const store = require('./store');
|
|
|
|
const PORT = Number(process.env.MEMORY_BRIDGE_PORT) || 3722;
|
|
const VERSION = '1.0.0';
|
|
|
|
async function main() {
|
|
auth.init();
|
|
await store.init();
|
|
const embeddingMode = await embedder.init();
|
|
|
|
const app = express();
|
|
app.use(express.json({ limit: '10mb' }));
|
|
|
|
// ── GET /health (public) ──────────────────────────────────────────────────
|
|
app.get('/health', async (_req, res) => {
|
|
try {
|
|
const memoriesCount = await store.count();
|
|
res.json({ status: 'ok', version: VERSION, embeddingMode, memoriesCount });
|
|
} catch (err) {
|
|
res.status(500).json({ status: 'error', error: err.message });
|
|
}
|
|
});
|
|
|
|
// ── Auth guard ────────────────────────────────────────────────────────────
|
|
app.use(auth.middleware);
|
|
|
|
// ── POST /store ───────────────────────────────────────────────────────────
|
|
app.post('/store', async (req, res) => {
|
|
const { text, tags = [], source = '' } = req.body || {};
|
|
if (!text || typeof text !== 'string' || !text.trim()) {
|
|
return res.status(400).json({ error: '`text` is required' });
|
|
}
|
|
try {
|
|
const vector = await embedder.embed(text.trim());
|
|
const result = await store.add({ text: text.trim(), tags, source, vector });
|
|
res.status(201).json(result);
|
|
} catch (err) {
|
|
console.error('POST /store:', err.message);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ── POST /recall ──────────────────────────────────────────────────────────
|
|
app.post('/recall', async (req, res) => {
|
|
const { query, limit = 10, tags = [] } = req.body || {};
|
|
if (!query || typeof query !== 'string' || !query.trim()) {
|
|
return res.status(400).json({ error: '`query` is required' });
|
|
}
|
|
try {
|
|
const vector = await embedder.embed(query.trim());
|
|
const memories = await store.query({
|
|
vector,
|
|
limit: Math.min(Number(limit) || 10, 100),
|
|
tags,
|
|
});
|
|
res.json({ memories });
|
|
} catch (err) {
|
|
console.error('POST /recall:', err.message);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ── POST /forget ──────────────────────────────────────────────────────────
|
|
app.post('/forget', async (req, res) => {
|
|
const { id } = req.body || {};
|
|
if (!id) return res.status(400).json({ error: '`id` is required' });
|
|
try {
|
|
await store.remove(id);
|
|
res.json({ message: 'forgotten', id });
|
|
} catch (err) {
|
|
console.error('POST /forget:', err.message);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ── GET /list ─────────────────────────────────────────────────────────────
|
|
app.get('/list', async (req, res) => {
|
|
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize) || 50));
|
|
try {
|
|
const result = await store.list({ page, pageSize });
|
|
res.json(result);
|
|
} catch (err) {
|
|
console.error('GET /list:', err.message);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`\nmemory-bridge v${VERSION} listening on http://localhost:${PORT}`);
|
|
console.log(`Embedding : ${embeddingMode}`);
|
|
console.log(`Storage : ${path.join(os.homedir(), '.memory-bridge')}\n`);
|
|
});
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Failed to start memory-bridge:', err);
|
|
process.exit(1);
|
|
});
|