consortium
Version:
Remote control and session sharing CLI for AI coding agents
1,518 lines (1,503 loc) • 85.9 kB
JavaScript
import axios from 'axios';
import chalk from 'chalk';
import { appendFileSync } from 'fs';
import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, basename } from 'node:path';
import { EventEmitter } from 'node:events';
import { io } from 'socket.io-client';
import { z } from 'zod';
import { randomBytes, createCipheriv, createDecipheriv, randomUUID } from 'node:crypto';
import tweetnacl from 'tweetnacl';
import { spawn, exec } from 'child_process';
import { promisify } from 'util';
import { readFile, stat, writeFile, readdir } from 'fs/promises';
import { createHash } from 'crypto';
import { dirname, resolve, join as join$1 } from 'path';
import { fileURLToPath } from 'url';
import { platform } from 'os';
import { Expo } from 'expo-server-sdk';
var name = "consortium";
var version = "0.1.0";
var description = "Remote control and session sharing CLI for AI coding agents";
var author = "ConsortiumAI";
var license = "MIT";
var type = "module";
var homepage = "https://github.com/ConsortiumAI/consortium-cli";
var bugs = "https://github.com/ConsortiumAI/consortium-cli/issues";
var repository = "ConsortiumAI/consortium-cli";
var bin = {
consortium: "./bin/consortium.mjs",
"consortium-mcp": "./bin/consortium-mcp.mjs"
};
var main = "./dist/index.cjs";
var module$1 = "./dist/index.mjs";
var types = "./dist/index.d.cts";
var exports$1 = {
".": {
require: {
types: "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"import": {
types: "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"./lib": {
require: {
types: "./dist/lib.d.cts",
"default": "./dist/lib.cjs"
},
"import": {
types: "./dist/lib.d.mts",
"default": "./dist/lib.mjs"
}
},
"./codex/consortiumMcpStdioBridge": {
require: {
types: "./dist/codex/consortiumMcpStdioBridge.d.cts",
"default": "./dist/codex/consortiumMcpStdioBridge.cjs"
},
"import": {
types: "./dist/codex/consortiumMcpStdioBridge.d.mts",
"default": "./dist/codex/consortiumMcpStdioBridge.mjs"
}
}
};
var files = [
"dist",
"bin",
"scripts",
"tools",
"package.json"
];
var scripts = {
"why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started consortium",
typecheck: "tsc --noEmit",
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
test: "$npm_execpath run build && vitest run",
start: "$npm_execpath run build && node ./bin/consortium.mjs",
dev: "tsx src/index.ts",
"dev:local-server": "$npm_execpath run build && tsx --env-file .env.dev-local-server src/index.ts",
"dev:integration-test-env": "$npm_execpath run build && tsx --env-file .env.integration-test src/index.ts",
prepublishOnly: "$npm_execpath run build && $npm_execpath test",
release: "$npm_execpath install && release-it",
postinstall: "node scripts/unpack-tools.cjs",
"// ==== Dev/Stable Variant Management ====": "",
stable: "node scripts/env-wrapper.cjs stable",
"dev:variant": "node scripts/env-wrapper.cjs dev",
"// ==== Stable Version Quick Commands ====": "",
"stable:daemon:start": "node scripts/env-wrapper.cjs stable daemon start",
"stable:daemon:stop": "node scripts/env-wrapper.cjs stable daemon stop",
"stable:daemon:status": "node scripts/env-wrapper.cjs stable daemon status",
"stable:auth": "node scripts/env-wrapper.cjs stable auth",
"// ==== Development Version Quick Commands ====": "",
"dev:daemon:start": "node scripts/env-wrapper.cjs dev daemon start",
"dev:daemon:stop": "node scripts/env-wrapper.cjs dev daemon stop",
"dev:daemon:status": "node scripts/env-wrapper.cjs dev daemon status",
"dev:auth": "node scripts/env-wrapper.cjs dev auth",
"// ==== Setup ====": "",
"setup:dev": "node scripts/setup-dev.cjs",
doctor: "node scripts/env-wrapper.cjs stable doctor",
"// ==== Development Linking ====": "",
"link:dev": "node scripts/link-dev.cjs",
"unlink:dev": "node scripts/link-dev.cjs unlink"
};
var dependencies = {
"@agentclientprotocol/sdk": "^0.8.0",
"@modelcontextprotocol/sdk": "^1.25.3",
"@stablelib/base64": "^2.0.1",
"@stablelib/hex": "^2.0.1",
"@types/cross-spawn": "^6.0.6",
"@types/http-proxy": "^1.17.17",
"@types/ps-list": "^6.2.1",
"@types/qrcode-terminal": "^0.12.2",
"@types/react": "^19.2.7",
"@types/tmp": "^0.2.6",
ai: "^5.0.107",
axios: "^1.13.2",
chalk: "^5.6.2",
"cross-spawn": "^7.0.6",
"expo-server-sdk": "^3.15.0",
fastify: "^5.6.2",
"fastify-type-provider-zod": "4.0.2",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^3.0.5",
ink: "^6.5.1",
open: "^10.2.0",
"ps-list": "^8.1.1",
"qrcode-terminal": "^0.12.0",
react: "^19.2.0",
"socket.io-client": "^4.8.1",
tar: "^7.5.2",
tmp: "^0.2.5",
tweetnacl: "^1.0.3",
zod: "3.25.76"
};
var devDependencies = {
"@eslint/compat": "^1",
"@types/node": ">=20",
"cross-env": "^10.1.0",
dotenv: "^16.6.1",
eslint: "^9",
"eslint-config-prettier": "^10",
pkgroll: "^2.14.2",
"release-it": "^19.0.6",
shx: "^0.3.3",
"ts-node": "^10",
tsx: "^4.20.6",
typescript: "5.9.3",
vitest: "^3.2.4"
};
var resolutions = {
"whatwg-url": "14.2.0",
"parse-path": "7.0.3",
"@types/parse-path": "7.0.3"
};
var publishConfig = {
registry: "https://registry.npmjs.org"
};
var packageManager = "yarn@1.22.22";
var packageJson = {
name: name,
version: version,
description: description,
author: author,
license: license,
type: type,
homepage: homepage,
bugs: bugs,
repository: repository,
bin: bin,
main: main,
module: module$1,
types: types,
exports: exports$1,
files: files,
scripts: scripts,
dependencies: dependencies,
devDependencies: devDependencies,
resolutions: resolutions,
publishConfig: publishConfig,
packageManager: packageManager
};
class Configuration {
serverUrl;
webappUrl;
isDaemonProcess;
// Directories and paths (from persistence)
consortiumHomeDir;
logsDir;
settingsFile;
privateKeyFile;
daemonStateFile;
daemonLockFile;
currentCliVersion;
isExperimentalEnabled;
disableCaffeinate;
constructor() {
this.serverUrl = process.env.CONSORTIUM_SERVER_URL || "https://api.consortium.dev";
this.webappUrl = process.env.CONSORTIUM_WEBAPP_URL || "https://app.consortium.dev";
const args = process.argv.slice(2);
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
if (process.env.CONSORTIUM_HOME_DIR) {
const expandedPath = process.env.CONSORTIUM_HOME_DIR.replace(/^~/, homedir());
this.consortiumHomeDir = expandedPath;
} else {
this.consortiumHomeDir = join(homedir(), ".consortium");
}
this.logsDir = join(this.consortiumHomeDir, "logs");
this.settingsFile = join(this.consortiumHomeDir, "settings.json");
this.privateKeyFile = join(this.consortiumHomeDir, "access.key");
this.daemonStateFile = join(this.consortiumHomeDir, "daemon.state.json");
this.daemonLockFile = join(this.consortiumHomeDir, "daemon.state.json.lock");
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.CONSORTIUM_EXPERIMENTAL?.toLowerCase() || "");
this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.CONSORTIUM_DISABLE_CAFFEINATE?.toLowerCase() || "");
this.currentCliVersion = packageJson.version;
const variant = process.env.CONSORTIUM_VARIANT || "stable";
if (variant === "dev" && !this.consortiumHomeDir.includes("dev")) {
console.warn('\u26A0\uFE0F WARNING: CONSORTIUM_VARIANT=dev but CONSORTIUM_HOME_DIR does not contain "dev"');
console.warn(` Current: ${this.consortiumHomeDir}`);
console.warn(` Expected: Should contain "dev" (e.g., ~/.consortium-dev)`);
}
if (!this.isDaemonProcess && variant === "dev") {
console.log("\x1B[33m\u{1F527} DEV MODE\x1B[0m - Data: " + this.consortiumHomeDir);
}
if (!existsSync(this.consortiumHomeDir)) {
mkdirSync(this.consortiumHomeDir, { recursive: true });
}
if (!existsSync(this.logsDir)) {
mkdirSync(this.logsDir, { recursive: true });
}
}
}
const configuration = new Configuration();
function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
return date.toLocaleString("sv-SE", {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
}).replace(/[: ]/g, "-").replace(/,/g, "") + "-pid-" + process.pid;
}
function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
return date.toLocaleTimeString("en-US", {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3
});
}
function getSessionLogPath() {
const timestamp = createTimestampForFilename();
const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
return join(configuration.logsDir, filename);
}
class Logger {
constructor(logFilePath = getSessionLogPath()) {
this.logFilePath = logFilePath;
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && process.env.CONSORTIUM_SERVER_URL) {
this.dangerouslyUnencryptedServerLoggingUrl = process.env.CONSORTIUM_SERVER_URL;
console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
}
}
dangerouslyUnencryptedServerLoggingUrl;
// Use local timezone for simplicity of locating the logs,
// in practice you will not need absolute timestamps
localTimezoneTimestamp() {
return createTimestampForLogEntry();
}
debug(message, ...args) {
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
}
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
if (!process.env.DEBUG) {
this.debug(`In production, skipping message inspection`);
}
const truncateStrings = (obj) => {
if (typeof obj === "string") {
return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
}
if (Array.isArray(obj)) {
const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
if (obj.length > maxArrayLength) {
truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
}
return truncatedArray;
}
if (obj && typeof obj === "object") {
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (key === "usage") {
continue;
}
result[key] = truncateStrings(value);
}
return result;
}
return obj;
};
const truncatedObject = truncateStrings(object);
const json = JSON.stringify(truncatedObject, null, 2);
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
}
info(message, ...args) {
this.logToConsole("info", "", message, ...args);
this.debug(message, args);
}
infoDeveloper(message, ...args) {
this.debug(message, ...args);
if (process.env.DEBUG) {
this.logToConsole("info", "[DEV]", message, ...args);
}
}
warn(message, ...args) {
this.logToConsole("warn", "", message, ...args);
this.debug(`[WARN] ${message}`, ...args);
}
getLogPath() {
return this.logFilePath;
}
logToConsole(level, prefix, message, ...args) {
switch (level) {
case "debug": {
console.log(chalk.gray(prefix), message, ...args);
break;
}
case "error": {
console.error(chalk.red(prefix), message, ...args);
break;
}
case "info": {
console.log(chalk.blue(prefix), message, ...args);
break;
}
case "warn": {
console.log(chalk.yellow(prefix), message, ...args);
break;
}
default: {
this.debug("Unknown log level:", level);
console.log(chalk.blue(prefix), message, ...args);
break;
}
}
}
async sendToRemoteServer(level, message, ...args) {
if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
try {
await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
level,
message: `${message} ${args.map(
(a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)
).join(" ")}`,
source: "cli",
platform: process.platform
})
});
} catch (error) {
}
}
logToFile(prefix, message, ...args) {
const logLine = `${prefix} ${message} ${args.map(
(arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
).join(" ")}
`;
if (this.dangerouslyUnencryptedServerLoggingUrl) {
let level = "info";
if (prefix.includes(this.localTimezoneTimestamp())) {
level = "debug";
}
this.sendToRemoteServer(level, message, ...args).catch(() => {
});
}
try {
appendFileSync(this.logFilePath, logLine);
} catch (appendError) {
if (process.env.DEBUG) {
console.error("[DEV MODE ONLY THROWING] Failed to append to log file:", appendError);
throw appendError;
}
}
}
}
let logger = new Logger();
async function listDaemonLogFiles(limit = 50) {
try {
const logsDir = configuration.logsDir;
if (!existsSync(logsDir)) {
return [];
}
const logs = readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
const fullPath = join(logsDir, file);
const stats = statSync(fullPath);
return { file, path: fullPath, modified: stats.mtime };
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
try {
const { readDaemonState } = await import('./persistence-CJJbK-4u.mjs');
const state = await readDaemonState();
if (!state) {
return logs;
}
if (state.daemonLogPath && existsSync(state.daemonLogPath)) {
const stats = statSync(state.daemonLogPath);
const persisted = {
file: basename(state.daemonLogPath),
path: state.daemonLogPath,
modified: stats.mtime
};
const idx = logs.findIndex((l) => l.path === persisted.path);
if (idx >= 0) {
const [found] = logs.splice(idx, 1);
logs.unshift(found);
} else {
logs.unshift(persisted);
}
}
} catch {
}
return logs.slice(0, Math.max(0, limit));
} catch {
return [];
}
}
async function getLatestDaemonLog() {
const [latest] = await listDaemonLogFiles(1);
return latest || null;
}
const SessionMessageContentSchema = z.object({
c: z.string(),
// Base64 encoded encrypted content
t: z.literal("encrypted")
});
const UpdateBodySchema = z.object({
message: z.object({
id: z.string(),
seq: z.number(),
content: SessionMessageContentSchema
}),
sid: z.string(),
// Session ID
t: z.literal("new-message")
});
const UpdateSessionBodySchema = z.object({
t: z.literal("update-session"),
sid: z.string(),
metadata: z.object({
version: z.number(),
value: z.string()
}).nullish(),
agentState: z.object({
version: z.number(),
value: z.string()
}).nullish()
});
const UpdateMachineBodySchema = z.object({
t: z.literal("update-machine"),
machineId: z.string(),
metadata: z.object({
version: z.number(),
value: z.string()
}).nullish(),
daemonState: z.object({
version: z.number(),
value: z.string()
}).nullish()
});
z.object({
id: z.string(),
seq: z.number(),
body: z.union([
UpdateBodySchema,
UpdateSessionBodySchema,
UpdateMachineBodySchema
]),
createdAt: z.number()
});
z.object({
host: z.string(),
platform: z.string(),
consortiumCliVersion: z.string(),
homeDir: z.string(),
consortiumHomeDir: z.string(),
consortiumLibDir: z.string()
});
z.object({
status: z.union([
z.enum(["running", "shutting-down"]),
z.string()
// Forward compatibility
]),
pid: z.number().optional(),
httpPort: z.number().optional(),
startedAt: z.number().optional(),
shutdownRequestedAt: z.number().optional(),
shutdownSource: z.union([
z.enum(["mobile-app", "cli", "os-signal", "unknown"]),
z.string()
// Forward compatibility
]).optional()
});
z.object({
content: SessionMessageContentSchema,
createdAt: z.number(),
id: z.string(),
seq: z.number(),
updatedAt: z.number()
});
const MessageMetaSchema = z.object({
sentFrom: z.string().optional(),
// Source identifier
permissionMode: z.enum(["default", "acceptEdits", "bypassPermissions", "plan", "read-only", "safe-yolo", "yolo"]).optional(),
// Permission mode for this message
model: z.string().nullable().optional(),
// Model name for this message (null = reset)
fallbackModel: z.string().nullable().optional(),
// Fallback model for this message (null = reset)
customSystemPrompt: z.string().nullable().optional(),
// Custom system prompt for this message (null = reset)
appendSystemPrompt: z.string().nullable().optional(),
// Append to system prompt for this message (null = reset)
allowedTools: z.array(z.string()).nullable().optional(),
// Allowed tools for this message (null = reset)
disallowedTools: z.array(z.string()).nullable().optional()
// Disallowed tools for this message (null = reset)
});
z.object({
session: z.object({
id: z.string(),
tag: z.string(),
seq: z.number(),
createdAt: z.number(),
updatedAt: z.number(),
metadata: z.string(),
metadataVersion: z.number(),
agentState: z.string().nullable(),
agentStateVersion: z.number()
})
});
const UserMessageSchema = z.object({
role: z.literal("user"),
content: z.object({
type: z.literal("text"),
text: z.string()
}),
localKey: z.string().optional(),
// Mobile messages include this
meta: MessageMetaSchema.optional()
});
const AgentMessageSchema = z.object({
role: z.literal("agent"),
content: z.object({
type: z.literal("output"),
data: z.any()
}),
meta: MessageMetaSchema.optional()
});
z.union([UserMessageSchema, AgentMessageSchema]);
function encodeBase64(buffer, variant = "base64") {
if (variant === "base64url") {
return encodeBase64Url(buffer);
}
return Buffer.from(buffer).toString("base64");
}
function encodeBase64Url(buffer) {
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
}
function decodeBase64(base64, variant = "base64") {
if (variant === "base64url") {
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
return new Uint8Array(Buffer.from(base64Standard, "base64"));
}
return new Uint8Array(Buffer.from(base64, "base64"));
}
function getRandomBytes(size) {
return new Uint8Array(randomBytes(size));
}
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
const ephemeralKeyPair = tweetnacl.box.keyPair();
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
result.set(ephemeralKeyPair.publicKey, 0);
result.set(nonce, ephemeralKeyPair.publicKey.length);
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
return result;
}
function encryptLegacy(data, secret) {
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
const result = new Uint8Array(nonce.length + encrypted.length);
result.set(nonce);
result.set(encrypted, nonce.length);
return result;
}
function decryptLegacy(data, secret) {
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
if (!decrypted) {
return null;
}
return JSON.parse(new TextDecoder().decode(decrypted));
}
function encryptWithDataKey(data, dataKey) {
const nonce = getRandomBytes(12);
const cipher = createCipheriv("aes-256-gcm", dataKey, nonce);
const plaintext = new TextEncoder().encode(JSON.stringify(data));
const encrypted = Buffer.concat([
cipher.update(plaintext),
cipher.final()
]);
const authTag = cipher.getAuthTag();
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
bundle.set([0], 0);
bundle.set(nonce, 1);
bundle.set(new Uint8Array(encrypted), 13);
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
return bundle;
}
function decryptWithDataKey(bundle, dataKey) {
if (bundle.length < 1) {
return null;
}
if (bundle[0] !== 0) {
return null;
}
if (bundle.length < 12 + 16 + 1) {
return null;
}
const nonce = bundle.slice(1, 13);
const authTag = bundle.slice(bundle.length - 16);
const ciphertext = bundle.slice(13, bundle.length - 16);
try {
const decipher = createDecipheriv("aes-256-gcm", dataKey, nonce);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
return JSON.parse(new TextDecoder().decode(decrypted));
} catch (error) {
return null;
}
}
function encrypt(key, variant, data) {
if (variant === "legacy") {
return encryptLegacy(data, key);
} else {
return encryptWithDataKey(data, key);
}
}
function decrypt(key, variant, data) {
if (variant === "legacy") {
return decryptLegacy(data, key);
} else {
return decryptWithDataKey(data, key);
}
}
async function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
return Math.round(Math.random() * maxDelayRet);
}
function createBackoff(opts) {
return async (callback) => {
let currentFailureCount = 0;
const minDelay = 250;
const maxDelay = 1e3;
const maxFailureCount = 50;
while (true) {
try {
return await callback();
} catch (e) {
if (currentFailureCount < maxFailureCount) {
currentFailureCount++;
}
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
await delay(waitForRequest);
}
}
};
}
let backoff = createBackoff();
class AsyncLock {
permits = 1;
promiseResolverQueue = [];
async inLock(func) {
try {
await this.lock();
return await func();
} finally {
this.unlock();
}
}
async lock() {
if (this.permits > 0) {
this.permits = this.permits - 1;
return;
}
await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
}
unlock() {
this.permits += 1;
if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
throw new Error("this.permits should never be > 0 when there is someone waiting.");
} else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
this.permits -= 1;
const nextResolver = this.promiseResolverQueue.shift();
if (nextResolver) {
setTimeout(() => {
nextResolver(true);
}, 0);
}
}
}
}
class RpcHandlerManager {
handlers = /* @__PURE__ */ new Map();
scopePrefix;
encryptionKey;
encryptionVariant;
logger;
socket = null;
constructor(config) {
this.scopePrefix = config.scopePrefix;
this.encryptionKey = config.encryptionKey;
this.encryptionVariant = config.encryptionVariant;
this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
}
/**
* Register an RPC handler for a specific method
* @param method - The method name (without prefix)
* @param handler - The handler function
*/
registerHandler(method, handler) {
const prefixedMethod = this.getPrefixedMethod(method);
this.handlers.set(prefixedMethod, handler);
if (this.socket) {
this.socket.emit("rpc-register", { method: prefixedMethod });
}
}
/**
* Handle an incoming RPC request
* @param request - The RPC request data
* @param callback - The response callback
*/
async handleRequest(request) {
try {
const handler = this.handlers.get(request.method);
if (!handler) {
this.logger("[RPC] [ERROR] Method not found", { method: request.method });
const errorResponse = { error: "Method not found" };
const encryptedError = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
return encryptedError;
}
const decryptedParams = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(request.params));
this.logger("[RPC] Calling handler", { method: request.method });
const result = await handler(decryptedParams);
this.logger("[RPC] Handler returned", { method: request.method, hasResult: result !== void 0 });
const encryptedResponse = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, result));
this.logger("[RPC] Sending encrypted response", { method: request.method, responseLength: encryptedResponse.length });
return encryptedResponse;
} catch (error) {
this.logger("[RPC] [ERROR] Error handling request", { error });
const errorResponse = {
error: error instanceof Error ? error.message : "Unknown error"
};
return encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
}
}
onSocketConnect(socket) {
this.socket = socket;
for (const [prefixedMethod] of this.handlers) {
socket.emit("rpc-register", { method: prefixedMethod });
}
}
onSocketDisconnect() {
this.socket = null;
}
/**
* Get the number of registered handlers
*/
getHandlerCount() {
return this.handlers.size;
}
/**
* Check if a handler is registered
* @param method - The method name (without prefix)
*/
hasHandler(method) {
const prefixedMethod = this.getPrefixedMethod(method);
return this.handlers.has(prefixedMethod);
}
/**
* Clear all handlers
*/
clearHandlers() {
this.handlers.clear();
this.logger("Cleared all RPC handlers");
}
/**
* Get the prefixed method name
* @param method - The method name
*/
getPrefixedMethod(method) {
return `${this.scopePrefix}:${method}`;
}
}
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
function projectPath() {
const path = resolve(__dirname$1, "..");
return path;
}
function run$1(args, options) {
const RUNNER_PATH = resolve(join$1(projectPath(), "scripts", "ripgrep_launcher.cjs"));
return new Promise((resolve2, reject) => {
const child = spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
stdio: ["pipe", "pipe", "pipe"],
cwd: options?.cwd
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
resolve2({
exitCode: code || 0,
stdout,
stderr
});
});
child.on("error", (err) => {
reject(err);
});
});
}
function getBinaryPath() {
const platformName = platform();
const binaryName = platformName === "win32" ? "difft.exe" : "difft";
return resolve(join$1(projectPath(), "tools", "unpacked", binaryName));
}
function run(args, options) {
const binaryPath = getBinaryPath();
return new Promise((resolve2, reject) => {
const child = spawn(binaryPath, args, {
stdio: ["pipe", "pipe", "pipe"],
cwd: options?.cwd,
env: {
...process.env,
// Force color output when needed
FORCE_COLOR: "1"
}
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
resolve2({
exitCode: code || 0,
stdout,
stderr
});
});
child.on("error", (err) => {
reject(err);
});
});
}
function validatePath(targetPath, workingDirectory) {
const resolvedTarget = resolve(workingDirectory, targetPath);
const resolvedWorkingDir = resolve(workingDirectory);
if (!resolvedTarget.startsWith(resolvedWorkingDir + "/") && resolvedTarget !== resolvedWorkingDir) {
return {
valid: false,
error: `Access denied: Path '${targetPath}' is outside the working directory`
};
}
return { valid: true };
}
const execAsync = promisify(exec);
function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
rpcHandlerManager.registerHandler("bash", async (data) => {
logger.debug("Shell command request:", data.command);
if (data.cwd && data.cwd !== "/") {
const validation = validatePath(data.cwd, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
}
try {
const options = {
cwd: data.cwd === "/" ? void 0 : data.cwd,
timeout: data.timeout || 3e4
// Default 30 seconds timeout
};
logger.debug("Shell command executing...", { cwd: options.cwd, timeout: options.timeout });
const { stdout, stderr } = await execAsync(data.command, options);
logger.debug("Shell command executed, processing result...");
const result = {
success: true,
stdout: stdout ? stdout.toString() : "",
stderr: stderr ? stderr.toString() : "",
exitCode: 0
};
logger.debug("Shell command result:", {
success: true,
exitCode: 0,
stdoutLen: result.stdout.length,
stderrLen: result.stderr.length
});
return result;
} catch (error) {
const execError = error;
if (execError.code === "ETIMEDOUT" || execError.killed) {
const result2 = {
success: false,
stdout: execError.stdout || "",
stderr: execError.stderr || "",
exitCode: typeof execError.code === "number" ? execError.code : -1,
error: "Command timed out"
};
logger.debug("Shell command timed out:", {
success: false,
exitCode: result2.exitCode,
error: "Command timed out"
});
return result2;
}
const result = {
success: false,
stdout: execError.stdout ? execError.stdout.toString() : "",
stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
exitCode: typeof execError.code === "number" ? execError.code : 1,
error: execError.message || "Command failed"
};
logger.debug("Shell command failed:", {
success: false,
exitCode: result.exitCode,
error: result.error,
stdoutLen: result.stdout.length,
stderrLen: result.stderr.length
});
return result;
}
});
rpcHandlerManager.registerHandler("readFile", async (data) => {
logger.debug("Read file request:", data.path);
const validation = validatePath(data.path, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
try {
const buffer = await readFile(data.path);
const content = buffer.toString("base64");
return { success: true, content };
} catch (error) {
logger.debug("Failed to read file:", error);
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
}
});
rpcHandlerManager.registerHandler("writeFile", async (data) => {
logger.debug("Write file request:", data.path);
const validation = validatePath(data.path, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
try {
if (data.expectedHash !== null && data.expectedHash !== void 0) {
try {
const existingBuffer = await readFile(data.path);
const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
if (existingHash !== data.expectedHash) {
return {
success: false,
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
};
}
} catch (error) {
const nodeError = error;
if (nodeError.code !== "ENOENT") {
throw error;
}
return {
success: false,
error: "File does not exist but hash was provided"
};
}
} else {
try {
await stat(data.path);
return {
success: false,
error: "File already exists but was expected to be new"
};
} catch (error) {
const nodeError = error;
if (nodeError.code !== "ENOENT") {
throw error;
}
}
}
const buffer = Buffer.from(data.content, "base64");
await writeFile(data.path, buffer);
const hash = createHash("sha256").update(buffer).digest("hex");
return { success: true, hash };
} catch (error) {
logger.debug("Failed to write file:", error);
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
}
});
rpcHandlerManager.registerHandler("listDirectory", async (data) => {
logger.debug("List directory request:", data.path);
const validation = validatePath(data.path, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
try {
const entries = await readdir(data.path, { withFileTypes: true });
const directoryEntries = await Promise.all(
entries.map(async (entry) => {
const fullPath = join$1(data.path, entry.name);
let type = "other";
let size;
let modified;
if (entry.isDirectory()) {
type = "directory";
} else if (entry.isFile()) {
type = "file";
}
try {
const stats = await stat(fullPath);
size = stats.size;
modified = stats.mtime.getTime();
} catch (error) {
logger.debug(`Failed to stat ${fullPath}:`, error);
}
return {
name: entry.name,
type,
size,
modified
};
})
);
directoryEntries.sort((a, b) => {
if (a.type === "directory" && b.type !== "directory") return -1;
if (a.type !== "directory" && b.type === "directory") return 1;
return a.name.localeCompare(b.name);
});
return { success: true, entries: directoryEntries };
} catch (error) {
logger.debug("Failed to list directory:", error);
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
}
});
rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
const validation = validatePath(data.path, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
async function buildTree(path, name, currentDepth) {
try {
const stats = await stat(path);
const node = {
name,
path,
type: stats.isDirectory() ? "directory" : "file",
size: stats.size,
modified: stats.mtime.getTime()
};
if (stats.isDirectory() && currentDepth < data.maxDepth) {
const entries = await readdir(path, { withFileTypes: true });
const children = [];
await Promise.all(
entries.map(async (entry) => {
if (entry.isSymbolicLink()) {
logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
return;
}
const childPath = join$1(path, entry.name);
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
if (childNode) {
children.push(childNode);
}
})
);
children.sort((a, b) => {
if (a.type === "directory" && b.type !== "directory") return -1;
if (a.type !== "directory" && b.type === "directory") return 1;
return a.name.localeCompare(b.name);
});
node.children = children;
}
return node;
} catch (error) {
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
return null;
}
}
try {
if (data.maxDepth < 0) {
return { success: false, error: "maxDepth must be non-negative" };
}
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
const tree = await buildTree(data.path, baseName, 0);
if (!tree) {
return { success: false, error: "Failed to access the specified path" };
}
return { success: true, tree };
} catch (error) {
logger.debug("Failed to get directory tree:", error);
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
}
});
rpcHandlerManager.registerHandler("ripgrep", async (data) => {
logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
if (data.cwd) {
const validation = validatePath(data.cwd, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
}
try {
const result = await run$1(data.args, { cwd: data.cwd });
return {
success: true,
exitCode: result.exitCode,
stdout: result.stdout.toString(),
stderr: result.stderr.toString()
};
} catch (error) {
logger.debug("Failed to run ripgrep:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to run ripgrep"
};
}
});
rpcHandlerManager.registerHandler("difftastic", async (data) => {
logger.debug("Difftastic request with args:", data.args, "cwd:", data.cwd);
if (data.cwd) {
const validation = validatePath(data.cwd, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
}
try {
const result = await run(data.args, { cwd: data.cwd });
return {
success: true,
exitCode: result.exitCode,
stdout: result.stdout.toString(),
stderr: result.stderr.toString()
};
} catch (error) {
logger.debug("Failed to run difftastic:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to run difftastic"
};
}
});
}
const PRICING = {
// --- Claude 4 & Future Models ---
"claude-4.5-opus": {
input: 5,
output: 25,
cache_write: 6.25,
cache_read: 0.5
},
"claude-4.1-opus": {
input: 15,
output: 75,
cache_write: 18.75,
cache_read: 1.5
},
"claude-4-opus": {
input: 15,
output: 75,
cache_write: 18.75,
cache_read: 1.5
},
"claude-4.5-sonnet": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
"claude-4-sonnet": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
"claude-4.5-haiku": {
input: 1,
output: 5,
cache_write: 1.25,
cache_read: 0.1
},
// --- Legacy / Claude 3 ---
"claude-3-opus-20240229": {
input: 15,
output: 75,
cache_write: 18.75,
cache_read: 1.5
},
"claude-3-sonnet-20240229": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
"claude-3-5-sonnet-20240620": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
// New Sonnet 3.5 updated model
"claude-3-5-sonnet-20241022": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
"claude-3-haiku-20240307": {
input: 0.25,
output: 1.25,
cache_write: 0.3125,
cache_read: 0.025
},
"claude-3-5-haiku-20241022": {
input: 0.8,
output: 4,
cache_write: 1,
// Approx based on 1.25x rule usually or custom
cache_read: 0.08
}
};
const DEFAULT_MODEL = "claude-3-5-sonnet-20241022";
function calculateCost(usage, modelId) {
let pricing = PRICING[modelId];
if (!pricing) {
if (modelId?.includes("opus")) {
if (modelId.includes("4.5")) pricing = PRICING["claude-4.5-opus"];
else if (modelId.includes("4.1")) pricing = PRICING["claude-4.1-opus"];
else if (modelId.includes("4")) pricing = PRICING["claude-4-opus"];
else pricing = PRICING["claude-3-opus-20240229"];
} else if (modelId?.includes("sonnet")) {
if (modelId.includes("4.5")) pricing = PRICING["claude-4.5-sonnet"];
else if (modelId.includes("4")) pricing = PRICING["claude-4-sonnet"];
else pricing = PRICING["claude-3-5-sonnet-20241022"];
} else if (modelId?.includes("haiku")) {
if (modelId.includes("4.5")) pricing = PRICING["claude-4.5-haiku"];
else if (modelId.includes("3.5")) pricing = PRICING["claude-3-5-haiku-20241022"];
else pricing = PRICING["claude-3-haiku-20240307"];
} else pricing = PRICING[DEFAULT_MODEL];
}
const inputCost = usage.input_tokens / 1e6 * pricing.input;
const outputCost = usage.output_tokens / 1e6 * pricing.output;
const cacheWriteCost = (usage.cache_creation_input_tokens || 0) / 1e6 * pricing.cache_write;
const cacheReadCost = (usage.cache_read_input_tokens || 0) / 1e6 * pricing.cache_read;
const totalInputCost = inputCost + cacheWriteCost + cacheReadCost;
return {
total: totalInputCost + outputCost,
input: totalInputCost,
output: outputCost
};
}
class ApiSessionClient extends EventEmitter {
token;
sessionId;
metadata;
metadataVersion;
agentState;
agentStateVersion;
socket;
pendingMessages = [];
pendingMessageCallback = null;
rpcHandlerManager;
agentStateLock = new AsyncLock();
metadataLock = new AsyncLock();
encryptionKey;
encryptionVariant;
constructor(token, session) {
super();
this.token = token;
this.sessionId = session.id;
this.metadata = session.metadata;
this.metadataVersion = session.metadataVersion;
this.agentState = session.agentState;
this.agentStateVersion = session.agentStateVersion;
this.encryptionKey = session.encryptionKey;
this.encryptionVariant = session.encryptionVariant;
this.rpcHandlerManager = new RpcHandlerManager({
scopePrefix: this.sessionId,
encryptionKey: this.encryptionKey,
encryptionVariant: this.encryptionVariant,
logger: (msg, data) => logger.debug(msg, data)
});
registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
this.socket = io(configuration.serverUrl, {
auth: {
token: this.token,
clientType: "session-scoped",
sessionId: this.sessionId
},
path: "/v1/updates",
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1e3,
reconnectionDelayMax: 5e3,
transports: ["websocket"],
withCredentials: true,
autoConnect: false
});
this.socket.on("connect", () => {
logger.debug("Socket connected successfully");
this.rpcHandlerManager.onSocketConnect(this.socket);
});
this.socket.on("rpc-request", async (data, callback) => {
callback(await this.rpcHandlerManager.handleRequest(data));
});
this.socket.on("disconnect", (reason) => {
logger.debug("[API] Socket disconnected:", reason);
this.rpcHandlerManager.onSocketDisconnect();
});
this.socket.on("connect_error", (error) => {
logger.debug("[API] Socket connection error:", error);
this.rpcHandlerManager.onSocketDisconnect();
});
this.socket.on("update", (data) => {
try {
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
if (!data.body) {
logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
return;
}
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
const userResult = UserMessageSchema.safeParse(body);
if (userResult.success) {
if (this.pendingMessageCallback) {
this.pendingMessageCallback(userResult.data);
} else {
this.pendingMessages.push(userResult.data);
}
} else {
this.emit("message", body);
}
} else if (data.body.t === "update-session") {
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.metadata.value));
this.metadataVersion = data.body.metadata.version;
}
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.agentState.value)) : null;
this.agentStateVersion = data.body.agentState.version;
}
} else if (data.body.t === "update-machine") {
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
} else {
this.emit("message", data.body);
}
} catch (error) {
logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
}
});
this.socket.on("error", (error) => {
logger.debug("[API] Socket error:", error);
});
this.socket.connect();
}
onUserMessage(callback) {
this.pendingMessageCallback = callback;
while (this.pendingMessages.length > 0) {
callback(this.pendingMessages.shift());
}
}
/**
* Send message to session
* @param body - Message body (can be MessageContent or raw content for agent messages)
*/
sendClaudeSessionMessage(body) {
let content;
if (body.type === "user" && typeof body.message.content === "string" && body.isSidechain !== true && body.isMeta !== true) {
content = {
role: "user",
content: {
type: "text",
text: body.message.content
},
meta: {
sentFrom: "cli"
}
};
} else {
content = {
role: "agent",
content: {
type: "output",
data: body
// This wraps the entire Claude message
},
meta: {
sentFrom: "cli"
}
};
}
logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
if (!this.socket.connected) {
logger.debug("[API] Socket not connected, cannot send Claude session message. Message will be lost:", { type: body.type });
return;
}
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
this.socket.emit("message", {
sid: this.sessionId,
message: encrypted
});
if (body.type === "assistant" && body.message?.usage) {
try {
this.sendUsageData(body.message.usage, body.message.model);
} catch (error) {
logger.debug("[SOCKET] Failed to send usage data:", error);
}
}
if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
this.updateMetadata((metadata) => ({
...metadata,
summary: {
text: body.summary,
updatedAt: Date.now()
}
}));
}
}
sendCodexMessage(body) {
let content = {
role: "agent",
content: {
type: "codex",
data: body
// This wraps the entire Claude message
},
meta: {
sentFrom: "cli"
}
};
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
if (!this.socket.connected) {
logger.debug("[API] Socket not connected, cannot send message. Message will be lost:", { type: body.type });
}
this.socket.emit("message", {
sid: this.sessionId,
message: encrypted
});
}
/**
* Send a generic agent message to the session using ACP (Agent Communication Protocol) format.
* Works for any agent type (Gemini, Codex, Claude, etc.) - CLI normalizes to unified ACP format.
*
* @param provider - The agent provider sending the message (e.g., 'gemini', 'codex', 'claude')
* @param body - The message payload (type: 'message' | 'reasoning' | 'tool-call' | 'tool-result')
*/
sendAgentMessage(provider, body) {
let content = {
role: "agent",
content: {
type: "acp",
provider,
data: body
},
meta: {
sentFrom: "cli"
}
};
logger.debug(`[SOCKET] Sending ACP message from ${provider}:`, { type: body.type, hasMessage: "message" in body });
con