function normalizeUri(value) { const raw = String(value ?? '').trim(); if (!raw) return null; try { const url = new URL(raw); if (!['bolt:', 'neo4j:', 'neo4j+s:', 'neo4j+ssc:'].includes(url.protocol)) { throw new Error(`Unsupported protocol: ${url.protocol}`); } return raw; } catch (err) { throw new Error(`Invalid MEMORY_NEO4J_URI: ${err instanceof Error ? err.message : err}`); } } function normalizeHttpUrl(value) { const raw = String(value ?? '').trim(); if (!raw) return null; try { const url = new URL(raw); if (!['http:', 'https:'].includes(url.protocol)) { throw new Error(`Unsupported protocol: ${url.protocol}`); } return url.toString().replace(/\/$/, ''); } catch (err) { throw new Error(`Invalid MEMORY_NEO4J_HTTP_URL: ${err instanceof Error ? err.message : err}`); } } function basicAuth(username, password) { return Buffer.from(`${username}:${password}`).toString('base64'); } export function createNeo4jHttpClient({ httpUrl, username, password, database = 'neo4j', fetchImpl = globalThis.fetch, timeoutMs = 3000, } = {}) { const resolvedHttpUrl = normalizeHttpUrl(httpUrl); if (!resolvedHttpUrl) throw new Error('createNeo4jHttpClient requires MEMORY_NEO4J_HTTP_URL'); if (!String(username ?? '').trim() || !String(password ?? '').trim()) { throw new Error('createNeo4jHttpClient requires MEMORY_NEO4J_USER and MEMORY_NEO4J_PASSWORD'); } if (typeof fetchImpl !== 'function') throw new Error('createNeo4jHttpClient requires fetch'); const resolvedDatabase = String(database ?? 'neo4j').trim() || 'neo4j'; async function run(statement, parameters = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), Number(timeoutMs) || 3000); try { const response = await fetchImpl( `${resolvedHttpUrl}/db/${encodeURIComponent(resolvedDatabase)}/tx/commit`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Basic ${basicAuth(username, password)}`, }, body: JSON.stringify({ statements: [{ statement, parameters }] }), signal: controller.signal, }, ); if (!response?.ok) throw new Error(`Neo4j request failed: ${response?.status ?? 'unknown'}`); const data = await response.json(); if (Array.isArray(data?.errors) && data.errors.length) { throw new Error(`Neo4j query failed: ${data.errors[0]?.message ?? 'unknown'}`); } return data?.results?.[0]?.data ?? []; } finally { clearTimeout(timeout); } } return { async resolve(input = {}) { const rows = await run( `MATCH (u:MemindUser {id: $userId})-[:HAS_MEMORY_EVENT]->(e:MemoryEvent) RETURN e.summary AS behaviorSummary ORDER BY e.created_at DESC LIMIT $limit`, { userId: String(input.userId ?? ''), limit: Math.max(1, Math.min(50, Number(input.limit) || 8)) }, ); const summaries = rows .map((row) => row?.row?.[0]) .filter((item) => typeof item === 'string' && item.trim()); return { memories: [], semanticMemories: [], behaviorSummary: summaries.join('\n') || null, activeGoals: [], }; }, async write(input = {}) { await run( `MERGE (u:MemindUser {id: $userId}) CREATE (e:MemoryEvent { session_id: $sessionId, summary: $summary, created_at: datetime() }) CREATE (u)-[:HAS_MEMORY_EVENT]->(e) RETURN id(e)`, { userId: String(input.userId ?? ''), sessionId: String(input.sessionId ?? ''), summary: JSON.stringify(Array.isArray(input.messages) ? input.messages : []), }, ); return { saved: 1, analyzed: 0, memories: 0 }; }, }; } export function createNeo4jMemoryBackend({ enabled = false, uri = null, httpUrl = null, username = null, password = null, client = null, unavailableReason = null, } = {}) { const resolvedUri = normalizeUri(uri); const resolvedHttpUrl = normalizeHttpUrl(httpUrl); const hasAuth = Boolean(String(username ?? '').trim() && String(password ?? '').trim()); const configured = Boolean(enabled && (resolvedUri || resolvedHttpUrl) && hasAuth); const hasClient = Boolean(client?.resolve && client?.write); const wired = Boolean(configured && hasClient); const reason = unavailableReason ?? (enabled ? (!(resolvedUri || resolvedHttpUrl) ? 'uri_not_configured' : (!hasAuth ? 'auth_not_configured' : 'client_not_configured')) : 'not_configured'); return { name: 'neo4j', category: 'behavior', role: 'behavior-graph', flag: 'MEMORY_NEO4J_ENABLED', unavailableReason: reason, uri: resolvedUri, httpUrl: resolvedHttpUrl, isAvailable() { return Boolean(wired); }, getUnavailableReason() { return this.isAvailable() ? null : reason; }, async resolve(input = {}) { if (!this.isAvailable()) { return { memories: [], semanticMemories: [], behaviorSummary: null, activeGoals: [] }; } const payload = await client.resolve({ ...input, uri: resolvedUri, httpUrl: resolvedHttpUrl }); return { memories: Array.isArray(payload?.memories) ? payload.memories : [], semanticMemories: Array.isArray(payload?.semanticMemories) ? payload.semanticMemories : [], behaviorSummary: payload?.behaviorSummary ?? null, activeGoals: Array.isArray(payload?.activeGoals) ? payload.activeGoals : [], }; }, async write(input = {}) { if (!this.isAvailable()) return { saved: 0, analyzed: 0, memories: 0 }; const result = await client.write({ ...input, uri: resolvedUri, httpUrl: resolvedHttpUrl }); return { saved: Number(result?.saved ?? 0), analyzed: Number(result?.analyzed ?? 0), memories: Number(result?.memories ?? 0), }; }, }; }