consortium
Version:
Remote control and session sharing CLI for AI coding agents
1,465 lines (1,445 loc) • 270 kB
JavaScript
'use strict';
var axios = require('axios');
var chalk = require('chalk');
var fs$1 = require('fs');
var fs = require('node:fs');
var os = require('node:os');
var path = require('node:path');
var node_events = require('node:events');
var socket_ioClient = require('socket.io-client');
var z = require('zod');
var node_crypto = require('node:crypto');
var tweetnacl = require('tweetnacl');
var child_process = require('child_process');
var util = require('util');
var fs$2 = require('fs/promises');
var crypto = require('crypto');
var path$1 = require('path');
var url = require('url');
var os$1 = require('os');
var node_child_process = require('node:child_process');
var fs$3 = require('node:fs/promises');
var node_module = require('node:module');
var node_util = require('node:util');
var expoServerSdk = require('expo-server-sdk');
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 z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
var name = "consortium";
var version = "0.8.11";
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-local": "./bin/consortium-local.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"
}
},
"./pi-ext": {
require: {
types: "./dist/pi/ext/consortium-permission/index.d.cts",
"default": "./dist/pi/ext/consortium-permission/index.cjs"
},
"import": {
types: "./dist/pi/ext/consortium-permission/index.d.mts",
"default": "./dist/pi/ext/consortium-permission/index.mjs"
}
}
};
var files = [
"dist",
"bin",
"scripts",
"tools/archives",
"tools/licenses",
"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: "npx shx rm -rf dist && node -e \"process.argv=process.argv.concat(['--noEmit']);require('typescript/lib/tsc.js')\" && npx pkgroll && node scripts/copy-pi-ext-pkg.cjs",
"check:no-placeholder-key": "node -e \"const fs=require('fs');const s=fs.readFileSync('src/harness/verifySignature.ts','utf8');const m=s.match(/-----BEGIN PUBLIC KEY-----[\\s\\S]*?-----END PUBLIC KEY-----/);if(!m||m[0].includes('PLACEHOLDER')){console.error('placeholder pubkey in verifySignature.ts');process.exit(1)}\"",
test: "$npm_execpath run check:no-placeholder-key && $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:cluster": "$npm_execpath run build && tsx --env-file .env.dev-cluster 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 && node scripts/check-dist-version.cjs",
release: "$npm_execpath install && release-it",
postinstall: "node scripts/unpack-tools.cjs && node scripts/fix-node-pty-perms.cjs && node scripts/fix-libsodium-esm.cjs && node scripts/verify-platform.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",
"@consortium/extension-sdk": "*",
"@modelcontextprotocol/sdk": "^1.25.3",
"@sentry/node": "^8.46.0",
"@stablelib/base64": "^2.0.1",
"@stablelib/hex": "^2.0.1",
ai: "^5.0.107",
axios: "^1.13.2",
"better-sqlite3": "^12.9.0",
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",
"libsodium-wrappers": "^0.7.13",
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/better-sqlite3": "^7.6.13",
"@types/cross-spawn": "^6.0.6",
"@types/http-proxy": "^1.17.17",
"@types/libsodium-wrappers": "^0.7.14",
"@types/node": ">=20",
"@types/ps-list": "^6.2.1",
"@types/qrcode-terminal": "^0.12.2",
"@types/react": "^19.2.7",
"@types/tmp": "^0.2.6",
"cross-env": "^10.1.0",
dotenv: "^16.6.1",
eslint: "^9",
"eslint-config-prettier": "^10",
"ink-testing-library": "^4.0.0",
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",
"@consortium/crypto": "*"
};
var resolutions = {
"whatwg-url": "14.2.0",
"parse-path": "7.0.3",
"@types/parse-path": "7.0.3"
};
var optionalDependencies = {
"node-pty": "^1.0.0",
"consortium-code-darwin-arm64": "0.2.0-canary.20260417210700",
"consortium-code-darwin-x64": "0.2.0-canary.20260417210700",
"consortium-code-linux-arm64": "0.2.0-canary.20260417210700",
"consortium-code-linux-x64": "0.2.0-canary.20260417210700"
};
var publishConfig = {
registry: "https://registry.npmjs.org"
};
var packageManager = "yarn@1.22.22";
var engines = {
node: ">=20"
};
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,
optionalDependencies: optionalDependencies,
publishConfig: publishConfig,
packageManager: packageManager,
engines: engines
};
function isLocalhostUrl(url) {
try {
const host = new URL(url).hostname;
return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1";
} catch {
return false;
}
}
class Configuration {
serverUrl;
webappUrl;
isDaemonProcess;
// Directories and paths (from persistence)
consortiumHomeDir;
logsDir;
settingsFile;
privateKeyFile;
daemonStateFile;
daemonLockFile;
currentCliVersion;
isExperimentalEnabled;
disableCaffeinate;
authToken = process.env.CONSORTIUM_TOKEN;
constructor() {
this.serverUrl = process.env.CONSORTIUM_SERVER_URL || "https://api.consortium.dev";
this.webappUrl = process.env.CONSORTIUM_WEBAPP_URL || (isLocalhostUrl(this.serverUrl) ? `http://localhost:${process.env.CONSORTIUM_WEBAPP_PORT || "8081"}` : "https://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(/^~/, os.homedir());
this.consortiumHomeDir = expandedPath;
} else {
this.consortiumHomeDir = path.join(os.homedir(), ".consortium");
}
this.logsDir = path.join(this.consortiumHomeDir, "logs");
this.settingsFile = path.join(this.consortiumHomeDir, "settings.json");
this.privateKeyFile = path.join(this.consortiumHomeDir, "access.key");
this.daemonStateFile = path.join(this.consortiumHomeDir, "daemon.state.json");
this.daemonLockFile = path.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 (!fs.existsSync(this.consortiumHomeDir)) {
fs.mkdirSync(this.consortiumHomeDir, { recursive: true, mode: 448 });
}
if (!fs.existsSync(this.logsDir)) {
fs.mkdirSync(this.logsDir, { recursive: true, mode: 448 });
}
}
}
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 path.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 : arg instanceof Error ? JSON.stringify(arg, Object.getOwnPropertyNames(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 {
fs$1.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 (!fs.existsSync(logsDir)) {
return [];
}
const logs = fs.readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
const fullPath = path.join(logsDir, file);
const stats = fs.statSync(fullPath);
return { file, path: fullPath, modified: stats.mtime };
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
try {
const { readDaemonState } = await Promise.resolve().then(function () { return persistence; });
const state = await readDaemonState();
if (!state) {
return logs;
}
if (state.daemonLogPath && fs.existsSync(state.daemonLogPath)) {
const stats = fs.statSync(state.daemonLogPath);
const persisted = {
file: path.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.z.object({
c: z.z.string(),
// Base64 encoded encrypted content
t: z.z.literal("encrypted")
});
const UpdateBodySchema = z.z.object({
message: z.z.object({
id: z.z.string(),
seq: z.z.number(),
content: SessionMessageContentSchema
}),
sid: z.z.string(),
// Session ID
t: z.z.literal("new-message")
});
const UpdateSessionBodySchema = z.z.object({
t: z.z.literal("update-session"),
sid: z.z.string(),
metadata: z.z.object({
version: z.z.number(),
value: z.z.string()
}).nullish(),
agentState: z.z.object({
version: z.z.number(),
value: z.z.string()
}).nullish()
});
const UpdateMachineBodySchema = z.z.object({
t: z.z.literal("update-machine"),
machineId: z.z.string(),
metadata: z.z.object({
version: z.z.number(),
value: z.z.string()
}).nullish(),
daemonState: z.z.object({
version: z.z.number(),
value: z.z.string()
}).nullish()
});
z.z.object({
id: z.z.string(),
seq: z.z.number(),
body: z.z.union([
UpdateBodySchema,
UpdateSessionBodySchema,
UpdateMachineBodySchema
]),
createdAt: z.z.number()
});
z.z.object({
host: z.z.string(),
platform: z.z.string(),
consortiumCliVersion: z.z.string(),
homeDir: z.z.string(),
consortiumHomeDir: z.z.string(),
consortiumLibDir: z.z.string(),
// Hardware specs — collected once at daemon startup from os module
// (no syscall, libuv-cached). Optional so older daemons stay valid.
cpuCount: z.z.number().optional(),
memoryTotalMb: z.z.number().optional(),
// OpenClaw detection fields (populated by daemon heartbeat)
openclawInstalled: z.z.boolean().optional(),
openclawVersion: z.z.string().nullable().optional(),
openclawWorkspaceExists: z.z.boolean().optional(),
openclawGatewayRunning: z.z.boolean().optional(),
openclawGatewayPort: z.z.number().optional(),
openclawChannels: z.z.array(z.z.string()).optional(),
openclawAgentCount: z.z.number().optional(),
openclawAgents: z.z.array(z.z.object({
name: z.z.string(),
workspace: z.z.string(),
agentDir: z.z.string(),
model: z.z.string(),
routingRules: z.z.number(),
isDefault: z.z.boolean()
})).optional(),
// Terminal multiplexer capabilities — populated at daemon startup.
// Tells the mobile app whether "New persistent terminal" should be enabled.
terminalCapabilities: z.z.object({
tmux: z.z.boolean(),
zellij: z.z.boolean()
}).optional()
});
z.z.object({
status: z.z.union([
z.z.enum(["running", "shutting-down"]),
z.z.string()
// Forward compatibility
]),
pid: z.z.number().optional(),
httpPort: z.z.number().optional(),
startedAt: z.z.number().optional(),
shutdownRequestedAt: z.z.number().optional(),
shutdownSource: z.z.union([
z.z.enum(["mobile-app", "cli", "os-signal", "unknown"]),
z.z.string()
// Forward compatibility
]).optional()
});
z.z.object({
content: SessionMessageContentSchema,
createdAt: z.z.number(),
id: z.z.string(),
seq: z.z.number(),
updatedAt: z.z.number()
});
const MessageMetaSchema = z.z.object({
sentFrom: z.z.string().optional(),
// Source identifier
permissionMode: z.z.enum(["default", "acceptEdits", "bypassPermissions", "plan", "read-only", "safe-yolo", "yolo"]).optional(),
// Permission mode for this message
model: z.z.string().nullable().optional(),
// Model name for this message (null = reset)
fallbackModel: z.z.string().nullable().optional(),
// Fallback model for this message (null = reset)
customSystemPrompt: z.z.string().nullable().optional(),
// Custom system prompt for this message (null = reset)
appendSystemPrompt: z.z.string().nullable().optional(),
// Append to system prompt for this message (null = reset)
allowedTools: z.z.array(z.z.string()).nullable().optional(),
// Allowed tools for this message (null = reset)
disallowedTools: z.z.array(z.z.string()).nullable().optional()
// Disallowed tools for this message (null = reset)
});
z.z.object({
session: z.z.object({
id: z.z.string(),
tag: z.z.string(),
seq: z.z.number(),
createdAt: z.z.number(),
updatedAt: z.z.number(),
metadata: z.z.string(),
metadataVersion: z.z.number(),
agentState: z.z.string().nullable(),
agentStateVersion: z.z.number()
})
});
const UserMessageSchema = z.z.object({
role: z.z.literal("user"),
content: z.z.object({
type: z.z.literal("text"),
text: z.z.string(),
// Session-attachments (PR 5): Drive node ids and the client-generated
// local id ride alongside `text` inside the encrypted body. Optional /
// additive — older messages without these fields must continue to
// parse. PR 6 will consume them when forwarding to ACP.
attachmentIds: z.z.array(z.z.string()).optional(),
localId: z.z.string().optional(),
// PR 5 inline-envelope path: when the message carries its own
// driveId + per-message DEK, the resolver can hit Drive directly
// without consulting the server's binding rows.
driveId: z.z.string().optional(),
driveNodeIds: z.z.array(z.z.string()).optional(),
driveDek: z.z.string().optional()
}),
localKey: z.z.string().optional(),
// Mobile messages include this
meta: MessageMetaSchema.optional(),
// Convenience copy — apiSession also surfaces attachmentIds at the top
// level (mirroring `content.attachmentIds`) so consumers don't have to
// reach through `content`. PR 6 owns the forwarding path that reads
// this; PR 5 just makes sure the field is preserved end-to-end.
attachmentIds: z.z.array(z.z.string()).optional(),
localId: z.z.string().optional(),
driveId: z.z.string().optional(),
driveDek: z.z.string().optional()
});
const AgentMessageSchema = z.z.object({
role: z.z.literal("agent"),
content: z.z.object({
type: z.z.literal("output"),
data: z.z.any()
}),
meta: MessageMetaSchema.optional()
});
z.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(node_crypto.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 = node_crypto.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 = node_crypto.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);
}
}
function authChallenge(secret) {
const keypair = tweetnacl.sign.keyPair.fromSeed(secret);
const challenge = getRandomBytes(32);
const signature = tweetnacl.sign.detached(challenge, keypair.secretKey);
return {
challenge,
publicKey: keypair.publicKey,
signature
};
}
class MessageDecodeError extends Error {
constructor(message) {
super(message);
this.name = "MessageDecodeError";
}
}
new TextEncoder();
const strictTextDecoder = new TextDecoder("utf-8", { fatal: true });
function isPlainObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function coerceAttachmentIds(raw) {
if (!Array.isArray(raw)) return [];
const out = [];
for (const entry of raw) {
if (typeof entry !== "string") return [];
out.push(entry);
}
return out;
}
function decodeMessageBody(plaintext) {
let text;
try {
text = strictTextDecoder.decode(plaintext);
} catch (e) {
throw new MessageDecodeError(
`Message body is not valid UTF-8: ${e.message}`
);
}
const trimmed = text.trimStart();
if (!trimmed.startsWith("{")) {
return { v: 1, text };
}
let parsed;
try {
parsed = JSON.parse(text);
} catch {
return { v: 1, text };
}
if (!isPlainObject(parsed)) {
return { v: 1, text };
}
if (typeof parsed.v !== "number" || parsed.v !== 2) {
return { v: 1, text };
}
const innerText = typeof parsed.text === "string" ? parsed.text : "";
const attachmentIds = coerceAttachmentIds(parsed.attachmentIds);
const result = {
v: 2,
text: innerText,
attachmentIds
};
if (typeof parsed.localId === "string") {
result.localId = parsed.localId;
}
return result;
}
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.emitWithAck("rpc-register", { method: prefixedMethod }).catch((err) => {
this.logger(`[RPC] Failed to register ${prefixedMethod}: ${err}`);
});
}
}
/**
* 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));
}
}
async onSocketConnect(socket) {
this.socket = socket;
const registrations = Array.from(this.handlers.keys()).map(
(prefixedMethod) => socket.emitWithAck("rpc-register", { method: prefixedMethod }).catch((err) => {
this.logger(`[RPC] Failed to register ${prefixedMethod}: ${err}`);
})
);
await Promise.all(registrations);
}
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);
}
/**
* Invoke a registered handler in-process with already-decrypted params.
* Used by the generic harness dispatcher to fall through to a legacy
* per-harness RPC (e.g. `openclaw-agent-add`) when the harness provider
* doesn't expose the dispatcher-facing method directly.
*/
async callLocal(method, params) {
const prefixedMethod = this.getPrefixedMethod(method);
const handler = this.handlers.get(prefixedMethod);
if (!handler) return void 0;
return await handler(params);
}
/**
* 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 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-B_i6lpTn.cjs', document.baseURI).href))));
function projectPath() {
const path = path$1.resolve(__dirname$1, "..");
return path;
}
function run$1(args, options) {
const RUNNER_PATH = path$1.resolve(path$1.join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
return new Promise((resolve2, reject) => {
const child = child_process.spawn(process.execPath, [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 = os$1.platform();
const binaryName = platformName === "win32" ? "difft.exe" : "difft";
return path$1.resolve(path$1.join(projectPath(), "tools", "unpacked", binaryName));
}
function run(args, options) {
const binaryPath = getBinaryPath();
return new Promise((resolve2, reject) => {
const child = child_process.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 = path$1.resolve(workingDirectory, targetPath);
const resolvedWorkingDir = path$1.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$1 = util.promisify(child_process.exec);
async function gitExec(command, cwd, timeout = 3e4) {
const options = { cwd, timeout };
const result = await execAsync$1(command, options);
return {
stdout: result.stdout?.toString() || "",
stderr: result.stderr?.toString() || ""
};
}
async function ensureDevGitignore(repoPath) {
const gitignorePath = path$1.join(repoPath, ".gitignore");
try {
const content = await fs$2.readFile(gitignorePath, "utf8");
if (!content.includes(".dev/") && !content.includes(".dev\n")) {
await fs$2.appendFile(gitignorePath, "\n# Worktree directory\n.dev/\n");
}
} catch {
await fs$2.appendFile(gitignorePath, "# Worktree directory\n.dev/\n");
}
}
function slugify(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 40);
}
async function createCardWorktree(repoPath, cardId, cardSlug, baseBranch = "main") {
const slug = slugify(cardSlug);
const cardIdShort = cardId.substring(0, 7);
const branchName = `card/${cardIdShort}/${slug}`;
const worktreeDir = `.dev/worktrees/${slug}`;
const worktreePath = path$1.join(repoPath, worktreeDir);
await ensureDevGitignore(repoPath);
await fs$2.mkdir(path$1.join(repoPath, ".dev/worktrees"), { recursive: true });
try {
await gitExec("git fetch origin", repoPath, 6e4);
} catch (e) {
logger.debug(`[Worktree] Fetch failed (may be offline): ${e}`);
}
const startPoint = `origin/${baseBranch}`;
try {
await gitExec(`git worktree add -b "${branchName}" "${worktreeDir}" "${startPoint}"`, repoPath);
} catch (error) {
if (error.stderr?.includes("already exists")) {
try {
await gitExec(`git worktree add "${worktreeDir}" "${branchName}"`, repoPath);
} catch (e2) {
if (e2.stderr?.includes("already registered")) {
return { worktreePath, branchName };
}
throw new Error(`Failed to create worktree: ${e2.stderr || e2.message}`);
}
} else {
throw new Error(`Failed to create worktree: ${error.stderr || error.message}`);
}
}
return { worktreePath, branchName };
}
async function listWorktrees(repoPath) {
const { stdout } = await gitExec("git worktree list --porcelain", repoPath);
const worktrees = [];
let current = {};
for (const line of stdout.split("\n")) {
if (line.startsWith("worktree ")) {
if (current.path) {
worktrees.push(current);
}
current = { path: line.substring(9), changedFiles: [] };
} else if (line.startsWith("HEAD ")) {
current.head = line.substring(5);
} else if (line.startsWith("branch ")) {
const branch = line.substring(7).replace("refs/heads/", "");
current.branch = branch;
const cardMatch = branch.match(/^card\/([^/]+)\//);
if (cardMatch) {
current.cardId = cardMatch[1];
}
}
}
if (current.path) {
worktrees.push(current);
}
const cardWorktrees = worktrees.filter((w) => w.path.includes(".dev/worktrees"));
for (const wt of cardWorktrees) {
wt.changedFiles = await getWorktreeChangedFiles(wt.path);
}
return cardWorktrees;
}
async function getWorktreeChangedFiles(worktreePath, repoPath) {
try {
const { stdout } = await gitExec("git diff --name-only origin/main...HEAD", worktreePath);
return stdout.trim().split("\n").filter(Boolean);
} catch {
try {
const { stdout } = await gitExec("git diff --name-only HEAD", worktreePath);
return stdout.trim().split("\n").filter(Boolean);
} catch {
return [];
}
}
}
async function cleanupWorktree(repoPath, worktreePath, deleteBranch = false) {
let branchName;
if (deleteBranch) {
try {
const { stdout } = await gitExec("git rev-parse --abbrev-ref HEAD", worktreePath);
branchName = stdout.trim();
} catch {
}
}
try {
await gitExec(`git worktree remove "${worktreePath}" --force`, repoPath);
} catch (error) {
await gitExec("git worktree prune", repoPath);
}
if (deleteBranch && branchName && branchName !== "main" && branchName !== "master") {
try {
await gitExec(`git branch -D "${branchName}"`, repoPath);
} catch {
}
}
}
async function createPRFromWorktree(worktreePath, title, body, baseBranch = "main") {
try {
await gitExec("git add -A", worktreePath);
await gitExec(`git commit -m "${title.replace(/"/g, '\\"')}" --allow-empty`, worktreePath);
} catch {
}
const { stdout: branchStdout } = await gitExec("git rev-parse --abbrev-ref HEAD", worktreePath);
const branchName = branchStdout.trim();
await gitExec(`git push -u origin "${branchName}"`, worktreePath, 6e4);
const escapedTitle = title.replace(/'/g, "'\\''");
const escapedBody = body.replace(/'/g, "'\\''");
const { stdout: prStdout } = await gitExec(
`gh pr create --title '${escapedTitle}' --body '${escapedBody}' --base '${baseBranch}' --json number,url`,
worktreePath,
6e4
);
const prData = JSON.parse(prStdout.trim());
return {
prNumber: prData.number,
prUrl: prData.url
};
}
const ALWAYS_CONFLICT_FILES = [
"package.json",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"Cargo.lock",
"Gemfile.lock",
"go.sum",
"poetry.lock",
"composer.lock"
];
function detectConflicts(activeWorktrees, proposedFiles) {
const fileToWorktrees = /* @__PURE__ */ new Map();
for (const wt of activeWorktrees) {
for (const file of wt.changedFiles) {
const existing = fileToWorktrees.get(file) || [];
existing.push(wt.branch);
fileToWorktrees.set(file, existing);
}
}
const conflicts = [];
for (const [file, worktrees] of fileToWorktrees) {
if (worktrees.length > 1) {
const basename = file.split("/").pop() || file;
conflicts.push({
file,
worktrees,
isLockFile: ALWAYS_CONFLICT_FILES.includes(basename)
});
}
}
return {
hasConflicts: conflicts.length > 0,
hasLockFileConflicts: conflicts.some((c) => c.isLockFile),
conflicts
};
}
function formatConflictReport(report) {
if (!report.hasConflicts) {
return "No file conflicts detected between active worktrees.";
}
const lines = ["## File Conflict Warning\n"];
if (report.hasLockFileConflicts) {
lines.push("**CRITICAL: Lock file conflicts detected.** These cards should NOT run in parallel.\n");
}
lines.push("Conflicting files:");
for (const conflict of report.conflicts) {
const marker = conflict.isLockFile ? " \u26A0\uFE0F LOCK FILE" : "";
lines.push(`- \`${conflict.file}\`${marker} \u2192 modified by: ${conflict.worktrees.join(", ")}`);
}
return lines.join("\n");
}
const execAsync = util.promisify(child_process.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 fs$2.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 fs$2.readFile(data.path);
const existingHash = crypto.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 fs$2.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 fs$2.writeFile(data.path, buffer);
const hash = crypto.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 fs$2.readdir(data.path, { withFileTypes: true });
const directoryEntries = await Promise.all(
entries.map(async (entry) => {
const fullPath = path$1.join(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 fs$2.stat(fullPath);
size = stats.size;
modified = stats.mtime.getTime();
} catch (error) {
logger.debug(`Failed to stat ${fullPath}:`, error);