diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 527bae7..fd4b85a 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -1,3 +1,5 @@ +require('dotenv').config(); + module.exports = { apps: [{ name: 'cloud-agents-backend', @@ -5,9 +7,10 @@ module.exports = { args: 'tsx src/index.ts', cwd: '/root/cloud-agents', env: { - PORT: 3000, + PORT: 3001, NODE_ENV: 'production', - SENTRY_DSN: 'https://66a26f4df181c1c92a9b4178fd8e4913@o4510621142024192.ingest.de.sentry.io/4510627168649296' + BRAIN_SERVER_URL: 'http://49.13.158.176:5001', + ...process.env } }] }; diff --git a/src/api/brain-proxy.ts b/src/api/brain-proxy.ts new file mode 100644 index 0000000..8237c08 --- /dev/null +++ b/src/api/brain-proxy.ts @@ -0,0 +1,294 @@ +/** + * Brain API Proxy + * + * Proxies all brain requests to the central Brain-Server (49.13.158.176:5001) + * Maps between Cloud-Agents brain API and The-Brain memory API + */ + +import { Router } from "express"; +import { requireAuth } from "../auth/middleware.js"; + +const BRAIN_SERVER_URL = process.env.BRAIN_SERVER_URL || "http://49.13.158.176:5001"; + +interface BrainProxyOptions { + fallbackToLocal?: boolean; +} + +/** + * Creates Brain API Proxy router + */ +export function createBrainProxyRouter(options: BrainProxyOptions = {}): Router { + const router = Router(); + router.use(requireAuth); + + // Helper to proxy requests + async function proxyToBrain( + endpoint: string, + method: string, + userId: string, + body?: any + ): Promise { + const url = `${BRAIN_SERVER_URL}${endpoint}`; + const headers: Record = { + "Content-Type": "application/json", + "x-user-id": userId, + }; + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: response.statusText })); + throw new Error(error.error || "Brain server error"); + } + + return response.json(); + } + + // ===== INGEST ENDPOINTS ===== + // Maps to /api/memory/store with type="document" + + router.post("/ingest/text", async (req, res) => { + try { + const userId = (req as any).userId; + const { title, content, metadata } = req.body; + + if (!title || !content) { + return res.status(400).json({ success: false, error: "title and content required" }); + } + + const result = await proxyToBrain("/api/memory/store", "POST", userId, { + type: "document", + content: `# ${title}\n\n${content}`, + tags: metadata?.tags || ["ingested", "text"], + }); + + res.status(201).json({ + success: true, + doc: { + id: result.id, + userId, + title, + sourceType: "text", + status: "ready", + createdAt: new Date().toISOString(), + }, + }); + } catch (error: any) { + console.error("Brain proxy ingest/text failed:", error.message); + res.status(500).json({ success: false, error: error.message }); + } + }); + + router.post("/ingest/url", async (req, res) => { + try { + const userId = (req as any).userId; + const { title, url, content, metadata } = req.body; + + if (!title || !url || !content) { + return res.status(400).json({ success: false, error: "title, url and content required" }); + } + + const result = await proxyToBrain("/api/memory/store", "POST", userId, { + type: "document", + content: `# ${title}\n\nSource: ${url}\n\n${content}`, + tags: metadata?.tags || ["ingested", "url"], + }); + + res.status(201).json({ + success: true, + doc: { + id: result.id, + userId, + title, + url, + sourceType: "url", + status: "ready", + createdAt: new Date().toISOString(), + }, + }); + } catch (error: any) { + console.error("Brain proxy ingest/url failed:", error.message); + res.status(500).json({ success: false, error: error.message }); + } + }); + + router.post("/ingest/file", async (req, res) => { + try { + const userId = (req as any).userId; + const { title, filePath, fileName, fileType, content, metadata } = req.body; + + if (!title || !content) { + return res.status(400).json({ success: false, error: "title and content required" }); + } + + const result = await proxyToBrain("/api/memory/store", "POST", userId, { + type: "document", + content: `# ${title}\n\nFile: ${fileName || filePath}\nType: ${fileType}\n\n${content}`, + tags: metadata?.tags || ["ingested", "file", fileType || "unknown"], + }); + + res.status(201).json({ + success: true, + doc: { + id: result.id, + userId, + title, + filePath, + fileName, + fileType, + sourceType: "file", + status: "ready", + createdAt: new Date().toISOString(), + }, + }); + } catch (error: any) { + console.error("Brain proxy ingest/file failed:", error.message); + res.status(500).json({ success: false, error: error.message }); + } + }); + + // ===== SEARCH ENDPOINTS ===== + + router.post("/search", async (req, res) => { + try { + const userId = (req as any).userId; + const { query, limit = 10, mode = "keyword" } = req.body; + + if (!query) { + return res.status(400).json({ success: false, error: "query required" }); + } + + const result = await proxyToBrain("/api/memory/search", "POST", userId, { + query, + limit, + }); + + // Transform memory results to brain search results + const results = (result.results || []).map((r: any) => ({ + docId: r.id, + chunkId: r.id, + content: r.content, + score: 1.0, + title: r.content.split("\n")[0].replace(/^#\s*/, "") || "Untitled", + snippet: r.content.substring(0, 200), + })); + + res.json({ success: true, results, count: results.length, mode }); + } catch (error: any) { + console.error("Brain proxy search failed:", error.message); + res.status(500).json({ success: false, error: error.message }); + } + }); + + router.get("/search", async (req, res) => { + try { + const userId = (req as any).userId; + const query = req.query.q as string; + const limit = parseInt(req.query.limit as string) || 10; + + if (!query) { + return res.status(400).json({ success: false, error: "q parameter required" }); + } + + const result = await proxyToBrain("/api/memory/search", "POST", userId, { + query, + limit, + }); + + const results = (result.results || []).map((r: any) => ({ + docId: r.id, + chunkId: r.id, + content: r.content, + score: 1.0, + title: r.content.split("\n")[0].replace(/^#\s*/, "") || "Untitled", + snippet: r.content.substring(0, 200), + })); + + res.json({ success: true, results, count: results.length, mode: "keyword" }); + } catch (error: any) { + console.error("Brain proxy search failed:", error.message); + res.status(500).json({ success: false, error: error.message }); + } + }); + + // ===== DOCUMENT MANAGEMENT ===== + + router.get("/docs", async (req, res) => { + try { + const userId = (req as any).userId; + const limit = parseInt(req.query.limit as string) || 50; + + const result = await proxyToBrain(`/api/memory/recent?limit=${limit}`, "GET", userId); + + // Transform memory entries to docs + const docs = (result.results || []).map((r: any) => ({ + id: r.id, + userId, + title: r.content.split("\n")[0].replace(/^#\s*/, "") || "Untitled", + sourceType: r.type === "document" ? "text" : r.type, + status: "ready", + createdAt: r.createdAt, + })); + + res.json({ success: true, docs, count: docs.length }); + } catch (error: any) { + console.error("Brain proxy docs list failed:", error.message); + res.status(500).json({ success: false, error: error.message }); + } + }); + + // ===== STATS ===== + + router.get("/stats", async (req, res) => { + try { + const userId = (req as any).userId; + + // Get recent documents to calculate stats + const result = await proxyToBrain("/api/memory/recent?limit=50", "GET", userId); + const docs = result.results || []; + + res.json({ + success: true, + enabled: true, + proxyMode: true, + brainServer: BRAIN_SERVER_URL, + stats: { + totalDocs: docs.length, + totalChunks: docs.length, // Each memory entry is one "chunk" + searchCount: 0, + lastSearchAt: null, + }, + }); + } catch (error: any) { + console.error("Brain proxy stats failed:", error.message); + res.status(500).json({ success: false, error: error.message }); + } + }); + + // Health check for proxy + router.get("/proxy/health", async (_req, res) => { + try { + const response = await fetch(`${BRAIN_SERVER_URL}/health`); + const health = await response.json(); + res.json({ + success: true, + proxyMode: true, + brainServer: BRAIN_SERVER_URL, + brainHealth: health, + }); + } catch (error: any) { + res.status(503).json({ + success: false, + proxyMode: true, + brainServer: BRAIN_SERVER_URL, + error: error.message, + }); + } + }); + + return router; +} diff --git a/src/brain/client.ts b/src/brain/client.ts new file mode 100644 index 0000000..091916c --- /dev/null +++ b/src/brain/client.ts @@ -0,0 +1,152 @@ +/** + * BrainClient - HTTP Client for Brain Server + * + * Calls the centralized Brain Server at 49.13.158.176:5001 + * instead of using local database operations. + */ + +const BRAIN_SERVER_URL = process.env.BRAIN_SERVER_URL || "http://49.13.158.176:5001"; + +export interface MemoryEntry { + id: string; + type: string; + content: string; + tags: string[]; + createdAt: string; +} + +export interface StoreOptions { + userId?: string; + projectId?: string; + type: string; + content: string; + tags?: string[]; +} + +export interface SearchOptions { + userId?: string; + projectId?: string; + query: string; + limit?: number; +} + +export class BrainClient { + private baseUrl: string; + private defaultHeaders: Record; + + constructor(options?: { baseUrl?: string; userId?: string; projectId?: string }) { + this.baseUrl = options?.baseUrl || BRAIN_SERVER_URL; + this.defaultHeaders = { + "Content-Type": "application/json", + }; + if (options?.userId) { + this.defaultHeaders["x-user-id"] = options.userId; + } + if (options?.projectId) { + this.defaultHeaders["x-project-id"] = options.projectId; + } + } + + async health(): Promise<{ status: string; service: string; version: string }> { + const res = await fetch(`${this.baseUrl}/health`); + if (!res.ok) throw new Error(`Brain health check failed: ${res.status}`); + return res.json(); + } + + async storeMemory(options: StoreOptions): Promise<{ id: string }> { + const headers = { ...this.defaultHeaders }; + if (options.userId) headers["x-user-id"] = options.userId; + if (options.projectId) headers["x-project-id"] = options.projectId; + + const res = await fetch(`${this.baseUrl}/api/memory/store`, { + method: "POST", + headers, + body: JSON.stringify({ + type: options.type, + content: options.content, + tags: options.tags || [], + }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || "Failed to store memory"); + } + return res.json(); + } + + async searchMemory(options: SearchOptions): Promise<{ results: MemoryEntry[] }> { + const headers = { ...this.defaultHeaders }; + if (options.userId) headers["x-user-id"] = options.userId; + if (options.projectId) headers["x-project-id"] = options.projectId; + + const res = await fetch(`${this.baseUrl}/api/memory/search`, { + method: "POST", + headers, + body: JSON.stringify({ + query: options.query, + limit: options.limit || 10, + }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || "Failed to search memory"); + } + return res.json(); + } + + async recentMemory(options?: { userId?: string; projectId?: string; limit?: number }): Promise<{ results: MemoryEntry[] }> { + const headers = { ...this.defaultHeaders }; + if (options?.userId) headers["x-user-id"] = options.userId; + if (options?.projectId) headers["x-project-id"] = options.projectId; + + const limit = options?.limit || 10; + const res = await fetch(`${this.baseUrl}/api/memory/recent?limit=${limit}`, { + headers, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || "Failed to get recent memory"); + } + return res.json(); + } + + async listProjects(): Promise<{ projects: Array<{ id: string; name: string }> }> { + const res = await fetch(`${this.baseUrl}/api/projects`, { + headers: this.defaultHeaders, + }); + if (!res.ok) throw new Error("Failed to list projects"); + return res.json(); + } + + async logAudit(options: { userId?: string; action: string; details?: Record }): Promise<{ id: string }> { + const headers = { ...this.defaultHeaders }; + if (options.userId) headers["x-user-id"] = options.userId; + + const res = await fetch(`${this.baseUrl}/api/audit/log`, { + method: "POST", + headers, + body: JSON.stringify({ + action: options.action, + details: options.details || {}, + }), + }); + + if (!res.ok) throw new Error("Failed to log audit event"); + return res.json(); + } +} + +// Singleton instance +let brainClient: BrainClient | null = null; + +export function getBrainClient(options?: { userId?: string; projectId?: string }): BrainClient { + if (!brainClient) { + brainClient = new BrainClient(options); + } + return brainClient; +} + +export default BrainClient; diff --git a/src/brain/index.ts b/src/brain/index.ts index 269e8df..d7c9aca 100644 --- a/src/brain/index.ts +++ b/src/brain/index.ts @@ -15,6 +15,21 @@ export type { export { BrainSearch } from "./search.js"; export type { BrainSearchResult, KeywordSearchResult } from "./search.js"; +// Core Brain Client (central knowledge base) +export { + coreBrainSearch, + coreBrainStore, + coreBrainRecent, + buildCoreBrainContext, + coreBrainHealthCheck, +} from "./core-brain.js"; +export type { + CoreBrainMemory, + CoreBrainSearchResult, + CoreBrainStoreParams, + CoreBrainSearchParams, +} from "./core-brain.js"; + // Re-export types from DB schema export type { BrainDoc, diff --git a/src/index.ts b/src/index.ts index d324244..5cb1a65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,7 @@ import { createBillingRouter } from "./api/billing.js"; import { createModulesRouter } from "./api/modules.js"; import { createChatRouter } from "./api/chat.js"; import { createBrainRouter } from "./api/brain.js"; +import { createBrainProxyRouter } from "./api/brain-proxy.js"; import { handleSlackEvents } from "./api/slack-events.js"; import { ChatStorage } from "./chat/storage.js"; import { ChatManager } from "./chat/manager.js"; @@ -65,7 +66,7 @@ import { initEventRecorder, events } from "./lib/events.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const PORT = process.env.PORT ?? 3000; +const PORT = process.env.PORT || 3001; async function main() { console.log("🚀 Starting Code Cloud Agents..."); @@ -202,7 +203,14 @@ async function main() { app.use("/api/billing", createBillingRouter()); app.use("/api/modules", createModulesRouter()); app.use("/api/chat", createChatRouter(chatManager)); - app.use("/api/brain", createBrainRouter(db)); + // Use proxy if BRAIN_SERVER_URL is set, otherwise local + if (process.env.BRAIN_SERVER_URL) { + console.log("🧠 Brain: Proxy mode -> " + process.env.BRAIN_SERVER_URL); + app.use("/api/brain", createBrainProxyRouter()); + } else { + console.log("🧠 Brain: Local mode"); + app.use("/api/brain", createBrainRouter(db)); + } app.use("/api/agent-tasks", createAgentTasksRouter()); app.use("/api/ops", createOpsRouter(db)); diff --git a/start-backend.sh b/start-backend.sh new file mode 100755 index 0000000..86d1737 --- /dev/null +++ b/start-backend.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd /root/cloud-agents +source .env +export PORT=3001 +export BRAIN_SERVER_URL=http://49.13.158.176:5001 +exec npx tsx src/index.ts