consortium
Version:
Remote control and session sharing CLI for AI coding agents
1,456 lines (1,431 loc) • 757 kB
JavaScript
'use strict';
var chalk = require('chalk');
var fs = require('node:fs');
var os = require('node:os');
var path = require('node:path');
var node_crypto = require('node:crypto');
var persistence = require('./types-B_i6lpTn.cjs');
var node_child_process = require('node:child_process');
var readline = require('node:readline');
var fs$3 = require('node:fs/promises');
var fs$1 = require('fs/promises');
var path$1 = require('path');
var fs$2 = require('fs');
var ink = require('ink');
var React = require('react');
var node_url = require('node:url');
var axios = require('axios');
var node_events = require('node:events');
var socket_ioClient = require('socket.io-client');
var tweetnacl = require('tweetnacl');
require('expo-server-sdk');
var node_util = require('node:util');
var crypto = require('crypto');
var child_process = require('child_process');
var psList = require('ps-list');
var spawn = require('cross-spawn');
var os$1 = require('os');
var tmp = require('tmp');
var qrcode = require('qrcode-terminal');
var open = require('open');
var fastify = require('fastify');
var z = require('zod');
var fastifyTypeProviderZod = require('fastify-type-provider-zod');
var node_module = require('node:module');
var http = require('http');
var util = require('util');
var index_js = require('@modelcontextprotocol/sdk/client/index.js');
var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
var readline$1 = require('readline');
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
var node_http = require('node:http');
var streamableHttp_js$1 = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$1);
var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);
var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
var os__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(os$1);
var tmp__namespace = /*#__PURE__*/_interopNamespaceDefault(tmp);
var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline$1);
const UNICODE = {
success: "\u2713",
failure: "\u2717",
warning: "\u26A0",
info: "\u2139",
rocket: "\u{1F680}",
user: "\u{1F464}",
bot: "\u{1F916}",
tool: "\u{1F527}",
sparkles: "\u2728",
chart: "\u{1F4CA}",
desktop: "\u{1F4BB}",
eyes: "\u{1F440}",
pointRight: "\u{1F449}",
stethoscope: "\u{1FA7A}",
clipboard: "\u{1F4CB}",
wrench: "\u{1F527}",
globe: "\u{1F30D}",
page: "\u{1F4C4}",
lock: "\u{1F510}",
gear: "\u2699",
hLine: "\u2500",
hLineDouble: "\u2550"
};
const ASCII = {
success: "[OK]",
failure: "[X]",
warning: "[!]",
info: "[i]",
rocket: ">>",
user: "User:",
bot: "Bot:",
tool: "*",
sparkles: "*",
chart: "#",
desktop: "[PC]",
eyes: "?",
pointRight: "->",
stethoscope: "Rx",
clipboard: "[=]",
wrench: "*",
globe: "@",
page: "=",
lock: "#",
gear: "*",
hLine: "-",
hLineDouble: "="
};
let resolved = false;
let active = UNICODE;
let ascii = false;
function initGlyphs() {
if (resolved) return;
resolved = true;
const argv = process.argv;
const flagIdx = argv.indexOf("--ascii");
if (flagIdx !== -1) {
argv.splice(flagIdx, 1);
ascii = true;
}
const env = process.env;
if (!ascii && env.CONSORTIUM_ASCII === "1") ascii = true;
if (!ascii) {
const locale = (env.LC_ALL || env.LC_CTYPE || env.LANG || "").toLowerCase();
if (locale && !locale.includes("utf-8") && !locale.includes("utf8")) {
ascii = true;
}
}
if (!ascii && process.platform === "win32") {
if (!env.WT_SESSION && !env.TERM_PROGRAM) ascii = true;
}
active = ascii ? ASCII : UNICODE;
}
const glyphs = new Proxy({}, {
get(_t, key) {
if (!resolved) initGlyphs();
return active[key];
}
});
const SCHEMA_VERSION = 1;
const STDERR_RING_BUFFER_BYTES = 8 * 1024;
const MAX_FILE_BYTES = 64 * 1024;
const MAX_STACK_TAIL_BYTES = 2 * 1024;
let installed$1 = false;
let startTimeNs = 0n;
let stderrBuffer = Buffer.alloc(0);
let latestError = null;
const packageInfo = readPackageInfo();
function readPackageInfo() {
try {
const candidates = [
// When bundled into dist/, package.json sits one level up
path__namespace.resolve(process.cwd(), "package.json")
];
try {
const u = typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-BMIckAk5.cjs', document.baseURI).href)) }) !== "undefined" ? (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-BMIckAk5.cjs', document.baseURI).href)) : void 0;
if (u && typeof u === "string" && u.startsWith("file://")) {
const here = new URL(u).pathname;
candidates.unshift(path__namespace.resolve(path__namespace.dirname(here), "..", "package.json"));
candidates.unshift(path__namespace.resolve(path__namespace.dirname(here), "..", "..", "package.json"));
}
} catch {
}
for (const p of candidates) {
try {
const raw = fs__namespace.readFileSync(p, "utf8");
const j = JSON.parse(raw);
if (j && typeof j.version === "string" && typeof j.name === "string") {
return { version: j.version, name: j.name };
}
} catch {
}
}
} catch {
}
return { version: "unknown", name: "consortium" };
}
function consortiumHomeDir() {
const env = process.env.CONSORTIUM_HOME_DIR;
if (env) {
return env.startsWith("~") ? path__namespace.join(os__namespace.homedir(), env.slice(1)) : env;
}
return path__namespace.join(os__namespace.homedir(), ".consortium");
}
function lastExitFilePath() {
return path__namespace.join(consortiumHomeDir(), "logs", "last-exit.json");
}
function ensureLogsDir() {
try {
const dir = path__namespace.join(consortiumHomeDir(), "logs");
fs__namespace.mkdirSync(dir, { recursive: true, mode: 448 });
} catch {
}
}
function detectGlibcVersion$1() {
if (process.platform !== "linux") return null;
try {
const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
const v = report?.header?.glibcVersionRuntime;
if (typeof v === "string" && v.length > 0) return v;
} catch {
}
return null;
}
function tailString(s, maxBytes) {
if (!s) return s;
const buf = Buffer.from(s, "utf8");
if (buf.byteLength <= maxBytes) return s;
return buf.subarray(buf.byteLength - maxBytes).toString("utf8");
}
function appendStderrRing(chunk) {
try {
const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk);
if (!buf || buf.byteLength === 0) return;
if (buf.byteLength >= STDERR_RING_BUFFER_BYTES) {
stderrBuffer = Buffer.from(buf.subarray(buf.byteLength - STDERR_RING_BUFFER_BYTES));
return;
}
const combined = Buffer.concat([stderrBuffer, buf]);
stderrBuffer = combined.byteLength > STDERR_RING_BUFFER_BYTES ? Buffer.from(combined.subarray(combined.byteLength - STDERR_RING_BUFFER_BYTES)) : combined;
} catch {
}
}
function patchStderr() {
try {
const originalWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = function patched(chunk, encoding, cb) {
try {
if (typeof encoding === "string" && typeof chunk === "string") {
appendStderrRing(Buffer.from(chunk, encoding));
} else if (typeof chunk === "string" || Buffer.isBuffer(chunk)) {
appendStderrRing(chunk);
}
} catch {
}
return originalWrite(chunk, encoding, cb);
};
} catch {
}
}
function buildPayload(code, signal, err) {
const durationMs = (() => {
try {
if (!startTimeNs) return 0;
const elapsed = process.hrtime.bigint() - startTimeNs;
return Number(elapsed / 1000000n);
} catch {
return 0;
}
})();
const merged = err ?? (latestError ? { name: latestError.name, message: latestError.message, stack: latestError.stack } : null);
const payload = {
schemaVersion: SCHEMA_VERSION,
code: typeof code === "number" ? code : null,
signal: signal ?? null,
args: Array.isArray(process.argv) ? process.argv.slice(2) : [],
subcommand: Array.isArray(process.argv) && process.argv.length >= 3 ? process.argv[2] : null,
nodeVersion: process.version,
platform: `${process.platform}-${process.arch}`,
arch: process.arch,
glibcVersion: detectGlibcVersion$1(),
isTTY: Boolean(process.stdout && process.stdout.isTTY),
durationMs,
exitedAt: (/* @__PURE__ */ new Date()).toISOString(),
cliVersion: packageInfo.version,
cliPackageName: packageInfo.name,
errorName: merged ? merged.name ?? "Error" : null,
errorMessage: merged ? merged.message ?? "" : null,
errorStack: merged ? tailString(String(merged.stack ?? ""), MAX_STACK_TAIL_BYTES) : null,
stderrTail: stderrBuffer.length > 0 ? stderrBuffer.toString("utf8") : ""
};
return payload;
}
function serializeWithCap(payload) {
const dropOrder = ["stderrTail", "errorStack"];
let current = { ...payload };
for (let i = 0; i <= dropOrder.length; i++) {
let json;
try {
json = JSON.stringify(current, null, 2);
} catch {
json = "{}";
}
if (Buffer.byteLength(json, "utf8") <= MAX_FILE_BYTES) return json;
const toDrop = dropOrder[i];
if (!toDrop) break;
current = { ...current, [toDrop]: null };
}
try {
return JSON.stringify(current).slice(0, MAX_FILE_BYTES);
} catch {
return "{}";
}
}
function writeLastExit(code, err, signal = null) {
try {
ensureLogsDir();
const payload = buildPayload(code, signal, err);
const json = serializeWithCap(payload);
const target = lastExitFilePath();
fs__namespace.writeFileSync(target, json, { mode: 384 });
} catch {
}
}
function installCrashReporter() {
if (installed$1) return;
installed$1 = true;
try {
startTimeNs = process.hrtime.bigint();
} catch {
startTimeNs = 0n;
}
patchStderr();
const captureError = (err) => {
try {
if (err instanceof Error) {
latestError = {
name: err.name || "Error",
message: err.message || "",
stack: err.stack || ""
};
} else {
latestError = {
name: "NonError",
message: String(err),
stack: ""
};
}
} catch {
}
};
process.on("uncaughtException", captureError);
process.on("unhandledRejection", captureError);
try {
process.stdout.on("error", captureError);
} catch {
}
try {
process.stderr.on("error", captureError);
} catch {
}
process.prependListener("exit", (code) => {
writeLastExit(code ?? 0, null, null);
});
}
class Session {
path;
logPath;
api;
client;
queue;
claudeEnvVars;
claudeArgs;
// Made mutable to allow filtering
mcpServers;
allowedTools;
_onModeChange;
/** Path to temporary settings file with SessionStart hook (required for session tracking) */
hookSettingsPath;
/** JavaScript runtime to use for spawning Claude Code (default: 'node') */
jsRuntime;
sessionId;
mode = "local";
thinking = false;
/** Callbacks to be notified when session ID is found/changed */
sessionFoundCallbacks = [];
/** Keep alive interval reference for cleanup */
keepAliveInterval;
constructor(opts) {
this.path = opts.path;
this.api = opts.api;
this.client = opts.client;
this.logPath = opts.logPath;
this.sessionId = opts.sessionId;
this.queue = opts.messageQueue;
this.claudeEnvVars = opts.claudeEnvVars;
this.claudeArgs = opts.claudeArgs;
this.mcpServers = opts.mcpServers;
this.allowedTools = opts.allowedTools;
this._onModeChange = opts.onModeChange;
this.hookSettingsPath = opts.hookSettingsPath;
this.jsRuntime = opts.jsRuntime ?? "node";
this.client.keepAlive(this.thinking, this.mode);
this.keepAliveInterval = setInterval(() => {
this.client.keepAlive(this.thinking, this.mode);
}, 2e3);
}
/**
* Cleanup resources (call when session is no longer needed)
*/
cleanup = () => {
clearInterval(this.keepAliveInterval);
this.sessionFoundCallbacks = [];
persistence.logger.debug("[Session] Cleaned up resources");
};
onThinkingChange = (thinking) => {
this.thinking = thinking;
this.client.keepAlive(thinking, this.mode);
};
onModeChange = (mode) => {
this.mode = mode;
this.client.keepAlive(this.thinking, mode);
this._onModeChange(mode);
};
/**
* Called when Claude session ID is discovered or changed.
*
* This is triggered by the SessionStart hook when:
* - Claude starts a new session (fresh start)
* - Claude resumes a session (--continue, --resume flags)
* - Claude forks a session (/compact, double-escape fork)
*
* Updates internal state, syncs to API metadata, and notifies
* all registered callbacks (e.g., SessionScanner) about the change.
*/
onSessionFound = (sessionId) => {
this.sessionId = sessionId;
this.client.updateMetadata((metadata) => ({
...metadata,
claudeSessionId: sessionId,
agentSessionId: sessionId
}));
persistence.logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
for (const callback of this.sessionFoundCallbacks) {
callback(sessionId);
}
};
/**
* Register a callback to be notified when session ID is found/changed
*/
addSessionFoundCallback = (callback) => {
this.sessionFoundCallbacks.push(callback);
};
/**
* Remove a session found callback
*/
removeSessionFoundCallback = (callback) => {
const index = this.sessionFoundCallbacks.indexOf(callback);
if (index !== -1) {
this.sessionFoundCallbacks.splice(index, 1);
}
};
/**
* Clear the current session ID (used by /clear command)
*/
clearSessionId = () => {
this.sessionId = null;
persistence.logger.debug("[Session] Session ID cleared");
};
/**
* Consume one-time Claude flags from claudeArgs after Claude spawn
* Handles: --resume (with or without session ID), --continue
*/
consumeOneTimeFlags = () => {
if (!this.claudeArgs) return;
const filteredArgs = [];
for (let i = 0; i < this.claudeArgs.length; i++) {
const arg = this.claudeArgs[i];
if (arg === "--continue") {
persistence.logger.debug("[Session] Consumed --continue flag");
continue;
}
if (arg === "--resume") {
if (i + 1 < this.claudeArgs.length) {
const nextArg = this.claudeArgs[i + 1];
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
i++;
persistence.logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
} else {
persistence.logger.debug("[Session] Consumed --resume flag (no session ID)");
}
} else {
persistence.logger.debug("[Session] Consumed --resume flag (no session ID)");
}
continue;
}
filteredArgs.push(arg);
}
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
persistence.logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
};
}
function getProjectPath(workingDirectory) {
const projectId = path.resolve(workingDirectory).replace(/[\\\/\.: _]/g, "-");
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
return path.join(claudeConfigDir, "projects", projectId);
}
function claudeCheckSession(sessionId, path$1) {
const projectDir = getProjectPath(path$1);
const sessionFile = path.join(projectDir, `${sessionId}.jsonl`);
const sessionExists = fs.existsSync(sessionFile);
if (!sessionExists) {
persistence.logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
return false;
}
const sessionData = fs.readFileSync(sessionFile, "utf-8").split("\n");
const hasGoodMessage = !!sessionData.find((v, index) => {
if (!v.trim()) return false;
try {
const parsed = JSON.parse(v);
return typeof parsed.uuid === "string" && parsed.uuid.length > 0 || // Claude Code 2.1.x
typeof parsed.messageId === "string" && parsed.messageId.length > 0 || // Older Claude Code
typeof parsed.leafUuid === "string" && parsed.leafUuid.length > 0;
} catch (e) {
persistence.logger.debug(`[claudeCheckSession] Malformed JSON at line ${index + 1}:`, e);
return false;
}
});
persistence.logger.debug(`[claudeCheckSession] Session ${sessionId}: ${hasGoodMessage ? "valid" : "invalid"}`);
return hasGoodMessage;
}
function claudeFindLastSession(workingDirectory) {
try {
const projectDir = getProjectPath(workingDirectory);
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const files = fs.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
const sessionId = f.replace(".jsonl", "");
if (!uuidPattern.test(sessionId)) {
return null;
}
if (claudeCheckSession(sessionId, workingDirectory)) {
return {
name: f,
sessionId,
mtime: fs.statSync(path.join(projectDir, f)).mtime.getTime()
};
}
return null;
}).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
return files.length > 0 ? files[0].sessionId : null;
} catch (e) {
persistence.logger.debug("[claudeFindLastSession] Error finding sessions:", e);
return null;
}
}
function trimIdent(text) {
const lines = text.split("\n");
while (lines.length > 0 && lines[0].trim() === "") {
lines.shift();
}
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
lines.pop();
}
const minSpaces = lines.reduce((min, line) => {
if (line.trim() === "") {
return min;
}
const leadingSpaces = line.match(/^\s*/)[0].length;
return Math.min(min, leadingSpaces);
}, Infinity);
const trimmedLines = lines.map((line) => line.slice(minSpaces));
return trimmedLines.join("\n");
}
function getClaudeSettingsPath() {
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
return path.join(claudeConfigDir, "settings.json");
}
function readClaudeSettings() {
try {
const settingsPath = getClaudeSettingsPath();
if (!fs.existsSync(settingsPath)) {
persistence.logger.debug(`[ClaudeSettings] No Claude settings file found at ${settingsPath}`);
return null;
}
const settingsContent = fs.readFileSync(settingsPath, "utf-8");
const settings = JSON.parse(settingsContent);
persistence.logger.debug(`[ClaudeSettings] Successfully read Claude settings from ${settingsPath}`);
persistence.logger.debug(`[ClaudeSettings] includeCoAuthoredBy: ${settings.includeCoAuthoredBy}`);
return settings;
} catch (error) {
persistence.logger.debug(`[ClaudeSettings] Error reading Claude settings: ${error}`);
return null;
}
}
function shouldIncludeCoAuthoredBy() {
const settings = readClaudeSettings();
if (!settings || settings.includeCoAuthoredBy === void 0) {
return true;
}
return settings.includeCoAuthoredBy;
}
const CO_AUTHORED_CREDITS = (() => trimIdent(`
When making commit messages, instead of just giving co-credit to Claude, also give credit to Consortium like so:
<main commit message>
Generated with [Claude Code](https://claude.ai/code)
via [Consortium](https://consortium.dev)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Consortium <yesreply@consortium.dev>
`))();
const systemPrompt = (() => {
return shouldIncludeCoAuthoredBy() ? CO_AUTHORED_CREDITS : "";
})();
class ExitCodeError extends Error {
exitCode;
constructor(exitCode) {
super(`Process exited with code: ${exitCode}`);
this.name = "ExitCodeError";
this.exitCode = exitCode;
}
}
const claudeCliPath = path.resolve(path.join(persistence.projectPath(), "scripts", "claude_local_launcher.cjs"));
async function claudeLocal(opts) {
const projectDir = getProjectPath(opts.path);
fs.mkdirSync(projectDir, { recursive: true });
const hasContinueFlag = opts.claudeArgs?.includes("--continue");
const hasResumeFlag = opts.claudeArgs?.includes("--resume");
const hasUserSessionControl = hasContinueFlag || hasResumeFlag;
let startFrom = opts.sessionId;
const extractFlag = (flags, withValue = false) => {
if (!opts.claudeArgs) return { found: false };
for (const flag of flags) {
const index = opts.claudeArgs.indexOf(flag);
if (index !== -1) {
if (withValue && index + 1 < opts.claudeArgs.length) {
const nextArg = opts.claudeArgs[index + 1];
if (!nextArg.startsWith("-")) {
const value = nextArg;
opts.claudeArgs = opts.claudeArgs.filter((_, i) => i !== index && i !== index + 1);
return { found: true, value };
}
}
if (!withValue) {
opts.claudeArgs = opts.claudeArgs.filter((_, i) => i !== index);
return { found: true };
}
return { found: false };
}
}
return { found: false };
};
const sessionIdFlag = extractFlag(["--session-id"], true);
if (sessionIdFlag.found && sessionIdFlag.value) {
startFrom = null;
persistence.logger.debug(`[ClaudeLocal] Using explicit --session-id: ${sessionIdFlag.value}`);
}
if (!startFrom && !sessionIdFlag.value) {
const resumeFlag = extractFlag(["--resume", "-r"], true);
if (resumeFlag.found) {
if (resumeFlag.value) {
startFrom = resumeFlag.value;
persistence.logger.debug(`[ClaudeLocal] Using provided session ID from --resume: ${startFrom}`);
} else {
const lastSession = claudeFindLastSession(opts.path);
if (lastSession) {
startFrom = lastSession;
persistence.logger.debug(`[ClaudeLocal] --resume: Found last session: ${lastSession}`);
}
}
}
}
if (!startFrom && !sessionIdFlag.value) {
const continueFlag = extractFlag(["--continue", "-c"], false);
if (continueFlag.found) {
const lastSession = claudeFindLastSession(opts.path);
if (lastSession) {
startFrom = lastSession;
persistence.logger.debug(`[ClaudeLocal] --continue: Found last session: ${lastSession}`);
}
}
}
const explicitSessionId = sessionIdFlag.value || null;
let newSessionId = null;
let effectiveSessionId = startFrom;
if (!opts.hookSettingsPath) {
newSessionId = startFrom ? null : explicitSessionId || node_crypto.randomUUID();
effectiveSessionId = startFrom || newSessionId;
if (startFrom) {
persistence.logger.debug(`[ClaudeLocal] Resuming session: ${startFrom}`);
opts.onSessionFound(startFrom);
} else if (explicitSessionId) {
persistence.logger.debug(`[ClaudeLocal] Using explicit session ID: ${explicitSessionId}`);
opts.onSessionFound(explicitSessionId);
} else {
persistence.logger.debug(`[ClaudeLocal] Generated new session ID: ${newSessionId}`);
opts.onSessionFound(newSessionId);
}
} else {
if (startFrom) {
persistence.logger.debug(`[ClaudeLocal] Will resume existing session: ${startFrom}`);
} else if (hasUserSessionControl) {
persistence.logger.debug(`[ClaudeLocal] User passed ${hasContinueFlag ? "--continue" : "--resume"} flag, session ID will be determined by hook`);
} else {
persistence.logger.debug(`[ClaudeLocal] Fresh start, session ID will be provided by hook`);
}
}
let thinking = false;
let stopThinkingTimeout = null;
const updateThinking = (newThinking) => {
if (thinking !== newThinking) {
thinking = newThinking;
persistence.logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
if (opts.onThinkingChange) {
opts.onThinkingChange(thinking);
}
}
};
try {
process.stdin.pause();
await new Promise((r, reject) => {
const args = [];
if (!opts.hookSettingsPath) {
const hasResumeFlag2 = opts.claudeArgs?.includes("--resume") || opts.claudeArgs?.includes("-r");
if (startFrom) {
args.push("--resume", startFrom);
} else if (!hasResumeFlag2 && newSessionId) {
args.push("--session-id", newSessionId);
}
} else {
if (startFrom) {
args.push("--resume", startFrom);
}
}
args.push("--append-system-prompt", systemPrompt);
if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
args.push("--mcp-config", JSON.stringify({ mcpServers: opts.mcpServers }));
}
if (opts.allowedTools && opts.allowedTools.length > 0) {
args.push("--allowedTools", opts.allowedTools.join(","));
}
if (opts.claudeArgs) {
args.push(...opts.claudeArgs);
}
if (opts.hookSettingsPath) {
args.push("--settings", opts.hookSettingsPath);
persistence.logger.debug(`[ClaudeLocal] Using hook settings: ${opts.hookSettingsPath}`);
}
if (!claudeCliPath || !fs.existsSync(claudeCliPath)) {
throw new Error("Claude local launcher not found. Please ensure CONSORTIUM_PROJECT_ROOT is set correctly for development.");
}
const env = {
...process.env,
...opts.claudeEnvVars
};
persistence.logger.debug(`[ClaudeLocal] Spawning launcher: ${claudeCliPath}`);
persistence.logger.debug(`[ClaudeLocal] Args: ${JSON.stringify(args)}`);
const child = node_child_process.spawn(process.execPath, [claudeCliPath, ...args], {
stdio: ["inherit", "inherit", "inherit", "pipe"],
signal: opts.abort,
cwd: opts.path,
env
});
if (child.stdio[3]) {
const rl = readline.createInterface({
input: child.stdio[3],
crlfDelay: Infinity
});
const activeFetches = /* @__PURE__ */ new Map();
rl.on("line", (line) => {
try {
const message = JSON.parse(line);
switch (message.type) {
case "fetch-start":
activeFetches.set(message.id, {
hostname: message.hostname,
path: message.path,
startTime: message.timestamp
});
if (stopThinkingTimeout) {
clearTimeout(stopThinkingTimeout);
stopThinkingTimeout = null;
}
updateThinking(true);
break;
case "fetch-end":
activeFetches.delete(message.id);
if (activeFetches.size === 0 && thinking && !stopThinkingTimeout) {
stopThinkingTimeout = setTimeout(() => {
if (activeFetches.size === 0) {
updateThinking(false);
}
stopThinkingTimeout = null;
}, 500);
}
break;
default:
persistence.logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
}
} catch (e) {
persistence.logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
}
});
rl.on("error", (err) => {
console.error("Error reading from fd 3:", err);
});
child.on("exit", () => {
if (stopThinkingTimeout) {
clearTimeout(stopThinkingTimeout);
}
updateThinking(false);
});
}
child.on("error", (error) => {
});
child.on("exit", (code, signal) => {
if (signal === "SIGTERM" && opts.abort.aborted) {
r();
} else if (signal) {
reject(new Error(`Process terminated with signal: ${signal}`));
} else if (code !== 0 && code !== null) {
reject(new ExitCodeError(code));
} else {
r();
}
});
});
} finally {
process.stdin.resume();
if (stopThinkingTimeout) {
clearTimeout(stopThinkingTimeout);
stopThinkingTimeout = null;
}
updateThinking(false);
}
return effectiveSessionId;
}
class Future {
_resolve;
_reject;
_promise;
constructor() {
this._promise = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}
resolve(value) {
this._resolve(value);
}
reject(reason) {
this._reject(reason);
}
get promise() {
return this._promise;
}
}
class InvalidateSync {
_invalidated = false;
_invalidatedDouble = false;
_stopped = false;
_command;
_pendings = [];
constructor(command) {
this._command = command;
}
invalidate() {
if (this._stopped) {
return;
}
if (!this._invalidated) {
this._invalidated = true;
this._invalidatedDouble = false;
this._doSync();
} else {
if (!this._invalidatedDouble) {
this._invalidatedDouble = true;
}
}
}
async invalidateAndAwait() {
if (this._stopped) {
return;
}
await new Promise((resolve) => {
this._pendings.push(resolve);
this.invalidate();
});
}
stop() {
if (this._stopped) {
return;
}
this._notifyPendings();
this._stopped = true;
}
_notifyPendings = () => {
for (let pending of this._pendings) {
pending();
}
this._pendings = [];
};
_doSync = async () => {
await persistence.backoff(async () => {
if (this._stopped) {
return;
}
await this._command();
});
if (this._stopped) {
this._notifyPendings();
return;
}
if (this._invalidatedDouble) {
this._invalidatedDouble = false;
this._doSync();
} else {
this._invalidated = false;
this._notifyPendings();
}
};
}
function startFileWatcher(file, onFileChange) {
const abortController = new AbortController();
const parentDir = path$1.dirname(file);
const fileBasename = path$1.basename(file);
async function waitForFileToAppear() {
if (fs$2.existsSync(file)) return true;
if (!fs$2.existsSync(parentDir)) {
persistence.logger.debug(`[FILE_WATCHER] Parent directory ${parentDir} does not exist, polling for it`);
while (!abortController.signal.aborted && !fs$2.existsSync(parentDir)) {
await persistence.delay(5e3);
}
if (abortController.signal.aborted) return false;
if (fs$2.existsSync(file)) return true;
}
persistence.logger.debug(`[FILE_WATCHER] File ${file} does not exist yet, watching parent directory ${parentDir}`);
try {
const dirWatcher = fs$1.watch(parentDir, { persistent: true, signal: abortController.signal });
const pollInterval = setInterval(() => {
if (fs$2.existsSync(file)) {
}
}, 2e3);
try {
for await (const event of dirWatcher) {
if (abortController.signal.aborted) return false;
if (event.filename === fileBasename || event.filename === null) {
if (fs$2.existsSync(file)) {
persistence.logger.debug(`[FILE_WATCHER] File ${file} appeared, switching to direct watch`);
return true;
}
}
}
} finally {
clearInterval(pollInterval);
}
} catch (e) {
if (abortController.signal.aborted) return false;
persistence.logger.debug(`[FILE_WATCHER] Failed to watch parent directory ${parentDir}: ${e.message}, falling back to polling`);
while (!abortController.signal.aborted) {
if (fs$2.existsSync(file)) return true;
await persistence.delay(2e3);
}
}
return false;
}
void (async () => {
let consecutiveErrors = 0;
while (!abortController.signal.aborted) {
try {
const appeared = await waitForFileToAppear();
if (!appeared || abortController.signal.aborted) return;
persistence.logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
const watcher = fs$1.watch(file, { persistent: true, signal: abortController.signal });
for await (const event of watcher) {
if (abortController.signal.aborted) return;
persistence.logger.debug(`[FILE_WATCHER] File changed: ${file}`);
consecutiveErrors = 0;
onFileChange(file);
}
consecutiveErrors = 0;
} catch (e) {
if (abortController.signal.aborted) return;
if (e.code === "ENOENT") {
persistence.logger.debug(`[FILE_WATCHER] File ${file} disappeared, will wait for it to reappear`);
continue;
}
consecutiveErrors++;
const backoffMs = Math.min(1e3 * Math.pow(2, consecutiveErrors - 1), 3e4);
persistence.logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, retrying in ${backoffMs}ms (attempt ${consecutiveErrors})`);
await persistence.delay(backoffMs);
}
}
})();
return () => {
abortController.abort();
};
}
const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set([
"file-history-snapshot",
"change",
"queue-operation"
]);
async function createSessionScanner(opts) {
const projectDir = getProjectPath(opts.workingDirectory);
let finishedSessions = /* @__PURE__ */ new Set();
let pendingSessions = /* @__PURE__ */ new Set();
let currentSessionId = null;
let watchers = /* @__PURE__ */ new Map();
let processedMessageKeys = /* @__PURE__ */ new Set();
if (opts.sessionId) {
let messages = await readSessionLog(projectDir, opts.sessionId);
persistence.logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
for (let m of messages) {
processedMessageKeys.add(messageKey(m));
}
currentSessionId = opts.sessionId;
}
const sync = new InvalidateSync(async () => {
let sessions = [];
for (let p of pendingSessions) {
sessions.push(p);
}
if (currentSessionId && !pendingSessions.has(currentSessionId)) {
sessions.push(currentSessionId);
}
for (let [sessionId] of watchers) {
if (!sessions.includes(sessionId)) {
sessions.push(sessionId);
}
}
for (let session of sessions) {
const sessionMessages = await readSessionLog(projectDir, session);
let skipped = 0;
let sent = 0;
for (let file of sessionMessages) {
let key = messageKey(file);
if (processedMessageKeys.has(key)) {
skipped++;
continue;
}
processedMessageKeys.add(key);
persistence.logger.debug(`[SESSION_SCANNER] Sending new message: type=${file.type}, uuid=${file.type === "summary" ? file.leafUuid : file.uuid}`);
opts.onMessage(file);
sent++;
}
if (sessionMessages.length > 0) {
persistence.logger.debug(`[SESSION_SCANNER] Session ${session}: found=${sessionMessages.length}, skipped=${skipped}, sent=${sent}`);
}
}
for (let p of sessions) {
if (pendingSessions.has(p)) {
pendingSessions.delete(p);
finishedSessions.add(p);
}
}
for (let p of sessions) {
if (!watchers.has(p)) {
persistence.logger.debug(`[SESSION_SCANNER] Starting watcher for session: ${p}`);
watchers.set(p, startFileWatcher(path.join(projectDir, `${p}.jsonl`), () => {
sync.invalidate();
}));
}
}
});
await sync.invalidateAndAwait();
const intervalId = setInterval(() => {
sync.invalidate();
}, 3e3);
return {
cleanup: async () => {
clearInterval(intervalId);
for (let w of watchers.values()) {
w();
}
watchers.clear();
await sync.invalidateAndAwait();
sync.stop();
},
onNewSession: (sessionId) => {
if (currentSessionId === sessionId) {
persistence.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
return;
}
if (finishedSessions.has(sessionId)) {
persistence.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
return;
}
if (pendingSessions.has(sessionId)) {
persistence.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
return;
}
if (currentSessionId) {
pendingSessions.add(currentSessionId);
}
persistence.logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
currentSessionId = sessionId;
sync.invalidate();
}
};
}
function messageKey(message) {
if (message.type === "user") {
return message.uuid;
} else if (message.type === "assistant") {
return message.uuid;
} else if (message.type === "summary") {
return "summary: " + message.leafUuid + ": " + message.summary;
} else if (message.type === "system") {
return message.uuid;
} else {
throw Error();
}
}
async function readSessionLog(projectDir, sessionId) {
const expectedSessionFile = path.join(projectDir, `${sessionId}.jsonl`);
let file;
try {
file = await fs$3.readFile(expectedSessionFile, "utf-8");
} catch (error) {
if (error?.code !== "ENOENT") {
persistence.logger.debug(`[SESSION_SCANNER] Error reading session file ${expectedSessionFile}: ${error.message ?? error}`);
}
return [];
}
let lines = file.split("\n");
let messages = [];
for (let l of lines) {
try {
if (l.trim() === "") {
continue;
}
let message = JSON.parse(l);
if (message.type && INTERNAL_CLAUDE_EVENT_TYPES.has(message.type)) {
continue;
}
let parsed = persistence.RawJSONLinesSchema.safeParse(message);
if (!parsed.success) {
continue;
}
messages.push(parsed.data);
} catch (e) {
persistence.logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
continue;
}
}
return messages;
}
async function claudeLocalLauncher(session) {
const scanner = await createSessionScanner({
sessionId: session.sessionId,
workingDirectory: session.path,
onMessage: (message) => {
if (message.type !== "summary") {
session.client.sendClaudeSessionMessage(message);
}
}
});
const scannerSessionCallback = (sessionId) => {
scanner.onNewSession(sessionId);
};
session.addSessionFoundCallback(scannerSessionCallback);
let exitReason = null;
const processAbortController = new AbortController();
let exutFuture = new Future();
try {
async function abort() {
if (!processAbortController.signal.aborted) {
processAbortController.abort();
}
await exutFuture.promise;
}
async function doAbort() {
persistence.logger.debug("[local]: doAbort");
if (!exitReason) {
exitReason = { type: "switch" };
}
session.queue.reset();
await abort();
}
async function doSwitch() {
persistence.logger.debug("[local]: doSwitch");
if (!exitReason) {
exitReason = { type: "switch" };
}
await abort();
}
session.client.rpcHandlerManager.registerHandler("abort", doAbort);
session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
session.queue.setOnMessage((message, mode) => {
doSwitch();
});
if (session.queue.size() > 0) {
return { type: "switch" };
}
const handleSessionStart = (sessionId) => {
session.onSessionFound(sessionId);
scanner.onNewSession(sessionId);
};
while (true) {
if (exitReason) {
return exitReason;
}
persistence.logger.debug("[local]: launch");
try {
await claudeLocal({
path: session.path,
sessionId: session.sessionId,
onSessionFound: handleSessionStart,
onThinkingChange: session.onThinkingChange,
abort: processAbortController.signal,
claudeEnvVars: session.claudeEnvVars,
claudeArgs: session.claudeArgs,
mcpServers: session.mcpServers,
allowedTools: session.allowedTools,
hookSettingsPath: session.hookSettingsPath
});
session.consumeOneTimeFlags();
if (!exitReason) {
exitReason = { type: "exit", code: 0 };
break;
}
} catch (e) {
persistence.logger.debug("[local]: launch error", e);
if (e instanceof ExitCodeError) {
exitReason = { type: "exit", code: e.exitCode };
break;
}
if (!exitReason) {
session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
continue;
} else {
break;
}
}
persistence.logger.debug("[local]: launch done");
}
} finally {
exutFuture.resolve(void 0);
session.client.rpcHandlerManager.registerHandler("abort", async () => {
});
session.client.rpcHandlerManager.registerHandler("switch", async () => {
});
session.queue.setOnMessage(null);
session.removeSessionFoundCallback(scannerSessionCallback);
await scanner.cleanup();
}
return exitReason || { type: "exit", code: 0 };
}
class MessageBuffer {
messages = [];
listeners = [];
nextId = 1;
addMessage(content, type = "assistant") {
const message = {
id: `msg-${this.nextId++}`,
timestamp: /* @__PURE__ */ new Date(),
content,
type
};
this.messages.push(message);
this.notifyListeners();
}
/**
* Update the last message of a specific type by appending content to it
* Useful for streaming responses where deltas should accumulate in one message
*/
updateLastMessage(contentDelta, type = "assistant") {
for (let i = this.messages.length - 1; i >= 0; i--) {
if (this.messages[i].type === type) {
const oldMessage = this.messages[i];
const updatedMessage = {
...oldMessage,
content: oldMessage.content + contentDelta
};
this.messages[i] = updatedMessage;
this.notifyListeners();
return;
}
}
this.addMessage(contentDelta, type);
}
/**
* Remove the last message of a specific type
* Useful for removing placeholder messages like "Thinking..." when actual response starts
*/
removeLastMessage(type) {
for (let i = this.messages.length - 1; i >= 0; i--) {
if (this.messages[i].type === type) {
this.messages.splice(i, 1);
this.notifyListeners();
return true;
}
}
return false;
}
getMessages() {
return [...this.messages];
}
clear() {
this.messages = [];
this.nextId = 1;
this.notifyListeners();
}
onUpdate(listener) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
};
}
notifyListeners() {
const messages = this.getMessages();
this.listeners.forEach((listener) => listener(messages));
}
}
const RemoteModeDisplay = ({ messageBuffer, logPath, onExit, onSwitchToLocal }) => {
const [messages, setMessages] = React.useState([]);
const [confirmationMode, setConfirmationMode] = React.useState(null);
const [actionInProgress, setActionInProgress] = React.useState(null);
const confirmationTimeoutRef = React.useRef(null);
const { stdout } = ink.useStdout();
const terminalWidth = stdout.columns || 80;
const terminalHeight = stdout.rows || 24;
React.useEffect(() => {
setMessages(messageBuffer.getMessages());
const unsubscribe = messageBuffer.onUpdate((newMessages) => {
setMessages(newMessages);
});
return () => {
unsubscribe();
if (confirmationTimeoutRef.current) {
clearTimeout(confirmationTimeoutRef.current);
}
};
}, [messageBuffer]);
const resetConfirmation = React.useCallback(() => {
setConfirmationMode(null);
if (confirmationTimeoutRef.current) {
clearTimeout(confirmationTimeoutRef.current);
confirmationTimeoutRef.current = null;
}
}, []);
const setConfirmationWithTimeout = React.useCallback((mode) => {
setConfirmationMode(mode);
if (confirmationTimeoutRef.current) {
clearTimeout(confirmationTimeoutRef.current);
}
confirmationTimeoutRef.current = setTimeout(() => {
resetConfirmation();
}, 15e3);
}, [resetConfirmation]);
ink.useInput(React.useCallback(async (input, key) => {
if (actionInProgress) return;
if (key.ctrl && input === "c") {
if (confirmationMode === "exit") {
resetConfirmation();
setActionInProgress("exiting");
await new Promise((resolve) => setTimeout(resolve, 100));
onExit?.();
} else {
setConfirmationWithTimeout("exit");
}
return;
}
if (input === " ") {
if (confirmationMode === "switch") {
resetConfirmation();
setActionInProgress("switching");
await new Promise((resolve) => setTimeout(resolve, 100));
onSwitchToLocal?.();
} else {
setConfirmationWithTimeout("switch");
}
return;
}
if (confirmationMode) {
resetConfirmation();
}
}, [confirmationMode, actionInProgress, onExit, onSwitchToLocal, setConfirmationWithTimeout, resetConfirmation]));
const getMessageColor = (type) => {
switch (type) {
case "user":
return "magenta";
case "assistant":
return "cyan";
case "system":
return "blue";
case "tool":
return "yellow";
case "result":
return "green";
case "status":
return "gray";
default:
return "white";
}
};
const formatMessage = (msg) => {
const lines = msg.content.split("\n");
const maxLineLength = terminalWidth - 10;
return lines.map((line) => {
if (line.length <= maxLineLength) return line;
const chunks = [];
for (let i = 0; i < line.length; i += maxLineLength) {
chunks.push(line.slice(i, i + maxLineLength));
}
return chunks.join("\n");
}).join("\n");
};
return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
ink.Box,
{
flexDirection: "column",
width: terminalWidth,
height: terminalHeight - 4,
borderStyle: "round",
borderColor: "gray",
paddingX: 1,
overflow: "hidden"
},
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "\u{1F4E1} Remote Mode - Claude Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
// Show only the last messages that fit in the available space
messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
))
), /* @__PURE__ */ React.createElement(
ink.Box,
{
width: terminalWidth,
borderStyle: "round",
borderColor: actionInProgress ? "gray" : confirmationMode === "exit" ? "red" : confirmationMode === "switch" ? "yellow" : "green",
paddingX: 2,
justifyContent: "center",
alignItems: "center",
flexDirection: "column"
},
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress === "exiting" ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting...") : actionInProgress === "switching" ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Switching to local mode...") : confirmationMode === "exit" ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit completely") : confirmationMode === "switch" ? /* @__PURE__ */ React.createElement(ink.Text, { color: "yellow", bold: true }, "\u23F8\uFE0F Press space again to switch to local mode") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "green", bold: true }, "\u{1F4F1} Press space to switch to local mode \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
));
};
class Stream {
constructor(returned) {
this.returned = returned;