UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

500 lines (495 loc) 15.6 kB
import { l as logger } from './types-DETLaopx.mjs'; import { f as MessageBuffer } from './index-DiNLHtkZ.mjs'; import { spawn } from 'node:child_process'; import { randomUUID } from 'node:crypto'; import Database from 'better-sqlite3'; import { join } from 'node:path'; import { homedir } from 'node:os'; import { existsSync, readdirSync, statSync } from 'node:fs'; import 'axios'; import 'chalk'; import 'fs'; import 'node:events'; import 'socket.io-client'; import 'zod'; import 'tweetnacl'; import 'child_process'; import 'util'; import 'fs/promises'; import 'crypto'; import 'path'; import 'url'; import 'os'; import 'node:fs/promises'; import 'node:module'; import 'node:util'; import 'expo-server-sdk'; import 'node:readline'; import 'ink'; import 'react'; import 'node:url'; import 'ps-list'; import 'cross-spawn'; import 'tmp'; import 'qrcode-terminal'; import 'open'; import 'fastify'; import 'fastify-type-provider-zod'; import 'http'; import '@modelcontextprotocol/sdk/client/index.js'; import '@modelcontextprotocol/sdk/client/streamableHttp.js'; import 'readline'; import '@modelcontextprotocol/sdk/server/mcp.js'; import 'node:http'; import '@modelcontextprotocol/sdk/server/streamableHttp.js'; function resolveDbPath() { const xdgData = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"); const dataDir = join(xdgData, "opencode"); if (existsSync(dataDir)) { try { const candidates = readdirSync(dataDir).filter((name) => name.startsWith("opencode") && name.endsWith(".db")).map((name) => { const full = join(dataDir, name); try { return { full, mtime: 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) { logger.debug(`[OpenCodeScanner] readdir failed for ${dataDir}: ${err?.message ?? err}`); } } const localDb = join(dataDir, "opencode-local.db"); const standardDb = join(dataDir, "opencode.db"); if (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 (!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 `); logger.debug(`[opencode-scanner] DB opened: ${dbPath}`); return true; } catch (err) { 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; 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; 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); 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) { logger.debug(`[opencode-scanner] Forwarding: type=${transformed.type}, id=${transformed.id}`); opts.onMessage(transformed); forwardedCount++; } } if (forwardedCount > 0) { logger.debug(`[opencode-scanner] Forwarded ${forwardedCount} part(s) from message ${msg.id}`); } lastPollTime = Math.max(lastPollTime, msg.time_updated); } } catch (err) { 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() { logger.debug("[local]: doSwitch \u2014 switching to remote mode"); if (!exitReason) { exitReason = { type: "switch" }; } killChild(); } function doAbort() { 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 import('./types-DETLaopx.mjs').then(function (n) { return n.V; }); 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 }) => { 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; logger.debug("[local] node-pty unavailable, falling back to spawn()"); childProcess = 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) => { 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 || randomUUID() }); } break; case "thinking": break; case "tool-call": session.sendAgentMessage(agent, { type: "tool-call", name: msg.name ?? "unknown", callId: msg.callId ?? randomUUID(), input: msg.input, id: msg.id || randomUUID() }); break; case "tool-result": session.sendAgentMessage(agent, { type: "tool-result", callId: msg.callId ?? randomUUID(), output: msg.output, id: msg.id || randomUUID() }); break; case "task_started": session.sendAgentMessage(agent, { type: "task_started", id: msg.id || randomUUID() }); break; case "task_complete": session.sendAgentMessage(agent, { type: "task_complete", id: msg.id || 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 MessageBuffer(); let mode = opts.startingMode ?? "local"; let hasRunLocal = false; while (true) { 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 })); logger.debug("[consortiumCodeLoop] Switched to remote mode"); break; case "exit": return result.code; } break; } case "remote": { session.keepAlive(false, "remote"); const { runConsortiumCode } = await import('./runConsortiumCode-NjBj9tRM.mjs'); 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 })); logger.debug("[consortiumCodeLoop] Switched back to local mode"); } else { return 0; } break; } } } } export { consortiumCodeLoop };