|
"use strict"; |
|
|
|
const express = require("express"); |
|
const fs = require("fs"); |
|
const os = require("os"); |
|
const path = require("path"); |
|
const { spawn } = require("child_process"); |
|
|
|
const PORT = parseInt(process.env.CODEX_BROKER_PORT || "8787", 10); |
|
const HOST = process.env.CODEX_BROKER_HOST || "127.0.0.1"; |
|
const TIMEOUT_MS = parseInt(process.env.CODEX_BROKER_TIMEOUT_MS || "120000", 10); |
|
const CODEX_CMD = process.env.CODEX_CMD || "codex"; |
|
const LOG_PATH = process.env.CODEX_BROKER_LOG || path.join(__dirname, "logs", "broker.log"); |
|
const MAX_INPUT_CHARS = parseInt(process.env.CODEX_BROKER_MAX_INPUT_CHARS || "16000", 10); |
|
const MAX_OUTPUT_CHARS = parseInt(process.env.CODEX_BROKER_MAX_OUTPUT_CHARS || "12000", 10); |
|
const MAX_QUEUE_DEPTH = parseInt(process.env.CODEX_BROKER_MAX_QUEUE_DEPTH || "50", 10); |
|
const BRAIN_CONTEXT_ENABLED = process.env.BRAIN_CONTEXT_ENABLED !== "false"; |
|
const AGENTS_MD_PATH = process.env.AGENTS_MD_PATH || |
|
path.join(os.homedir(), ".codex", "AGENTS.md"); |
|
const MAX_BRAIN_CONTEXT_CHARS = parseInt(process.env.MAX_BRAIN_CONTEXT_CHARS || "3000", 10); |
|
|
|
const app = express(); |
|
app.use(express.json({ limit: "1mb" })); |
|
|
|
const queue = []; |
|
let isRunning = false; |
|
|
|
function nowIso() { |
|
return new Date().toISOString(); |
|
} |
|
|
|
function writeLog(entry) { |
|
const line = `${nowIso()} ${JSON.stringify(entry)}\n`; |
|
fs.appendFile(LOG_PATH, line, () => { }); |
|
} |
|
|
|
let _cachedBrainContext = ""; |
|
let _brainContextLoadedAt = 0; |
|
|
|
function loadBrainContext() { |
|
const now = Date.now(); |
|
if (_cachedBrainContext && now - _brainContextLoadedAt < 60000) { |
|
return _cachedBrainContext; |
|
} |
|
try { |
|
const raw = fs.readFileSync(AGENTS_MD_PATH, "utf8"); |
|
_cachedBrainContext = raw.slice(0, MAX_BRAIN_CONTEXT_CHARS); |
|
_brainContextLoadedAt = now; |
|
} catch { |
|
_cachedBrainContext = ""; |
|
} |
|
return _cachedBrainContext; |
|
} |
|
|
|
function runCodex(question) { |
|
return new Promise((resolve, reject) => { |
|
const outputPath = path.join( |
|
os.tmpdir(), |
|
`codex-broker-${Date.now()}-${Math.floor(Math.random() * 1e9)}.txt` |
|
); |
|
|
|
const args = [ |
|
"exec", |
|
"--skip-git-repo-check", |
|
"--color", |
|
"never", |
|
"--output-last-message", |
|
outputPath, |
|
"-", |
|
]; |
|
|
|
const child = spawn(CODEX_CMD, args, { |
|
stdio: ["pipe", "pipe", "pipe"], |
|
windowsHide: true, |
|
shell: true, |
|
}); |
|
|
|
let stdout = ""; |
|
let stderr = ""; |
|
|
|
const timer = setTimeout(() => { |
|
child.kill(); |
|
reject(new Error(`codex timeout after ${TIMEOUT_MS}ms`)); |
|
}, TIMEOUT_MS); |
|
|
|
child.stdout.on("data", (d) => { |
|
stdout += d.toString(); |
|
}); |
|
|
|
child.stderr.on("data", (d) => { |
|
stderr += d.toString(); |
|
}); |
|
|
|
child.on("error", (err) => { |
|
clearTimeout(timer); |
|
reject(err); |
|
}); |
|
|
|
child.on("close", (code) => { |
|
clearTimeout(timer); |
|
let output = ""; |
|
try { |
|
if (fs.existsSync(outputPath)) { |
|
output = fs.readFileSync(outputPath, "utf8").trim(); |
|
fs.unlinkSync(outputPath); |
|
} |
|
} catch { |
|
// ignore best-effort temp file cleanup |
|
} |
|
|
|
if (code === 0) { |
|
const answer = (output || stdout || "").trim(); |
|
if (answer.length > MAX_OUTPUT_CHARS) { |
|
resolve(answer.slice(0, MAX_OUTPUT_CHARS)); |
|
return; |
|
} |
|
resolve(answer); |
|
return; |
|
} |
|
reject(new Error(`codex exited with code ${code}: ${stderr.trim()}`)); |
|
}); |
|
|
|
child.stdin.write(`${question}\n`); |
|
child.stdin.end(); |
|
}); |
|
} |
|
|
|
async function processQueue() { |
|
if (isRunning || queue.length === 0) { |
|
return; |
|
} |
|
|
|
isRunning = true; |
|
const item = queue.shift(); |
|
|
|
try { |
|
const answer = await runCodex(item.question); |
|
writeLog({ requestId: item.id, question: item.question, answer, ok: true }); |
|
item.res.status(200).json({ answer }); |
|
} catch (err) { |
|
const message = err && err.message ? err.message : String(err); |
|
writeLog({ requestId: item.id, question: item.question, error: message, ok: false }); |
|
item.res.status(500).json({ error: message }); |
|
} finally { |
|
isRunning = false; |
|
setImmediate(processQueue); |
|
} |
|
} |
|
|
|
app.get("/health", (_req, res) => { |
|
res.status(200).json({ |
|
ok: true, |
|
queueDepth: queue.length, |
|
running: isRunning, |
|
limits: { |
|
maxInputChars: MAX_INPUT_CHARS, |
|
maxOutputChars: MAX_OUTPUT_CHARS, |
|
maxQueueDepth: MAX_QUEUE_DEPTH, |
|
}, |
|
}); |
|
}); |
|
|
|
app.post("/ask", (req, res) => { |
|
let question = req.body && typeof req.body.question === "string" ? req.body.question.trim() : ""; |
|
if (!question) { |
|
res.status(400).json({ error: "question is required" }); |
|
return; |
|
} |
|
|
|
// Auto-inject brain context unless explicitly disabled |
|
const skipBrain = req.body && req.body.brain_context === false; |
|
if (BRAIN_CONTEXT_ENABLED && !skipBrain) { |
|
const ctx = loadBrainContext(); |
|
if (ctx) { |
|
question = `[AG Brain Context]\n${ctx}\n[End Context]\n\n${question}`; |
|
} |
|
} |
|
|
|
if (question.length > MAX_INPUT_CHARS) { |
|
res.status(413).json({ |
|
error: `question exceeds max input size (${question.length} > ${MAX_INPUT_CHARS})`, |
|
}); |
|
return; |
|
} |
|
if (queue.length >= MAX_QUEUE_DEPTH) { |
|
res.status(429).json({ |
|
error: `broker queue is full (${queue.length}/${MAX_QUEUE_DEPTH})`, |
|
}); |
|
return; |
|
} |
|
|
|
const id = `${Date.now()}-${Math.floor(Math.random() * 1e6)}`; |
|
queue.push({ id, question, res }); |
|
processQueue(); |
|
}); |
|
|
|
// --------------------------------------------------------------------------- |
|
// Brain Bridge endpoints (Anti-Gravity native brain integration) |
|
// --------------------------------------------------------------------------- |
|
|
|
const BRAIN_BRIDGE_PATH = process.env.BRAIN_BRIDGE_PATH || |
|
path.join("E:", "Codex", "Falooza", "brain_bridge.py"); |
|
const PYTHON_CMD = process.env.BRAIN_PYTHON_CMD || |
|
path.join("E:", "Miniconda3", "envs", "bidsrag311", "python.exe"); |
|
const BRAIN_TIMEOUT_MS = parseInt(process.env.BRAIN_TIMEOUT_MS || "30000", 10); |
|
|
|
function runBrainBridge(args) { |
|
return new Promise((resolve, reject) => { |
|
const child = spawn(PYTHON_CMD, [BRAIN_BRIDGE_PATH, ...args], { |
|
stdio: ["pipe", "pipe", "pipe"], |
|
windowsHide: true, |
|
}); |
|
|
|
let stdout = ""; |
|
let stderr = ""; |
|
|
|
const timer = setTimeout(() => { |
|
child.kill(); |
|
reject(new Error(`brain_bridge timeout after ${BRAIN_TIMEOUT_MS}ms`)); |
|
}, BRAIN_TIMEOUT_MS); |
|
|
|
child.stdout.on("data", (d) => { stdout += d.toString(); }); |
|
child.stderr.on("data", (d) => { stderr += d.toString(); }); |
|
child.on("error", (err) => { clearTimeout(timer); reject(err); }); |
|
|
|
child.on("close", (code) => { |
|
clearTimeout(timer); |
|
if (code === 0) { |
|
try { |
|
resolve(JSON.parse(stdout.trim())); |
|
} catch { |
|
resolve({ raw: stdout.trim() }); |
|
} |
|
} else { |
|
reject(new Error(`brain_bridge exited ${code}: ${stderr.trim()}`)); |
|
} |
|
}); |
|
|
|
child.stdin.end(); |
|
}); |
|
} |
|
|
|
// POST /brain/context โ Get session context from AG brain |
|
app.post("/brain/context", async (req, res) => { |
|
const task = (req.body && req.body.task) || ""; |
|
const topK = (req.body && req.body.top_k) || 5; |
|
try { |
|
const result = await runBrainBridge(["context", task, String(topK)]); |
|
writeLog({ event: "brain_context", task, ok: true }); |
|
res.status(200).json(result); |
|
} catch (err) { |
|
writeLog({ event: "brain_context", task, error: err.message, ok: false }); |
|
res.status(500).json({ error: err.message }); |
|
} |
|
}); |
|
|
|
// POST /brain/query โ Query AG brain for relevant memory |
|
app.post("/brain/query", async (req, res) => { |
|
const prompt = (req.body && req.body.prompt) || ""; |
|
const topK = (req.body && req.body.top_k) || 5; |
|
if (!prompt.trim()) { |
|
res.status(400).json({ error: "prompt is required" }); |
|
return; |
|
} |
|
try { |
|
const result = await runBrainBridge(["query", prompt, String(topK)]); |
|
writeLog({ event: "brain_query", prompt, ok: true }); |
|
res.status(200).json(result); |
|
} catch (err) { |
|
writeLog({ event: "brain_query", prompt, error: err.message, ok: false }); |
|
res.status(500).json({ error: err.message }); |
|
} |
|
}); |
|
|
|
// POST /brain/capture โ Capture a knowledge entry |
|
app.post("/brain/capture", async (req, res) => { |
|
const title = (req.body && req.body.title) || ""; |
|
const content = (req.body && req.body.content) || ""; |
|
const category = (req.body && req.body.category) || "general"; |
|
if (!title.trim() || !content.trim()) { |
|
res.status(400).json({ error: "title and content are required" }); |
|
return; |
|
} |
|
try { |
|
const result = await runBrainBridge(["capture", title, content, category]); |
|
writeLog({ event: "brain_capture", title, category, ok: true }); |
|
res.status(200).json(result); |
|
} catch (err) { |
|
writeLog({ event: "brain_capture", title, error: err.message, ok: false }); |
|
res.status(500).json({ error: err.message }); |
|
} |
|
}); |
|
|
|
// GET /brain/digest โ Get brain stats overview |
|
app.get("/brain/digest", async (_req, res) => { |
|
try { |
|
const result = await runBrainBridge(["digest"]); |
|
res.status(200).json(result); |
|
} catch (err) { |
|
res.status(500).json({ error: err.message }); |
|
} |
|
}); |
|
|
|
// GET /brain/list โ List recent conversations |
|
app.get("/brain/list", async (_req, res) => { |
|
try { |
|
const result = await runBrainBridge(["list"]); |
|
res.status(200).json(result); |
|
} catch (err) { |
|
res.status(500).json({ error: err.message }); |
|
} |
|
}); |
|
|
|
// --------------------------------------------------------------------------- |
|
|
|
app.listen(PORT, HOST, () => { |
|
writeLog({ event: "startup", host: HOST, port: PORT, timeoutMs: TIMEOUT_MS, cmd: CODEX_CMD }); |
|
// eslint-disable-next-line no-console |
|
console.log(`codex-broker listening on http://${HOST}:${PORT}`); |
|
}); |