consortium
Version:
Remote control and session sharing CLI for AI coding agents
502 lines (496 loc) • 16.2 kB
JavaScript
;
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;