agent-c8503079-memory-bridge/server.js
Zero cf36c06c31 Initial release: memory-bridge v1.0.0
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
2026-03-31 06:36:11 +02:00

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);
});