consortium
Version:
Remote control and session sharing CLI for AI coding agents
1,519 lines (1,493 loc) • 276 kB
JavaScript
'use strict';
var chalk = require('chalk');
var os = require('node:os');
var node_crypto = require('node:crypto');
var api = require('./types-XjAAlKci.cjs');
var node_child_process = require('node:child_process');
var node_path = require('node:path');
var node_readline = require('node:readline');
var fs = require('node:fs');
var promises = require('node:fs/promises');
var fs$1 = require('fs/promises');
var ink = require('ink');
var React = require('react');
var node_url = require('node:url');
var axios = require('axios');
require('node:events');
require('socket.io-client');
var tweetnacl = require('tweetnacl');
require('expo-server-sdk');
var node_util = require('node:util');
var persistence = require('./persistence-ByBDgr7f.cjs');
var crypto = require('crypto');
var child_process = require('child_process');
var fs$2 = require('fs');
var path = require('path');
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 mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
var node_http = require('node:http');
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
var http = require('http');
var util = require('util');
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 tmp__namespace = /*#__PURE__*/_interopNamespaceDefault(tmp);
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 = [];
api.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
}));
api.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;
api.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") {
api.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++;
api.logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
} else {
api.logger.debug("[Session] Consumed --resume flag (no session ID)");
}
} else {
api.logger.debug("[Session] Consumed --resume flag (no session ID)");
}
continue;
}
filteredArgs.push(arg);
}
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
api.logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
};
}
function getProjectPath(workingDirectory) {
const projectId = node_path.resolve(workingDirectory).replace(/[\\\/\.: _]/g, "-");
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || node_path.join(os.homedir(), ".claude");
return node_path.join(claudeConfigDir, "projects", projectId);
}
function claudeCheckSession(sessionId, path) {
const projectDir = getProjectPath(path);
const sessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
const sessionExists = fs.existsSync(sessionFile);
if (!sessionExists) {
api.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) {
api.logger.debug(`[claudeCheckSession] Malformed JSON at line ${index + 1}:`, e);
return false;
}
});
api.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(node_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) {
api.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 || node_path.join(os.homedir(), ".claude");
return node_path.join(claudeConfigDir, "settings.json");
}
function readClaudeSettings() {
try {
const settingsPath = getClaudeSettingsPath();
if (!fs.existsSync(settingsPath)) {
api.logger.debug(`[ClaudeSettings] No Claude settings file found at ${settingsPath}`);
return null;
}
const settingsContent = fs.readFileSync(settingsPath, "utf-8");
const settings = JSON.parse(settingsContent);
api.logger.debug(`[ClaudeSettings] Successfully read Claude settings from ${settingsPath}`);
api.logger.debug(`[ClaudeSettings] includeCoAuthoredBy: ${settings.includeCoAuthoredBy}`);
return settings;
} catch (error) {
api.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 BASE_SYSTEM_PROMPT = (() => trimIdent(`
ALWAYS when you start a new chat - you must call a tool "mcp__consortium__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
`))();
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 = (() => {
const includeCoAuthored = shouldIncludeCoAuthoredBy();
if (includeCoAuthored) {
return BASE_SYSTEM_PROMPT + "\n\n" + CO_AUTHORED_CREDITS;
} else {
return BASE_SYSTEM_PROMPT;
}
})();
class ExitCodeError extends Error {
exitCode;
constructor(exitCode) {
super(`Process exited with code: ${exitCode}`);
this.name = "ExitCodeError";
this.exitCode = exitCode;
}
}
const claudeCliPath = node_path.resolve(node_path.join(api.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;
api.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;
api.logger.debug(`[ClaudeLocal] Using provided session ID from --resume: ${startFrom}`);
} else {
const lastSession = claudeFindLastSession(opts.path);
if (lastSession) {
startFrom = lastSession;
api.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;
api.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) {
api.logger.debug(`[ClaudeLocal] Resuming session: ${startFrom}`);
opts.onSessionFound(startFrom);
} else if (explicitSessionId) {
api.logger.debug(`[ClaudeLocal] Using explicit session ID: ${explicitSessionId}`);
opts.onSessionFound(explicitSessionId);
} else {
api.logger.debug(`[ClaudeLocal] Generated new session ID: ${newSessionId}`);
opts.onSessionFound(newSessionId);
}
} else {
if (startFrom) {
api.logger.debug(`[ClaudeLocal] Will resume existing session: ${startFrom}`);
} else if (hasUserSessionControl) {
api.logger.debug(`[ClaudeLocal] User passed ${hasContinueFlag ? "--continue" : "--resume"} flag, session ID will be determined by hook`);
} else {
api.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;
api.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);
api.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
};
api.logger.debug(`[ClaudeLocal] Spawning launcher: ${claudeCliPath}`);
api.logger.debug(`[ClaudeLocal] Args: ${JSON.stringify(args)}`);
const child = node_child_process.spawn("node", [claudeCliPath, ...args], {
stdio: ["inherit", "inherit", "inherit", "pipe"],
signal: opts.abort,
cwd: opts.path,
env
});
if (child.stdio[3]) {
const rl = node_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:
api.logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
}
} catch (e) {
api.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 api.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();
void (async () => {
while (true) {
try {
api.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;
}
api.logger.debug(`[FILE_WATCHER] File changed: ${file}`);
onFileChange(file);
}
} catch (e) {
if (abortController.signal.aborted) {
return;
}
api.logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
await api.delay(1e3);
}
}
})();
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);
api.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);
api.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) {
api.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)) {
api.logger.debug(`[SESSION_SCANNER] Starting watcher for session: ${p}`);
watchers.set(p, startFileWatcher(node_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) {
api.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
return;
}
if (finishedSessions.has(sessionId)) {
api.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
return;
}
if (pendingSessions.has(sessionId)) {
api.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
return;
}
if (currentSessionId) {
pendingSessions.add(currentSessionId);
}
api.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 = node_path.join(projectDir, `${sessionId}.jsonl`);
api.logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
let file;
try {
file = await promises.readFile(expectedSessionFile, "utf-8");
} catch (error) {
api.logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
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 = api.RawJSONLinesSchema.safeParse(message);
if (!parsed.success) {
continue;
}
messages.push(parsed.data);
} catch (e) {
api.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() {
api.logger.debug("[local]: doAbort");
if (!exitReason) {
exitReason = { type: "switch" };
}
session.queue.reset();
await abort();
}
async function doSwitch() {
api.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;
}
api.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) {
api.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;
}
}
api.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;
}
queue = [];
readResolve;
readReject;
isDone = false;
hasError;
started = false;
/**
* Implements async iterable protocol
*/
[Symbol.asyncIterator]() {
if (this.started) {
throw new Error("Stream can only be iterated once");
}
this.started = true;
return this;
}
/**
* Gets the next value from the stream
*/
async next() {
if (this.queue.length > 0) {
return Promise.resolve({
done: false,
value: this.queue.shift()
});
}
if (this.isDone) {
return Promise.resolve({ done: true, value: void 0 });
}
if (this.hasError) {
return Promise.reject(this.hasError);
}
return new Promise((resolve, reject) => {
this.readResolve = resolve;
this.readReject = reject;
});
}
/**
* Adds a value to the stream
*/
enqueue(value) {
if (this.readResolve) {
const resolve = this.readResolve;
this.readResolve = void 0;
this.readReject = void 0;
resolve({ done: false, value });
} else {
this.queue.push(value);
}
}
/**
* Marks the stream as complete
*/
done() {
this.isDone = true;
if (this.readResolve) {
const resolve = this.readResolve;
this.readResolve = void 0;
this.readReject = void 0;
resolve({ done: true, value: void 0 });
}
}
/**
* Propagates an error through the stream
*/
error(error) {
this.hasError = error;
if (this.readReject) {
const reject = this.readReject;
this.readResolve = void 0;
this.readReject = void 0;
reject(error);
}
}
/**
* Implements async iterator cleanup
*/
async return() {
this.isDone = true;
if (this.returned) {
this.returned();
}
return Promise.resolve({ done: true, value: void 0 });
}
}
class AbortError extends Error {
constructor(message) {
super(message);
this.name = "AbortError";
}
}
let cachedRuntime = null;
function getRuntime() {
if (cachedRuntime) return cachedRuntime;
if (typeof globalThis.Bun !== "undefined") {
cachedRuntime = "bun";
return cachedRuntime;
}
if (typeof globalThis.Deno !== "undefined") {
cachedRuntime = "deno";
return cachedRuntime;
}
if (process?.versions?.bun) {
cachedRuntime = "bun";
return cachedRuntime;
}
if (process?.versions?.deno) {
cachedRuntime = "deno";
return cachedRuntime;
}
if (process?.versions?.node) {
cachedRuntime = "node";
return cachedRuntime;
}
cachedRuntime = "unknown";
return cachedRuntime;
}
const isBun = () => getRuntime() === "bun";
const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-Chfz7o-q.cjs', document.baseURI).href)));
const __dirname$1 = node_path.join(__filename$1, "..");
function getGlobalClaudeVersion() {
try {
const cleanEnv = getCleanEnv();
const output = node_child_process.execSync("claude --version", {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
cwd: os.homedir(),
env: cleanEnv
}).trim();
const match = output.match(/(\d+\.\d+\.\d+)/);
api.logger.debug(`[Claude SDK] Global claude --version output: ${output}`);
return match ? match[1] : null;
} catch {
return null;
}
}
function getCleanEnv() {
const env = { ...process.env };
const cwd = process.cwd();
const pathSep = process.platform === "win32" ? ";" : ":";
const pathKey = process.platform === "win32" ? "Path" : "PATH";
const actualPathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") || pathKey;
if (env[actualPathKey]) {
const cleanPath = env[actualPathKey].split(pathSep).filter((p) => {
const normalizedP = p.replace(/\\/g, "/").toLowerCase();
const normalizedCwd = cwd.replace(/\\/g, "/").toLowerCase();
return !normalizedP.startsWith(normalizedCwd);
}).join(pathSep);
env[actualPathKey] = cleanPath;
api.logger.debug(`[Claude SDK] Cleaned PATH, removed local paths from: ${cwd}`);
}
if (isBun()) {
Object.keys(env).forEach((key) => {
if (key.startsWith("BUN_")) {
delete env[key];
}
});
api.logger.debug("[Claude SDK] Removed Bun-specific environment variables for Node.js compatibility");
}
return env;
}
function findGlobalClaudePath() {
const homeDir = os.homedir();
const cleanEnv = getCleanEnv();
try {
node_child_process.execSync("claude --version", {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
cwd: homeDir,
env: cleanEnv
});
api.logger.debug("[Claude SDK] Global claude command available (checked with clean PATH)");
return "claude";
} catch {
}
if (process.platform !== "win32") {
try {
const result = node_child_process.execSync("which claude", {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
cwd: homeDir,
env: cleanEnv
}).trim();
if (result && fs.existsSync(result)) {
api.logger.debug(`[Claude SDK] Found global claude path via which: ${result}`);
return result;
}
} catch {
}
}
return null;
}
function getDefaultClaudeCodePath() {
const nodeModulesPath = node_path.join(__dirname$1, "..", "..", "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
if (process.env.CONSORTIUM_CLAUDE_PATH) {
api.logger.debug(`[Claude SDK] Using CONSORTIUM_CLAUDE_PATH: ${process.env.CONSORTIUM_CLAUDE_PATH}`);
return process.env.CONSORTIUM_CLAUDE_PATH;
}
if (process.env.CONSORTIUM_USE_BUNDLED_CLAUDE === "1") {
api.logger.debug(`[Claude SDK] Forced bundled version: ${nodeModulesPath}`);
return nodeModulesPath;
}
const globalPath = findGlobalClaudePath();
if (!globalPath) {
api.logger.debug(`[Claude SDK] No global claude found, using bundled: ${nodeModulesPath}`);
return nodeModulesPath;
}
const globalVersion = getGlobalClaudeVersion();
api.logger.debug(`[Claude SDK] Global version: ${globalVersion || "unknown"}`);
if (!globalVersion) {
api.logger.debug(`[Claude SDK] Cannot compare versions, using global: ${globalPath}`);
return globalPath;
}
return globalPath;
}
function logDebug(message) {
if (process.env.DEBUG) {
api.logger.debug(message);
console.log(message);
}
}
async function streamToStdin(stream, stdin, abort) {
for await (const message of stream) {
if (abort?.aborted) break;
stdin.write(JSON.stringify(message) + "\n");
}
stdin.end();
}
class Query {
constructor(childStdin, childStdout, processExitPromise, canCallTool) {
this.childStdin = childStdin;
this.childStdout = childStdout;
this.processExitPromise = processExitPromise;
this.canCallTool = canCallTool;
this.readMessages();
this.sdkMessages = this.readSdkMessages();
}
pendingControlResponses = /* @__PURE__ */ new Map();
cancelControllers = /* @__PURE__ */ new Map();
sdkMessages;
inputStream = new Stream();
canCallTool;
/**
* Set an error on the stream
*/
setError(error) {
this.inputStream.error(error);
}
/**
* AsyncIterableIterator implementation
*/
next(...args) {
return this.sdkMessages.next(...args);
}
return(value) {
if (this.sdkMessages.return) {
return this.sdkMessages.return(value);
}
return Promise.resolve({ done: true, value: void 0 });
}
throw(e) {
if (this.sdkMessages.throw) {
return this.sdkMessages.throw(e);
}
return Promise.reject(e);
}
[Symbol.asyncIterator]() {
return this.sdkMessages;
}
/**
* Read messages from Claude process stdout
*/
async readMessages() {
const rl = node_readline.createInterface({ input: this.childStdout });
try {
for await (const line of rl) {
if (line.trim()) {
try {
const message = JSON.parse(line);
if (message.type === "control_response") {
const controlResponse = message;
const handler = this.pendingControlResponses.get(controlResponse.response.request_id);
if (handler) {
handler(controlResponse.response);
}
continue;
} else if (message.type === "control_request") {
await this.handleControlRequest(message);
continue;
} else if (message.type === "control_cancel_request") {
this.handleControlCancelRequest(message);
continue;
}
this.inputStream.enqueue(message);
} catch (e) {
api.logger.debug(line);
}
}
}
await this.processExitPromise;
} catch (error) {
this.inputStream.error(error);
} finally {
this.inputStream.done();
this.cleanupControllers();
rl.close();
}
}
/**
* Async generator for SDK messages
*/
async *readSdkMessages() {
for await (const message of this.inputStream) {
yield message;
}
}
/**
* Send interrupt request to Claude
*/
async interrupt() {
if (!this.childStdin) {
throw new Error("Interrupt requires --input-format stream-json");
}
await this.request({
subtype: "interrupt"
}, this.childStdin);
}
/**
* Send control request to Claude process
*/
request(request, childStdin) {
const requestId = Math.random().toString(36).substring(2, 15);
const sdkRequest = {
request_id: requestId,
type: "control_request",
request
};
return new Promise((resolve, reject) => {
this.pendingControlResponses.set(requestId, (response) => {
if (response.subtype === "success") {
resolve(response);
} else {
reject(new Error(response.error));
}
});
childStdin.write(JSON.stringify(sdkRequest) + "\n");
});
}
/**
* Handle incoming control requests for tool permissions
* Replicates the exact logic from the SDK's handleControlRequest method
*/
async handleControlRequest(request) {
if (!this.childStdin) {
logDebug("Cannot handle control request - no stdin available");
return;
}
const controller = new AbortController();
this.cancelControllers.set(request.request_id, controller);
try {
const response = await this.processControlRequest(request, controller.signal);
const controlResponse = {
type: "control_response",
response: {
subtype: "success",
request_id: request.request_id,
response
}
};
this.childStdin.write(JSON.stringify(controlResponse) + "\n");
} catch (error) {
const controlErrorResponse = {
type: "control_response",
response: {
subtype: "error",
request_id: request.request_id,
error: error instanceof Error ? error.message : String(error)
}
};
this.childStdin.write(JSON.stringify(controlErrorResponse) + "\n");
} finally {
this.cancelControllers.delete(request.request_id);
}
}
/**
* Handle control cancel requests
* Replicates the exact logic from the SDK's handleControlCancelRequest method
*/
handleControlCancelRequest(request) {
const controller = this.cancelControllers.get(request.request_id);
if (controller) {
controller.abort();
this.cancelControllers.delete(request.request_id);
}
}
/**
* Process control requests based on subtype
* Replicates the exact logic from the SDK's processControlRequest method
*/
async processControlRequ