consortium
Version:
Remote control and session sharing CLI for AI coding agents
374 lines (371 loc) • 13 kB
JavaScript
import { readFile, open, stat, unlink, mkdir, writeFile, rename } from 'node:fs/promises';
import { existsSync, constants, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
import { c as configuration, l as logger, e as encodeBase64 } from './types-WAGIe5yd.mjs';
import * as z from 'zod';
import 'axios';
import 'chalk';
import 'fs';
import 'node:os';
import 'node:path';
import 'node:events';
import 'socket.io-client';
import 'node:crypto';
import 'tweetnacl';
import 'child_process';
import 'util';
import 'fs/promises';
import 'crypto';
import 'path';
import 'url';
import 'os';
import 'expo-server-sdk';
const AnthropicConfigSchema = z.object({
baseUrl: z.string().url().optional(),
authToken: z.string().optional(),
model: z.string().optional()
});
const OpenAIConfigSchema = z.object({
apiKey: z.string().optional(),
baseUrl: z.string().url().optional(),
model: z.string().optional()
});
const AzureOpenAIConfigSchema = z.object({
apiKey: z.string().optional(),
endpoint: z.string().url().optional(),
apiVersion: z.string().optional(),
deploymentName: z.string().optional()
});
const TogetherAIConfigSchema = z.object({
apiKey: z.string().optional(),
model: z.string().optional()
});
const TmuxConfigSchema = z.object({
sessionName: z.string().optional(),
tmpDir: z.string().optional(),
updateEnvironment: z.boolean().optional()
});
const EnvironmentVariableSchema = z.object({
name: z.string().regex(/^[A-Z_][A-Z0-9_]*$/, "Invalid environment variable name"),
value: z.string()
});
const ProfileCompatibilitySchema = z.object({
claude: z.boolean().default(true),
codex: z.boolean().default(true),
gemini: z.boolean().default(true)
});
const AIBackendProfileSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
// Agent-specific configurations
anthropicConfig: AnthropicConfigSchema.optional(),
openaiConfig: OpenAIConfigSchema.optional(),
azureOpenAIConfig: AzureOpenAIConfigSchema.optional(),
togetherAIConfig: TogetherAIConfigSchema.optional(),
// Tmux configuration
tmuxConfig: TmuxConfigSchema.optional(),
// Environment variables (validated)
environmentVariables: z.array(EnvironmentVariableSchema).default([]),
// Default session type for this profile
defaultSessionType: z.enum(["simple", "worktree"]).optional(),
// Default permission mode for this profile (supports both Claude and Codex modes)
defaultPermissionMode: z.enum([
"default",
"acceptEdits",
"bypassPermissions",
"plan",
// Claude modes
"read-only",
"safe-yolo",
"yolo"
// Codex modes
]).optional(),
// Default model mode for this profile
defaultModelMode: z.string().optional(),
// Compatibility metadata
compatibility: ProfileCompatibilitySchema.default({ claude: true, codex: true, gemini: true }),
// Built-in profile indicator
isBuiltIn: z.boolean().default(false),
// Metadata
createdAt: z.number().default(() => Date.now()),
updatedAt: z.number().default(() => Date.now()),
version: z.string().default("1.0.0")
});
function validateProfileForAgent(profile, agent) {
return profile.compatibility[agent];
}
function getProfileEnvironmentVariables(profile) {
const envVars = {};
profile.environmentVariables.forEach((envVar) => {
envVars[envVar.name] = envVar.value;
});
if (profile.anthropicConfig) {
if (profile.anthropicConfig.baseUrl) envVars.ANTHROPIC_BASE_URL = profile.anthropicConfig.baseUrl;
if (profile.anthropicConfig.authToken) envVars.ANTHROPIC_AUTH_TOKEN = profile.anthropicConfig.authToken;
if (profile.anthropicConfig.model) envVars.ANTHROPIC_MODEL = profile.anthropicConfig.model;
}
if (profile.openaiConfig) {
if (profile.openaiConfig.apiKey) envVars.OPENAI_API_KEY = profile.openaiConfig.apiKey;
if (profile.openaiConfig.baseUrl) envVars.OPENAI_BASE_URL = profile.openaiConfig.baseUrl;
if (profile.openaiConfig.model) envVars.OPENAI_MODEL = profile.openaiConfig.model;
}
if (profile.azureOpenAIConfig) {
if (profile.azureOpenAIConfig.apiKey) envVars.AZURE_OPENAI_API_KEY = profile.azureOpenAIConfig.apiKey;
if (profile.azureOpenAIConfig.endpoint) envVars.AZURE_OPENAI_ENDPOINT = profile.azureOpenAIConfig.endpoint;
if (profile.azureOpenAIConfig.apiVersion) envVars.AZURE_OPENAI_API_VERSION = profile.azureOpenAIConfig.apiVersion;
if (profile.azureOpenAIConfig.deploymentName) envVars.AZURE_OPENAI_DEPLOYMENT_NAME = profile.azureOpenAIConfig.deploymentName;
}
if (profile.togetherAIConfig) {
if (profile.togetherAIConfig.apiKey) envVars.TOGETHER_API_KEY = profile.togetherAIConfig.apiKey;
if (profile.togetherAIConfig.model) envVars.TOGETHER_MODEL = profile.togetherAIConfig.model;
}
if (profile.tmuxConfig) {
if (profile.tmuxConfig.sessionName !== void 0) envVars.TMUX_SESSION_NAME = profile.tmuxConfig.sessionName;
if (profile.tmuxConfig.tmpDir) envVars.TMUX_TMPDIR = profile.tmuxConfig.tmpDir;
if (profile.tmuxConfig.updateEnvironment !== void 0) {
envVars.TMUX_UPDATE_ENVIRONMENT = profile.tmuxConfig.updateEnvironment.toString();
}
}
return envVars;
}
const SUPPORTED_SCHEMA_VERSION = 2;
const defaultSettings = {
schemaVersion: SUPPORTED_SCHEMA_VERSION,
onboardingCompleted: false,
profiles: [],
localEnvironmentVariables: {}
};
function migrateSettings(raw, fromVersion) {
let migrated = { ...raw };
if (fromVersion < 2) {
if (!migrated.profiles) {
migrated.profiles = [];
}
if (!migrated.localEnvironmentVariables) {
migrated.localEnvironmentVariables = {};
}
migrated.schemaVersion = 2;
}
return migrated;
}
async function readSettings() {
if (!existsSync(configuration.settingsFile)) {
return { ...defaultSettings };
}
try {
const content = await readFile(configuration.settingsFile, "utf8");
const raw = JSON.parse(content);
const schemaVersion = raw.schemaVersion ?? 1;
if (schemaVersion > SUPPORTED_SCHEMA_VERSION) {
logger.warn(
`\u26A0\uFE0F Settings schema v${schemaVersion} > supported v${SUPPORTED_SCHEMA_VERSION}. Update consortium-cli for full functionality.`
);
}
const migrated = migrateSettings(raw, schemaVersion);
if (migrated.profiles && Array.isArray(migrated.profiles)) {
const validProfiles = [];
for (const profile of migrated.profiles) {
try {
const validated = AIBackendProfileSchema.parse(profile);
validProfiles.push(validated);
} catch (error) {
logger.warn(
`\u26A0\uFE0F Invalid profile "${profile?.name || profile?.id || "unknown"}" - skipping. Error: ${error.message}`
);
}
}
migrated.profiles = validProfiles;
}
return { ...defaultSettings, ...migrated };
} catch (error) {
logger.warn(`Failed to read settings: ${error.message}`);
return { ...defaultSettings };
}
}
async function updateSettings(updater) {
const LOCK_RETRY_INTERVAL_MS = 100;
const MAX_LOCK_ATTEMPTS = 50;
const STALE_LOCK_TIMEOUT_MS = 1e4;
const lockFile = configuration.settingsFile + ".lock";
const tmpFile = configuration.settingsFile + ".tmp";
let fileHandle;
let attempts = 0;
while (attempts < MAX_LOCK_ATTEMPTS) {
try {
fileHandle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
break;
} catch (err) {
if (err.code === "EEXIST") {
attempts++;
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
try {
const stats = await stat(lockFile);
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
await unlink(lockFile).catch(() => {
});
}
} catch {
}
} else {
throw err;
}
}
}
if (!fileHandle) {
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
}
try {
const current = await readSettings() || { ...defaultSettings };
const updated = await updater(current);
if (!existsSync(configuration.consortiumHomeDir)) {
await mkdir(configuration.consortiumHomeDir, { recursive: true });
}
await writeFile(tmpFile, JSON.stringify(updated, null, 2));
await rename(tmpFile, configuration.settingsFile);
return updated;
} finally {
await fileHandle.close();
await unlink(lockFile).catch(() => {
});
}
}
const credentialsSchema = z.object({
token: z.string(),
secret: z.string().base64().nullish(),
// Legacy
encryption: z.object({
publicKey: z.string().base64(),
machineKey: z.string().base64()
}).nullish()
});
async function readCredentials() {
if (!existsSync(configuration.privateKeyFile)) {
return null;
}
try {
const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
if (credentials.secret) {
return {
token: credentials.token,
encryption: {
type: "legacy",
secret: new Uint8Array(Buffer.from(credentials.secret, "base64"))
}
};
} else if (credentials.encryption) {
return {
token: credentials.token,
encryption: {
type: "dataKey",
publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
}
};
}
} catch {
return null;
}
return null;
}
async function writeCredentialsLegacy(credentials) {
if (!existsSync(configuration.consortiumHomeDir)) {
await mkdir(configuration.consortiumHomeDir, { recursive: true });
}
await writeFile(configuration.privateKeyFile, JSON.stringify({
secret: encodeBase64(credentials.secret),
token: credentials.token
}, null, 2));
}
async function writeCredentialsDataKey(credentials) {
if (!existsSync(configuration.consortiumHomeDir)) {
await mkdir(configuration.consortiumHomeDir, { recursive: true });
}
await writeFile(configuration.privateKeyFile, JSON.stringify({
encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
token: credentials.token
}, null, 2));
}
async function clearCredentials() {
if (existsSync(configuration.privateKeyFile)) {
await unlink(configuration.privateKeyFile);
}
}
async function clearMachineId() {
await updateSettings((settings) => ({
...settings,
machineId: void 0
}));
}
async function readDaemonState() {
try {
if (!existsSync(configuration.daemonStateFile)) {
return null;
}
const content = await readFile(configuration.daemonStateFile, "utf-8");
return JSON.parse(content);
} catch (error) {
console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
return null;
}
}
function writeDaemonState(state) {
writeFileSync(configuration.daemonStateFile, JSON.stringify(state, null, 2), "utf-8");
}
async function clearDaemonState() {
if (existsSync(configuration.daemonStateFile)) {
await unlink(configuration.daemonStateFile);
}
if (existsSync(configuration.daemonLockFile)) {
try {
await unlink(configuration.daemonLockFile);
} catch {
}
}
}
async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const fileHandle = await open(
configuration.daemonLockFile,
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY
);
await fileHandle.writeFile(String(process.pid));
return fileHandle;
} catch (error) {
if (error.code === "EEXIST") {
try {
const lockPid = readFileSync(configuration.daemonLockFile, "utf-8").trim();
if (lockPid && !isNaN(Number(lockPid))) {
try {
process.kill(Number(lockPid), 0);
} catch {
unlinkSync(configuration.daemonLockFile);
continue;
}
}
} catch {
}
}
if (attempt === maxAttempts) {
return null;
}
const delayMs = attempt * delayIncrementMs;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
return null;
}
async function releaseDaemonLock(lockHandle) {
try {
await lockHandle.close();
} catch {
}
try {
if (existsSync(configuration.daemonLockFile)) {
unlinkSync(configuration.daemonLockFile);
}
} catch {
}
}
export { AIBackendProfileSchema, SUPPORTED_SCHEMA_VERSION, acquireDaemonLock, clearCredentials, clearDaemonState, clearMachineId, getProfileEnvironmentVariables, readCredentials, readDaemonState, readSettings, releaseDaemonLock, updateSettings, validateProfileForAgent, writeCredentialsDataKey, writeCredentialsLegacy, writeDaemonState };