import { randomBytes } from "node:crypto"; import { mkdir, readFile } from "node:fs/promises"; import { createServer } from "node:net"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import dotenv from "dotenv"; import { serviceOpencodeConfig } from "../src/runtime/opencode2-config.js"; import { startOpencode2 } from "../src/runtime/opencode2-server.js"; const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); dotenv.config({ path: join(projectRoot, ".local.env"), override: false, quiet: true }); const agentPort = readPort("INTEGRATION_AGENT_PORT", 18_787); const opencodePort = readPort("INTEGRATION_OPENCODE_PORT", 14_096); const dataRoot = resolve( process.env.INTEGRATION_DATA_DIR ?? join(projectRoot, "data", `integration-${agentPort}`), ); process.env.OPENCODE_SKILLS_ROOT_DIR = join(dataRoot, "skills"); const bridgeToken = process.env.AGENT_INTERNAL_TOKEN?.trim() || randomBytes(32).toString("hex"); const agentBaseUrl = `http://127.0.0.1:${agentPort}`; const opencodeBaseUrl = `http://127.0.0.1:${opencodePort}`; await assertPortAvailable(agentPort); await assertPortAvailable(opencodePort); await mkdir(dataRoot, { recursive: true }); const sharedEnvironment = { ...process.env, AGENT_INTERNAL_TOKEN: bridgeToken, CONVERSATION_WORKSPACE_ROOT: join(dataRoot, "session-workspaces"), TJWATER_AGENT_WORKSPACE_ROOT: join(dataRoot, "session-workspaces"), TJWATER_AGENT_INTERNAL_BASE_URL: agentBaseUrl, TJWATER_AGENT_MCP_CONNECTION_PATH: join(dataRoot, "mcp-query", "connection.json"), // Retained for tools loaded by an older compatible OpenCode process. TJWATER_AGENT_INTERNAL_TOKEN: bridgeToken, }; const opencode = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: opencodePort, timeout: 30_000, config: serviceOpencodeConfig(JSON.parse(await readFile(join(projectRoot, "opencode.json"), "utf8")), undefined, sharedEnvironment.CONVERSATION_WORKSPACE_ROOT), env: { ...sharedEnvironment, XDG_CACHE_HOME: join(dataRoot, "xdg-cache"), XDG_DATA_HOME: join(dataRoot, "xdg-data"), OPENCODE_DB: join(dataRoot, "xdg-data", "opencode", "opencode.db"), XDG_STATE_HOME: join(dataRoot, "xdg-state"), }, }); let agent: ReturnType | undefined; let shuttingDown = false; const shutdown = async () => { if (shuttingDown) return; shuttingDown = true; agent?.kill("SIGTERM"); opencode.close(); await Promise.allSettled([ ...(agent ? [agent.exited] : []), opencode.exited, ]); }; process.once("SIGINT", () => void shutdown()); process.once("SIGTERM", () => void shutdown()); try { await opencode.client.server.info(); agent = Bun.spawn([process.execPath, "src/server.ts"], { cwd: projectRoot, env: { ...sharedEnvironment, HOST: "127.0.0.1", PORT: String(agentPort), OPENCODE_MODE: "client", OPENCODE_CLIENT_BASE_URL: opencodeBaseUrl, OPENCODE_CLIENT_PASSWORD: opencode.password, LLM_REQUEST_AUDIT_LOG_PATH: join(dataRoot, "logs", "llm-request-audit.log"), LEARNING_AUDIT_LOG_PATH: join(dataRoot, "logs", "learning-audit.log"), MEMORY_STORAGE_DIR: join(dataRoot, "memory"), OPENCODE_SKILLS_ROOT_DIR: join(dataRoot, "skills"), PERSISTENCE_BACKUP_DIR: join(dataRoot, "backup"), SESSION_LEARNING_STATE_STORAGE_DIR: join(dataRoot, "session-learning-state"), SESSION_METADATA_STORAGE_DIR: join(dataRoot, "session-metadata"), SESSION_TRANSCRIPT_STORAGE_DIR: join(dataRoot, "session-transcripts"), SESSION_UI_STATE_STORAGE_DIR: join(dataRoot, "session-ui-states"), }, stderr: "inherit", stdout: "inherit", }); await waitUntilHealthy(`${agentBaseUrl}/health`, "Agent", agent); console.log(`isolated integration pair ready: Agent ${agentBaseUrl}, OpenCode ${opencodeBaseUrl}`); console.log(`isolated runtime data: ${dataRoot}`); const result = await Promise.race([ agent.exited.then((exitCode) => ({ exitCode, process: "Agent" })), opencode.exited.then((exitCode) => ({ exitCode, process: "OpenCode" })), ]); if (!shuttingDown) { console.error(`${result.process} exited with code ${result.exitCode}; stopping its paired process`); process.exitCode = result.exitCode || 1; } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; } finally { await shutdown(); } function readPort(name: string, fallback: number) { const raw = process.env[name]?.trim(); const value = raw ? Number(raw) : fallback; if (!Number.isInteger(value) || value < 1 || value > 65_535) { throw new Error(`${name} must be a valid TCP port`); } return value; } function assertPortAvailable(port: number) { return new Promise((resolvePromise, reject) => { const server = createServer(); server.unref(); server.once("error", (error: NodeJS.ErrnoException) => { reject(new Error(`port ${port} is unavailable: ${error.code ?? error.message}`)); }); server.listen({ host: "127.0.0.1", port, exclusive: true }, () => { server.close((error) => (error ? reject(error) : resolvePromise())); }); }); } async function waitUntilHealthy( url: string, label: string, child: ReturnType, ) { const deadline = Date.now() + 30_000; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error(`${label} exited before becoming ready (code ${child.exitCode})`); } try { const response = await fetch(url, { signal: AbortSignal.timeout(1_000) }); if (response.ok) return; } catch { // The child is still starting; readiness is decided only by its health endpoint. } await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); } throw new Error(`${label} did not become healthy within 30 seconds`); }