UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

431 lines (424 loc) 17 kB
import axios from 'axios'; import { randomUUID } from 'node:crypto'; import { b as authAndSetupMachineIfNeeded, e as ensureActiveOrg, i as initialMachineMetadata, r as registerKillSessionHandler, M as MessageQueue2, h as hashObject } from './index-DiNLHtkZ.mjs'; import { l as logger, A as ApiClient, c as configuration } from './types-DETLaopx.mjs'; import { c as createSessionMetadata } from './createSessionMetadata-I6-IgJ9q.mjs'; import { s as setupOfflineReconnection } from './setupOfflineReconnection-DaGfrqRS.mjs'; import { createRequire } from 'node:module'; import { existsSync, chmodSync, statSync, readFileSync, readdirSync } from 'node:fs'; import { execFile, execFileSync } from 'node:child_process'; import { dirname, sep, join } from 'node:path'; const SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set(["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]); const DEFAULT_INSTALLED_PKG = "consortium"; function detectInstalledPackageName(binaryPath) { try { const parts = binaryPath.split(sep); for (let i = parts.length - 1; i >= 2; i--) { if (parts[i] === "node_modules" && parts[i - 2] === "node_modules") { const candidate = parts[i - 1]; if (candidate && !candidate.startsWith("@")) { return candidate; } } } let dir = dirname(binaryPath); for (let depth = 0; depth < 8; depth++) { const pj = join(dir, "..", "..", "package.json"); if (existsSync(pj)) { const parsed = JSON.parse(readFileSync(pj, "utf8")); if (parsed.name) return parsed.name; } const next = dirname(dir); if (next === dir) break; dir = next; } } catch { } return DEFAULT_INSTALLED_PKG; } function allChannelsReinstallHint(pkgName) { return `Reinstall the channel you originally installed: npm install -g consortium@latest # stable npm install -g canary-dolphin-swimsuit@latest # canary npm install -g dev-hummingbird-sneakers@latest # dev # or force the missing platform package directly: npm install -g ${pkgName}`; } let cachedBinary = null; const probeCache = /* @__PURE__ */ new Map(); function currentPlatform() { return `${process.platform}-${process.arch}`; } function resolveConsortiumCodeBinarySync() { if (cachedBinary) return cachedBinary; const platform = currentPlatform(); if (!SUPPORTED_PLATFORMS.has(platform)) { throw new Error( `Consortium Code is not yet built for ${platform}. Supported: ${[...SUPPORTED_PLATFORMS].join(", ")}. Ping #platform-support if you need this one added.` ); } const pkgName = `consortium-code-${platform}`; const specifier = `${pkgName}/bin/consortium-code`; const req = createRequire(import.meta.url); let binaryPath; try { binaryPath = req.resolve(specifier); } catch (err) { const fromSource = import.meta.url.includes("/packages/consortium-cli/src/"); const hint = fromSource ? `Running from source tree \u2014 run \`yarn install\` at the monorepo root to pull the platform binary.` : `Optional dependency install likely failed during \`npm install -g\`. ` + allChannelsReinstallHint(pkgName); throw new Error( `Consortium Code binary (${pkgName}) is not installed. ${hint} Underlying resolver error: ${err instanceof Error ? err.message : String(err)}` ); } if (!existsSync(binaryPath)) { const installedPkg = detectInstalledPackageName(binaryPath); throw new Error( `Resolved ${pkgName} but ${binaryPath} does not exist on disk. Reinstall: npm install -g ${installedPkg}@latest` ); } try { chmodSync(binaryPath, 493); } catch { } try { const pkgDir = dirname(dirname(binaryPath)); chmodEveryBinFileUnder(pkgDir); } catch (err) { logger.debug(`[binaryResolution] bin/ chmod sweep failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`); } logger.debug(`[binaryResolution] Resolved ${pkgName} -> ${binaryPath}`); cachedBinary = binaryPath; return binaryPath; } function chmodEveryBinFileUnder(root, depth = 0) { if (depth > 6) return; let entries; try { entries = readdirSync(root); } catch { return; } for (const entry of entries) { const full = join(root, entry); let entryStat; try { entryStat = statSync(full); } catch { continue; } if (entryStat.isDirectory()) { if (entry === "bin") { chmodAllFilesIn(full); } chmodEveryBinFileUnder(full, depth + 1); } } } function chmodAllFilesIn(binDir) { let entries; try { entries = readdirSync(binDir); } catch { return; } for (const entry of entries) { const full = join(binDir, entry); try { const st = statSync(full); if (st.isFile()) { chmodSync(full, 493); } else if (st.isDirectory()) { chmodAllFilesIn(full); } } catch { } } } async function resolveConsortiumCodeBinary() { const binaryPath = resolveConsortiumCodeBinarySync(); if (process.env.CONSORTIUM_SKIP_BINARY_PROBE === "1") { logger.debug("[binaryResolution] Probe skipped via CONSORTIUM_SKIP_BINARY_PROBE=1"); return binaryPath; } const result = await probeBinaryHealthy(binaryPath); if (result.ok) return binaryPath; if (result.classification === "slow" || result.classification === "empty") { logger.warn( `[binaryResolution] --version probe was inconclusive (${result.classification}); proceeding to launch anyway. If launch hangs or fails, run \`consortium doctor\`. Diagnostics: ${result.diagnostics}` ); return binaryPath; } const pkgName = `consortium-code-${currentPlatform()}`; const installedPkg = detectInstalledPackageName(binaryPath); const remediation = [ "", "What to try, in order:", ` 1. Run \`consortium doctor\` for a guided health check.`, ` 2. Reinstall the CLI for your channel:`, ` npm install -g consortium@latest # stable`, ` npm install -g canary-dolphin-swimsuit@latest # canary`, ` npm install -g dev-hummingbird-sneakers@latest # dev`, ` (you appear to be on: ${installedPkg})`, ` 3. Force-install the platform package directly:`, ` npm install -g ${pkgName}`, ` 4. On macOS, clear the quarantine bit: \`xattr -dr com.apple.quarantine "$(dirname "$(which consortium)")/.."\``, ` 5. Fall back to the Docker image: \`docker run --rm -it ghcr.io/consortium/cli:dev\`.`, "" ].join("\n"); throw new Error( `Consortium Code binary failed its pre-launch probe (${result.classification}): ` + result.diagnostics + remediation ); } function getProbeTimeoutMs() { const override = parseInt(process.env.CONSORTIUM_PROBE_TIMEOUT_MS_OVERRIDE || "", 10); if (Number.isFinite(override) && override > 0) return override; return 3e4; } const CAPTURE_LIMIT_BYTES = 2 * 1024; function truncate(s, max) { if (!s) return ""; if (s.length <= max) return s; return s.slice(0, max) + ` \u2026[truncated ${s.length - max} bytes]`; } function safeExec(cmd, args, timeoutMs = 1e3) { try { const out = execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs }); return (out || "").toString().trim(); } catch (err) { const partial = ((err?.stdout || "") + (err?.stderr || "")).toString().trim(); if (partial) return partial; return `<unavailable: ${err?.code || err?.message || "error"}>`; } } function probeGlibcVersion() { if (process.platform !== "linux") return "<n/a (not linux)>"; const getconf = safeExec("getconf", ["GNU_LIBC_VERSION"]); if (getconf && !getconf.startsWith("<unavailable")) return getconf; const ldd = safeExec("ldd", ["--version"]); if (ldd && !ldd.startsWith("<unavailable")) return ldd.split("\n")[0] || ldd; return "<unavailable>"; } function probeBinaryRequiredGlibc(binaryPath) { if (process.platform !== "linux") return "<n/a (not linux)>"; const out = safeExec("readelf", ["-V", binaryPath], 2e3); if (!out || out.startsWith("<unavailable")) return "<unavailable>"; const matches = out.match(/GLIBC_(\d+)\.(\d+)(?:\.(\d+))?/g) || []; if (matches.length === 0) return "<no GLIBC references found>"; let best = { major: 0, minor: 0, patch: 0, raw: "" }; for (const m of matches) { const parts = m.replace("GLIBC_", "").split(".").map((n) => parseInt(n, 10)); const major = parts[0] || 0; const minor = parts[1] || 0; const patch = parts[2] || 0; if (major > best.major || major === best.major && minor > best.minor || major === best.major && minor === best.minor && patch > best.patch) { best = { major, minor, patch, raw: m }; } } return best.raw || "<unknown>"; } function runBinaryVersion(binaryPath) { return new Promise((resolve) => { let stdout = ""; let stderr = ""; let timedOut = false; let spawnErr = null; const timeoutMs = getProbeTimeoutMs(); const child = execFile(binaryPath, ["--version"], { timeout: timeoutMs, maxBuffer: 64 * 1024 }, (err, out, errOut) => { stdout = typeof out === "string" ? out : String(out ?? ""); stderr = typeof errOut === "string" ? errOut : String(errOut ?? ""); if (err) { const errAny = err; if (errAny.killed && errAny.signal === "SIGTERM") timedOut = true; if (errAny.code === "ENOENT") { spawnErr = errAny; } const code = typeof err.code === "number" ? err.code : null; resolve({ code, stdout, stderr, timedOut, spawnErr }); return; } resolve({ code: 0, stdout, stderr, timedOut: false, spawnErr: null }); }); child.on("error", (err) => { if (err.code === "ENOENT") spawnErr = err; }); }); } async function probeBinaryHealthy(binaryPath) { const cached = probeCache.get(binaryPath); if (cached) return cached; const res = await runBinaryVersion(binaryPath); const stdoutTrim = (res.stdout || "").trim(); const isEnoent = res.spawnErr?.code === "ENOENT"; const healthy = !isEnoent && !res.timedOut && res.code === 0 && stdoutTrim.length > 0; if (healthy) { const ok = { ok: true }; probeCache.set(binaryPath, ok); return ok; } const classification = isEnoent ? "enoent" : res.code != null && res.code !== 0 ? "nonzero-exit" : res.timedOut ? "slow" : "empty"; const reason = classification === "enoent" ? "ENOENT (binary not found at path)" : classification === "slow" ? `timed out after ${getProbeTimeoutMs()}ms with no output (likely slow launch, not incompatibility)` : classification === "nonzero-exit" ? `non-zero exit code ${res.code}` : "exited 0 with empty stdout"; const uname = process.platform === "linux" || process.platform === "darwin" ? safeExec("uname", ["-m"]) : "<n/a>"; const glibcHost = probeGlibcVersion(); const glibcRequired = probeBinaryRequiredGlibc(binaryPath); const alpine = existsSync("/etc/alpine-release"); let statInfo = "<unavailable>"; try { const st = statSync(binaryPath); statInfo = `size=${st.size} mode=0${(st.mode & 511).toString(8)} uid=${st.uid} gid=${st.gid}`; } catch (err) { statInfo = `<stat failed: ${err instanceof Error ? err.message : String(err)}>`; } const lines = []; lines.push(` binary: ${binaryPath}`); lines.push(` stat: ${statInfo}`); lines.push(` failure reason: ${reason}`); lines.push(` process.platform: ${process.platform}`); lines.push(` process.arch: ${process.arch}`); lines.push(` uname -m: ${uname}`); lines.push(` host glibc: ${glibcHost}`); lines.push(` binary needs glibc: ${glibcRequired}`); lines.push(` /etc/alpine-release present (musl signal): ${alpine ? "yes" : "no"}`); lines.push(" --- captured stderr ---"); lines.push(truncate(res.stderr || "<empty>", CAPTURE_LIMIT_BYTES)); lines.push(" --- captured stdout ---"); lines.push(truncate(res.stdout || "<empty>", CAPTURE_LIMIT_BYTES)); const diagnostics = lines.join("\n"); const out = { ok: false, diagnostics, classification }; probeCache.set(binaryPath, out); return out; } async function resolveSessionTagById(credentials, sessionId) { try { const response = await axios.get( `${configuration.serverUrl}/v1/sessions`, { headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 1e4 } ); const row = (response.data?.sessions ?? []).find((s) => s.id === sessionId); if (row && typeof row.tag === "string" && row.tag.length > 0) return row.tag; return null; } catch (err) { logger.debug("[cliRouting] resolveSessionTagById failed", err); return null; } } async function handleConsortiumCodeCommand(args, startedBy) { let startingMode = "local"; let parsedStartedBy = startedBy; let resumeSessionId = null; for (let i = 0; i < args.length; i++) { if (args[i] === "--consortium-starting-mode" && args[i + 1]) { startingMode = args[++i]; } else if (args[i] === "--started-by" && args[i + 1]) { parsedStartedBy = args[++i]; } else if (args[i] === "--resume" && args[i + 1]) { resumeSessionId = args[++i]; } } logger.debug(`[consortium-code] mode=${startingMode}, startedBy=${parsedStartedBy}, resume=${resumeSessionId ?? "(none)"}`); const { credentials, machineId } = await authAndSetupMachineIfNeeded(); let resumeTag = null; if (resumeSessionId) { resumeTag = await resolveSessionTagById(credentials, resumeSessionId); if (!resumeTag) { logger.debug(`[consortium-code] --resume ${resumeSessionId}: session not found; starting fresh`); } } if (startingMode === "remote") { logger.debug("[consortium-code] Starting in remote mode (ACP backend)"); const { runConsortiumCode } = await import('./runConsortiumCode-NjBj9tRM.mjs'); await runConsortiumCode({ credentials, startedBy: parsedStartedBy, resumeTag }); return; } const { isInteractiveTerminal, looksLikeWindowsSshNoPty } = await import('./index-DiNLHtkZ.mjs').then(function (n) { return n.t; }); if (!isInteractiveTerminal() && parsedStartedBy !== "daemon" && process.env.CONSORTIUM_ALLOW_NO_TTY !== "1") { const winSshHint = looksLikeWindowsSshNoPty() ? "\nDetected Windows OpenSSH without a PTY. Reconnect with: `ssh -t user@host consortium code`\n" : ""; const msg = "Error: Consortium Code requires an interactive terminal (TTY).\n" + winSshHint + "Options:\n - Reconnect over SSH with `-t` to force PTY allocation.\n - Run `consortium daemon start` then drive sessions from the mobile/web client.\n - Pass `CONSORTIUM_ALLOW_NO_TTY=1` to bypass this check (advanced).\n"; process.stdout.write(msg); process.stderr.write(msg); process.exit(1); } const api = await ApiClient.create(credentials); const activeOrgId = await ensureActiveOrg(credentials); const sessionTag = resumeTag ?? randomUUID(); const { state, metadata } = createSessionMetadata({ flavor: "consortium-code", machineId, startedBy: parsedStartedBy }); const [, sessionResponse] = await Promise.all([ api.getOrCreateMachine({ machineId, metadata: initialMachineMetadata, organizationId: activeOrgId ?? void 0 }), api.getOrCreateSession({ tag: sessionTag, metadata, state, organizationId: activeOrgId ?? void 0 }) ]); const reconnectionResult = setupOfflineReconnection({ api, sessionTag, metadata, state, response: sessionResponse, onSessionSwap: (newSession) => { session = newSession; } }); let session = reconnectionResult.session; session.keepAlive(false, "local"); registerKillSessionHandler(session.rpcHandlerManager, async () => { session.sendSessionDeath(); await session.flush(); await session.close(); process.exit(0); }); const messageQueue = new MessageQueue2((mode) => hashObject({ permissionMode: mode.permissionMode, model: mode.model })); session.onUserMessage((message) => { const text = message.content.text; if (!text) return; const permissionMode = message.meta?.permissionMode ?? "default"; const model = message.meta?.model; messageQueue.push(text, { permissionMode, model }); }); const binary = await resolveConsortiumCodeBinary(); logger.debug(`[consortium-code] Entering mode-switching loop (binary: ${binary})`); const { consortiumCodeLoop } = await import('./consortiumCodeLoop-Cn2JWXk_.mjs'); const exitCode = await consortiumCodeLoop({ credentials, machineId, cwd: process.cwd(), binary, session, startingMode: "local", startedBy: parsedStartedBy, messageQueue }); session.sendSessionDeath(); await session.flush().catch(() => { }); await session.close().catch(() => { }); process.exit(exitCode); } var cliRouting = /*#__PURE__*/Object.freeze({ __proto__: null, handleConsortiumCodeCommand: handleConsortiumCodeCommand }); export { cliRouting as c, resolveConsortiumCodeBinarySync as r };