UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

1,519 lines (1,493 loc) 276 kB
'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