UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

502 lines (496 loc) 16.2 kB
'use strict'; var persistence = require('./types-B_i6lpTn.cjs'); var index = require('./index-BMIckAk5.cjs'); var node_child_process = require('node:child_process'); var node_crypto = require('node:crypto'); var Database = require('better-sqlite3'); var path = require('node:path'); var os = require('node:os'); var fs = require('node:fs'); require('axios'); require('chalk'); require('fs'); require('node:events'); require('socket.io-client'); require('zod'); require('tweetnacl'); require('child_process'); require('util'); require('fs/promises'); require('crypto'); require('path'); require('url'); require('os'); require('node:fs/promises'); require('node:module'); require('node:util'); require('expo-server-sdk'); require('node:readline'); require('ink'); require('react'); require('node:url'); require('ps-list'); require('cross-spawn'); require('tmp'); require('qrcode-terminal'); require('open'); require('fastify'); require('fastify-type-provider-zod'); require('http'); require('@modelcontextprotocol/sdk/client/index.js'); require('@modelcontextprotocol/sdk/client/streamableHttp.js'); require('readline'); require('@modelcontextprotocol/sdk/server/mcp.js'); require('node:http'); require('@modelcontextprotocol/sdk/server/streamableHttp.js'); function resolveDbPath() { const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"); const dataDir = path.join(xdgData, "opencode"); if (fs.existsSync(dataDir)) { try { const candidates = fs.readdirSync(dataDir).filter((name) => name.startsWith("opencode") && name.endsWith(".db")).map((name) => { const full = path.join(dataDir, name); try { return { full, mtime: fs.statSync(full).mtimeMs }; } catch { return { full, mtime: 0 }; } }).filter((c) => c.mtime > 0).sort((a, b) => b.mtime - a.mtime); if (candidates.length > 0) return candidates[0].full; } catch (err) { persistence.logger.debug(`[OpenCodeScanner] readdir failed for ${dataDir}: ${err?.message ?? err}`); } } const localDb = path.join(dataDir, "opencode-local.db"); const standardDb = path.join(dataDir, "opencode.db"); if (fs.existsSync(localDb)) return localDb; return standardDb; } function createOpenCodeScanner(opts) { const dbPath = opts.dbPath ?? resolveDbPath(); const pollMs = opts.pollIntervalMs ?? 500; const seenParts = /* @__PURE__ */ new Set(); let detectedSessionId = opts.sessionId ?? null; const scannerStartTime = Date.now(); let lastPollTime = scannerStartTime; let isThinking = false; let interval = null; let db = null; let findSession = null; let getMessages = null; let getParts = null; function ensureDb() { if (db) return true; if (!fs.existsSync(dbPath)) return false; try { db = new Database(dbPath, { readonly: true, fileMustExist: true }); db.pragma("journal_mode = WAL"); findSession = db.prepare(` SELECT id FROM session WHERE time_archived IS NULL AND (directory = ? OR ? LIKE directory || '%' OR directory LIKE ? || '%') ORDER BY time_created DESC LIMIT 1 `); getMessages = db.prepare(` SELECT id, session_id, time_created, time_updated, data FROM message WHERE time_updated > ? ORDER BY time_created, id `); getParts = db.prepare(` SELECT id, data FROM part WHERE message_id = ? ORDER BY time_created, id `); persistence.logger.debug(`[opencode-scanner] DB opened: ${dbPath}`); return true; } catch (err) { persistence.logger.debug("[opencode-scanner] DB open failed (will retry):", err); db = null; return false; } } let hasFoundMessages = false; function detectSession() { if (!findSession) return; const row = findSession.get(opts.cwd, opts.cwd, opts.cwd); if (row && row.id !== detectedSessionId) { detectedSessionId = row.id; persistence.logger.debug(`[opencode-scanner] Detected session: ${detectedSessionId} (cwd: ${opts.cwd})`); } } function transformPart(role, partData, msgId, partId) { const id = `${msgId}:${partId}`; switch (partData.type) { case "text": return { type: "message", id, role, message: partData.text }; case "reasoning": return { type: "thinking", id, text: partData.text }; case "tool": { const tool = partData; const state = tool.state || {}; if (state.status === "completed" && state.output !== void 0) { opts.onMessage({ type: "tool-call", id: `${id}:call`, name: tool.tool, callId: tool.callID || id, input: state.input }); return { type: "tool-result", id: `${id}:result`, name: tool.tool, callId: tool.callID || id, output: state.output }; } else if (state.status === "in-progress") { return { type: "tool-call", id: `${id}:call`, name: tool.tool, callId: tool.callID || id, input: state.input }; } return null; } case "step-start": return { type: "task_started", id }; case "step-finish": return { type: "task_complete", id }; default: return null; } } function poll() { if (!ensureDb()) return; detectSession(); try { const messages = getMessages.all(lastPollTime); if (messages.length > 0) { hasFoundMessages = true; persistence.logger.debug(`[opencode-scanner] Found ${messages.length} new message(s), lastPollTime=${lastPollTime}`); } for (const msg of messages) { const data = JSON.parse(msg.data); const parts = getParts.all(msg.id); persistence.logger.debug(`[opencode-scanner] Message ${msg.id}: role=${data.role}, session=${msg.session_id}, parts=${parts.length}, time_updated=${msg.time_updated}`); if (data.role === "assistant") { const wasThinking = isThinking; const hasStepFinish = parts.some((p) => { const pd = JSON.parse(p.data); return pd.type === "step-finish"; }); isThinking = !hasStepFinish; if (wasThinking !== isThinking) { opts.onThinkingChange?.(isThinking); } } let forwardedCount = 0; for (const part of parts) { const partKey = `${msg.id}:${part.id}`; if (seenParts.has(partKey)) continue; seenParts.add(partKey); const partData = JSON.parse(part.data); const transformed = transformPart(data.role, partData, msg.id, part.id); if (transformed) { persistence.logger.debug(`[opencode-scanner] Forwarding: type=${transformed.type}, id=${transformed.id}`); opts.onMessage(transformed); forwardedCount++; } } if (forwardedCount > 0) { persistence.logger.debug(`[opencode-scanner] Forwarded ${forwardedCount} part(s) from message ${msg.id}`); } lastPollTime = Math.max(lastPollTime, msg.time_updated); } } catch (err) { persistence.logger.debug("[opencode-scanner] Poll error:", err); } } interval = setInterval(poll, pollMs); poll(); return { stop: () => { if (interval) clearInterval(interval); try { db?.close(); } catch { } }, poll, getSessionId: () => detectedSessionId }; } async function consortiumCodeLocalLauncher(opts) { const { binary, cwd, session, messageQueue, messageBuffer, continueSession } = opts; const agent = "consortium-code"; const binaryArgs = continueSession ? ["--continue"] : []; let exitReason = null; const tuiEnv = { ...process.env }; if (!tuiEnv.TERM) tuiEnv.TERM = "xterm-256color"; if (!tuiEnv.COLORTERM) tuiEnv.COLORTERM = "truecolor"; if (messageQueue.size() > 0) { return { type: "switch" }; } let ptyProcess = null; let childProcess = null; function killChild() { if (ptyProcess) { try { ptyProcess.kill("SIGTERM"); setTimeout(() => { try { ptyProcess?.kill("SIGKILL"); } catch { } }, 500); } catch { } } if (childProcess && !childProcess.killed) { childProcess.kill("SIGTERM"); setTimeout(() => { try { if (childProcess && !childProcess.killed) childProcess.kill("SIGKILL"); } catch { } }, 500); } } function doSwitch() { persistence.logger.debug("[local]: doSwitch \u2014 switching to remote mode"); if (!exitReason) { exitReason = { type: "switch" }; } killChild(); } function doAbort() { persistence.logger.debug("[local]: doAbort"); if (!exitReason) { exitReason = { type: "switch" }; } messageQueue.reset(); killChild(); } session.rpcHandlerManager.registerHandler("abort", async () => { doAbort(); }); session.rpcHandlerManager.registerHandler("switch", async () => { doSwitch(); }); messageQueue.setOnMessage(() => { doSwitch(); }); const scanner = createOpenCodeScanner({ cwd, onMessage: (msg) => { forwardToSession(session, agent, msg); if (messageBuffer) { bufferScannerMessage(messageBuffer, msg); } }, onThinkingChange: (thinking) => { session.keepAlive(thinking, "local"); } }); const cols = process.stdout.columns || 80; const rows = process.stdout.rows || 24; let usePty = false; try { const { spawnPtyWithDiagnostics } = await Promise.resolve().then(function () { return require('./types-B_i6lpTn.cjs'); }).then(function (n) { return n.spawnDiagnostics; }); usePty = true; ptyProcess = spawnPtyWithDiagnostics(binary, binaryArgs, { name: "xterm-256color", cols, rows, cwd, env: tuiEnv }); ptyProcess.onData((data) => { process.stdout.write(data); }); if (process.stdin.isTTY) process.stdin.setRawMode(true); process.stdin.resume(); const onData = (data) => { ptyProcess.write(data.toString()); }; process.stdin.on("data", onData); const onResize = () => { ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24); }; process.stdout.on("resize", onResize); session.sendSessionEvent({ type: "ready" }); await new Promise((resolve) => { ptyProcess.onExit(({ exitCode }) => { persistence.logger.debug(`[local] TUI exited: code=${exitCode}`); if (!exitReason) { exitReason = { type: "exit", code: exitCode }; } resolve(); }); }); process.stdout.removeListener("resize", onResize); process.stdin.removeListener("data", onData); if (process.stdin.isTTY) process.stdin.setRawMode(false); process.stdin.pause(); } catch (ptyErr) { if (usePty) throw ptyErr; persistence.logger.debug("[local] node-pty unavailable, falling back to spawn()"); childProcess = node_child_process.spawn(binary, binaryArgs, { cwd, env: tuiEnv, stdio: "inherit" }); process.on("SIGWINCH", () => { if (childProcess && !childProcess.killed) childProcess.kill("SIGWINCH"); }); session.sendSessionEvent({ type: "ready" }); await new Promise((resolve) => { childProcess.on("exit", (code) => { persistence.logger.debug(`[local] TUI exited: code=${code}`); if (!exitReason) { exitReason = { type: "exit", code: code ?? 0 }; } resolve(); }); childProcess.on("error", () => { resolve(); }); }); } scanner.stop(); session.rpcHandlerManager.registerHandler("abort", async () => { }); session.rpcHandlerManager.registerHandler("switch", async () => { }); messageQueue.setOnMessage(null); return exitReason ?? { type: "exit", code: 0 }; } function forwardToSession(session, agent, msg) { switch (msg.type) { case "message": if (msg.role === "user") { session.sendUserChatMessage(msg.message ?? ""); } else { session.sendAgentMessage(agent, { type: "message", message: msg.message ?? "", id: msg.id || node_crypto.randomUUID() }); } break; case "thinking": break; case "tool-call": session.sendAgentMessage(agent, { type: "tool-call", name: msg.name ?? "unknown", callId: msg.callId ?? node_crypto.randomUUID(), input: msg.input, id: msg.id || node_crypto.randomUUID() }); break; case "tool-result": session.sendAgentMessage(agent, { type: "tool-result", callId: msg.callId ?? node_crypto.randomUUID(), output: msg.output, id: msg.id || node_crypto.randomUUID() }); break; case "task_started": session.sendAgentMessage(agent, { type: "task_started", id: msg.id || node_crypto.randomUUID() }); break; case "task_complete": session.sendAgentMessage(agent, { type: "task_complete", id: msg.id || node_crypto.randomUUID() }); break; } } function bufferScannerMessage(buffer, msg) { switch (msg.type) { case "message": buffer.addMessage(msg.message ?? "", msg.role === "user" ? "user" : "assistant"); break; case "tool-call": buffer.addMessage(`${msg.name ?? "tool"}(${JSON.stringify(msg.input ?? {}).substring(0, 100)})`, "tool"); break; case "tool-result": { const output = typeof msg.output === "string" ? msg.output : JSON.stringify(msg.output ?? ""); buffer.addMessage(output.substring(0, 200), "result"); break; } case "task_started": buffer.addMessage("Processing...", "status"); break; case "task_complete": buffer.addMessage("Done", "status"); break; } } async function consortiumCodeLoop(opts) { const { session, messageQueue } = opts; const messageBuffer = new index.MessageBuffer(); let mode = opts.startingMode ?? "local"; let hasRunLocal = false; while (true) { persistence.logger.debug(`[consortiumCodeLoop] Mode: ${mode}`); switch (mode) { case "local": { session.updateAgentState((s) => ({ ...s, controlledByUser: true })); session.keepAlive(false, "local"); const result = await consortiumCodeLocalLauncher({ binary: opts.binary, cwd: opts.cwd, session, messageQueue, messageBuffer, continueSession: hasRunLocal // Resume previous OpenCode session after mode switch }); hasRunLocal = true; switch (result.type) { case "switch": mode = "remote"; session.sendSessionEvent({ type: "switch", mode: "remote" }); session.updateAgentState((s) => ({ ...s, controlledByUser: false })); persistence.logger.debug("[consortiumCodeLoop] Switched to remote mode"); break; case "exit": return result.code; } break; } case "remote": { session.keepAlive(false, "remote"); const { runConsortiumCode } = await Promise.resolve().then(function () { return require('./runConsortiumCode-DXwy0vho.cjs'); }); const reason = await runConsortiumCode({ credentials: opts.credentials, startedBy: opts.startedBy, existingSession: session, messageQueue, messageBuffer }); if (reason === "switch") { mode = "local"; session.sendSessionEvent({ type: "switch", mode: "local" }); session.updateAgentState((s) => ({ ...s, controlledByUser: true })); persistence.logger.debug("[consortiumCodeLoop] Switched back to local mode"); } else { return 0; } break; } } } } exports.consortiumCodeLoop = consortiumCodeLoop;