UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

410 lines (404 loc) 14.7 kB
'use strict'; var promises = require('node:fs/promises'); var fs = require('node:fs'); var api = require('./types-XjAAlKci.cjs'); var z = require('zod'); require('axios'); require('chalk'); require('fs'); require('node:os'); require('node:path'); require('node:events'); require('socket.io-client'); require('node:crypto'); require('tweetnacl'); require('child_process'); require('util'); require('fs/promises'); require('crypto'); require('path'); require('url'); require('os'); require('expo-server-sdk'); 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 z__namespace = /*#__PURE__*/_interopNamespaceDefault(z); const AnthropicConfigSchema = z__namespace.object({ baseUrl: z__namespace.string().url().optional(), authToken: z__namespace.string().optional(), model: z__namespace.string().optional() }); const OpenAIConfigSchema = z__namespace.object({ apiKey: z__namespace.string().optional(), baseUrl: z__namespace.string().url().optional(), model: z__namespace.string().optional() }); const AzureOpenAIConfigSchema = z__namespace.object({ apiKey: z__namespace.string().optional(), endpoint: z__namespace.string().url().optional(), apiVersion: z__namespace.string().optional(), deploymentName: z__namespace.string().optional() }); const TogetherAIConfigSchema = z__namespace.object({ apiKey: z__namespace.string().optional(), model: z__namespace.string().optional() }); const TmuxConfigSchema = z__namespace.object({ sessionName: z__namespace.string().optional(), tmpDir: z__namespace.string().optional(), updateEnvironment: z__namespace.boolean().optional() }); const EnvironmentVariableSchema = z__namespace.object({ name: z__namespace.string().regex(/^[A-Z_][A-Z0-9_]*$/, "Invalid environment variable name"), value: z__namespace.string() }); const ProfileCompatibilitySchema = z__namespace.object({ claude: z__namespace.boolean().default(true), codex: z__namespace.boolean().default(true), gemini: z__namespace.boolean().default(true) }); const AIBackendProfileSchema = z__namespace.object({ id: z__namespace.string().uuid(), name: z__namespace.string().min(1).max(100), description: z__namespace.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__namespace.array(EnvironmentVariableSchema).default([]), // Default session type for this profile defaultSessionType: z__namespace.enum(["simple", "worktree"]).optional(), // Default permission mode for this profile (supports both Claude and Codex modes) defaultPermissionMode: z__namespace.enum([ "default", "acceptEdits", "bypassPermissions", "plan", // Claude modes "read-only", "safe-yolo", "yolo" // Codex modes ]).optional(), // Default model mode for this profile defaultModelMode: z__namespace.string().optional(), // Compatibility metadata compatibility: ProfileCompatibilitySchema.default({ claude: true, codex: true, gemini: true }), // Built-in profile indicator isBuiltIn: z__namespace.boolean().default(false), // Metadata createdAt: z__namespace.number().default(() => Date.now()), updatedAt: z__namespace.number().default(() => Date.now()), version: z__namespace.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 (!fs.existsSync(api.configuration.settingsFile)) { return { ...defaultSettings }; } try { const content = await promises.readFile(api.configuration.settingsFile, "utf8"); const raw = JSON.parse(content); const schemaVersion = raw.schemaVersion ?? 1; if (schemaVersion > SUPPORTED_SCHEMA_VERSION) { api.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) { api.logger.warn( `\u26A0\uFE0F Invalid profile "${profile?.name || profile?.id || "unknown"}" - skipping. Error: ${error.message}` ); } } migrated.profiles = validProfiles; } return { ...defaultSettings, ...migrated }; } catch (error) { api.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 = api.configuration.settingsFile + ".lock"; const tmpFile = api.configuration.settingsFile + ".tmp"; let fileHandle; let attempts = 0; while (attempts < MAX_LOCK_ATTEMPTS) { try { fileHandle = await promises.open(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.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 promises.stat(lockFile); if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) { await promises.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 (!fs.existsSync(api.configuration.consortiumHomeDir)) { await promises.mkdir(api.configuration.consortiumHomeDir, { recursive: true }); } await promises.writeFile(tmpFile, JSON.stringify(updated, null, 2)); await promises.rename(tmpFile, api.configuration.settingsFile); return updated; } finally { await fileHandle.close(); await promises.unlink(lockFile).catch(() => { }); } } const credentialsSchema = z__namespace.object({ token: z__namespace.string(), secret: z__namespace.string().base64().nullish(), // Legacy encryption: z__namespace.object({ publicKey: z__namespace.string().base64(), machineKey: z__namespace.string().base64() }).nullish() }); async function readCredentials() { if (!fs.existsSync(api.configuration.privateKeyFile)) { return null; } try { const keyBase64 = await promises.readFile(api.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 (!fs.existsSync(api.configuration.consortiumHomeDir)) { await promises.mkdir(api.configuration.consortiumHomeDir, { recursive: true }); } await promises.writeFile(api.configuration.privateKeyFile, JSON.stringify({ secret: api.encodeBase64(credentials.secret), token: credentials.token }, null, 2)); } async function writeCredentialsDataKey(credentials) { if (!fs.existsSync(api.configuration.consortiumHomeDir)) { await promises.mkdir(api.configuration.consortiumHomeDir, { recursive: true }); } await promises.writeFile(api.configuration.privateKeyFile, JSON.stringify({ encryption: { publicKey: api.encodeBase64(credentials.publicKey), machineKey: api.encodeBase64(credentials.machineKey) }, token: credentials.token }, null, 2)); } async function clearCredentials() { if (fs.existsSync(api.configuration.privateKeyFile)) { await promises.unlink(api.configuration.privateKeyFile); } } async function clearMachineId() { await updateSettings((settings) => ({ ...settings, machineId: void 0 })); } async function readDaemonState() { try { if (!fs.existsSync(api.configuration.daemonStateFile)) { return null; } const content = await promises.readFile(api.configuration.daemonStateFile, "utf-8"); return JSON.parse(content); } catch (error) { console.error(`[PERSISTENCE] Daemon state file corrupted: ${api.configuration.daemonStateFile}`, error); return null; } } function writeDaemonState(state) { fs.writeFileSync(api.configuration.daemonStateFile, JSON.stringify(state, null, 2), "utf-8"); } async function clearDaemonState() { if (fs.existsSync(api.configuration.daemonStateFile)) { await promises.unlink(api.configuration.daemonStateFile); } if (fs.existsSync(api.configuration.daemonLockFile)) { try { await promises.unlink(api.configuration.daemonLockFile); } catch { } } } async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const fileHandle = await promises.open( api.configuration.daemonLockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY ); await fileHandle.writeFile(String(process.pid)); return fileHandle; } catch (error) { if (error.code === "EEXIST") { try { const lockPid = fs.readFileSync(api.configuration.daemonLockFile, "utf-8").trim(); if (lockPid && !isNaN(Number(lockPid))) { try { process.kill(Number(lockPid), 0); } catch { fs.unlinkSync(api.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 (fs.existsSync(api.configuration.daemonLockFile)) { fs.unlinkSync(api.configuration.daemonLockFile); } } catch { } } exports.AIBackendProfileSchema = AIBackendProfileSchema; exports.SUPPORTED_SCHEMA_VERSION = SUPPORTED_SCHEMA_VERSION; exports.acquireDaemonLock = acquireDaemonLock; exports.clearCredentials = clearCredentials; exports.clearDaemonState = clearDaemonState; exports.clearMachineId = clearMachineId; exports.getProfileEnvironmentVariables = getProfileEnvironmentVariables; exports.readCredentials = readCredentials; exports.readDaemonState = readDaemonState; exports.readSettings = readSettings; exports.releaseDaemonLock = releaseDaemonLock; exports.updateSettings = updateSettings; exports.validateProfileForAgent = validateProfileForAgent; exports.writeCredentialsDataKey = writeCredentialsDataKey; exports.writeCredentialsLegacy = writeCredentialsLegacy; exports.writeDaemonState = writeDaemonState;