feat: add AI Workspace npm bridge setup

This commit is contained in:
Codex
2026-06-24 20:40:12 +03:00
parent 92feff97e6
commit 999fe4d906
7 changed files with 4479 additions and 2 deletions
@@ -0,0 +1,22 @@
# NODE.DC AI Workspace Bridge
Installs the remote Codex worker used by NODE.DC AI Workspace.
Registry form:
```sh
npx --yes @nodedc/ai-workspace-bridge setup <setup-code> --gateway https://ai-hub.nodedc.ru
```
Commands:
```sh
ai-workspace-bridge setup <setup-code> [--gateway <url>] [--install-root <path>] [--workspace <path>] [--port <port>]
ai-workspace-bridge status [--install-root <path>]
ai-workspace-bridge doctor [--install-root <path>]
ai-workspace-bridge start [--install-root <path>]
ai-workspace-bridge stop [--install-root <path>]
ai-workspace-bridge logs [--install-root <path>]
```
The setup code is one-time and short-lived. The CLI does not print the redeemed pairing code or MCP tokens.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,878 @@
#!/usr/bin/env node
import { copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import fssync from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const DEFAULT_GATEWAY = "https://ai-hub.nodedc.ru";
const PACKAGE_NAME = "@nodedc/ai-workspace-bridge";
const SERVICE_NAME = "NDC AI Workspace Bridge";
const WINDOWS_TASK_NAME = "NDC AI Workspace Bridge";
const HEALTH_PATH = "/api/ai-workspace/bridge/v1/health";
const DEFAULT_CODEX_ARGS = [
"exec",
"--json",
"--skip-git-repo-check",
"-c",
"model_reasoning_summary=detailed",
"-c",
"model_supports_reasoning_summaries=true",
"-c",
"use_experimental_reasoning_summary=true",
"-c",
"hide_agent_reasoning=false",
"-c",
"show_raw_agent_reasoning=false",
"-",
];
const CLI_FILE = fileURLToPath(import.meta.url);
const PACKAGE_ROOT = path.resolve(path.dirname(CLI_FILE), "..");
const ASSETS_DIR = path.join(PACKAGE_ROOT, "assets");
class UsageError extends Error {}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
printHelp(args.command);
return;
}
if (args.command === "setup") return runSetup(args);
if (args.command === "status") return runStatus(args, { doctor: false });
if (args.command === "doctor") return runStatus(args, { doctor: true });
if (args.command === "start") return runStart(args);
if (args.command === "stop") return runStop(args);
if (args.command === "logs") return runLogs(args);
throw new UsageError(`Unknown command: ${args.command}`);
}
async function runSetup(args) {
if (!args.setupCode) {
throw new UsageError(`Missing setup code. Use: ai-workspace-bridge setup <setup-code>`);
}
ensureNodeRuntime();
const gateway = cleanHttpEndpoint(args.gateway || process.env.NODEDC_AI_WORKSPACE_GATEWAY || DEFAULT_GATEWAY);
if (!gateway) throw new UsageError("--gateway must be an http(s) URL.");
console.log(`${SERVICE_NAME} setup`);
console.log(`Gateway: ${gateway}`);
const redeemPayload = await redeemSetupCode(gateway, args.setupCode);
const setup = normalizeSetupPayload(redeemPayload);
const bridge = setup.bridge;
const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
const workspace = path.resolve(expandHome(args.workspace || bridge.workspace || process.cwd()));
const port = sanitizePort(args.port || bridge.port || 8787);
const codexHome = path.join(installRoot, "codex-home");
const paths = buildInstallPaths(installRoot);
const pathDirs = resolvePathDirs();
await mkdir(paths.logsDir, { recursive: true });
await mkdir(workspace, { recursive: true });
await copyFile(path.join(ASSETS_DIR, "worker.mjs"), paths.workerPath);
await copyFile(path.join(ASSETS_DIR, "ndcAgentMcpServer.mjs"), paths.ndcAgentMcpServerPath);
await writeStartScript(paths.startScriptPath);
await writeConfig(paths.configPath, {
service: SERVICE_NAME,
packageName: PACKAGE_NAME,
installedAt: new Date().toISOString(),
gateway,
installRoot,
workspace,
port,
codexHome,
worker: paths.workerPath,
ndcAgentMcpServer: paths.ndcAgentMcpServerPath,
startScript: paths.startScriptPath,
pidPath: paths.pidPath,
logPath: paths.logPath,
healthPath: HEALTH_PATH,
nodePath: process.execPath,
pathDirs,
hubUrl: bridge.hubUrl,
hubUrls: bridge.hubUrls,
pairingCode: bridge.pairingCode,
machineName: bridge.machineName || os.hostname(),
appMcpServers: bridge.appMcpServers,
});
if (!args.noCodexInstall) {
await ensureCodexCli({ pathDirs });
}
await setupCodexHome(codexHome, bridge.appMcpServers);
if (!args.noCodexLogin) {
await ensureCodexLogin(codexHome, { pathDirs });
}
if (!args.noAutostart) {
await installAutostart(paths.startScriptPath, installRoot).catch((error) => {
console.warn(`Autostart was not registered: ${error.message}`);
});
}
await stopBridge(installRoot).catch(() => {});
await startBridge(installRoot);
const health = await waitForHealth(port, 15000);
console.log(`${SERVICE_NAME} setup complete.`);
console.log(`Install root: ${installRoot}`);
console.log(`Workspace: ${workspace}`);
console.log(`Health: ${health.ok ? "online" : "not ready"}`);
console.log(`Run: ai-workspace-bridge doctor`);
}
async function runStatus(args, { doctor }) {
const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
const config = await readLocalConfig(installRoot);
if (!config) {
console.log(`${SERVICE_NAME} is not installed at ${installRoot}`);
process.exitCode = 1;
return;
}
const health = await fetchHealth(config.port);
const codex = doctor ? inspectCodex(config.codexHome, config.pathDirs || []) : null;
console.log(`${SERVICE_NAME} status`);
console.log(`Install root: ${config.installRoot || installRoot}`);
console.log(`Workspace: ${config.workspace || ""}`);
console.log(`Port: ${config.port || ""}`);
console.log(`Hub: ${health.ok ? "online" : "offline"}`);
if (doctor) {
console.log(`Node: ${process.version}`);
console.log(`Codex CLI: ${codex.codexOk ? codex.codexVersion || "ok" : "missing"}`);
console.log(`Codex auth: ${codex.authOk ? "present" : "missing"}`);
if (codex.error) console.log(`Codex detail: ${codex.error}`);
}
process.exitCode = health.ok && (!doctor || (codex.codexOk && codex.authOk)) ? 0 : 1;
}
async function runStart(args) {
const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
await startBridge(installRoot);
console.log(`${SERVICE_NAME} started.`);
}
async function runStop(args) {
const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
await stopBridge(installRoot);
console.log(`${SERVICE_NAME} stopped.`);
}
async function runLogs(args) {
const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
const config = await readLocalConfig(installRoot);
const logPath = config?.logPath || path.join(installRoot, "logs", "bridge.log");
const text = await readFile(logPath, "utf8").catch(() => "");
console.log(tailLines(text, Number(args.lines || 120)));
}
function parseArgs(rawArgs) {
let args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
let command = "setup";
if (["setup", "status", "doctor", "start", "stop", "logs"].includes(args[0])) {
command = args[0];
args = args.slice(1);
} else if (args[0] === "help") {
return { command: "help", help: true };
}
const parsed = {
command,
gateway: "",
setupCode: "",
installRoot: "",
workspace: "",
port: "",
lines: "",
help: false,
noAutostart: false,
noCodexInstall: false,
noCodexLogin: false,
};
const positional = [];
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
parsed.help = true;
continue;
}
if (arg === "--gateway") {
parsed.gateway = requireValue(args, ++i, arg);
continue;
}
if (arg.startsWith("--gateway=")) {
parsed.gateway = arg.slice("--gateway=".length);
continue;
}
if (arg === "--install-root") {
parsed.installRoot = requireValue(args, ++i, arg);
continue;
}
if (arg.startsWith("--install-root=")) {
parsed.installRoot = arg.slice("--install-root=".length);
continue;
}
if (arg === "--workspace") {
parsed.workspace = requireValue(args, ++i, arg);
continue;
}
if (arg.startsWith("--workspace=")) {
parsed.workspace = arg.slice("--workspace=".length);
continue;
}
if (arg === "--port") {
parsed.port = requireValue(args, ++i, arg);
continue;
}
if (arg.startsWith("--port=")) {
parsed.port = arg.slice("--port=".length);
continue;
}
if (arg === "--lines") {
parsed.lines = requireValue(args, ++i, arg);
continue;
}
if (arg.startsWith("--lines=")) {
parsed.lines = arg.slice("--lines=".length);
continue;
}
if (arg === "--no-autostart") {
parsed.noAutostart = true;
continue;
}
if (arg === "--no-codex-install") {
parsed.noCodexInstall = true;
continue;
}
if (arg === "--no-codex-login") {
parsed.noCodexLogin = true;
continue;
}
if (arg === "--setup-code" || arg === "--code") {
parsed.setupCode = requireValue(args, ++i, arg);
continue;
}
if (arg.startsWith("--setup-code=")) {
parsed.setupCode = arg.slice("--setup-code=".length);
continue;
}
if (arg.startsWith("--code=")) {
parsed.setupCode = arg.slice("--code=".length);
continue;
}
if (arg.startsWith("-")) throw new UsageError(`Unknown argument: ${arg}`);
positional.push(arg);
}
if (command === "setup") {
parsed.setupCode ||= positional.shift() || "";
}
if (positional.length) throw new UsageError(`Unexpected argument: ${positional[0]}`);
return parsed;
}
function requireValue(args, index, flag) {
const value = args[index];
if (!value || value.startsWith("--")) throw new UsageError(`Missing value for ${flag}.`);
return value;
}
function printHelp() {
console.log(`Install ${SERVICE_NAME}.
Usage:
ai-workspace-bridge setup <setup-code> [--gateway <url>] [--install-root <path>] [--workspace <path>] [--port <port>]
ai-workspace-bridge status [--install-root <path>]
ai-workspace-bridge doctor [--install-root <path>]
ai-workspace-bridge start [--install-root <path>]
ai-workspace-bridge stop [--install-root <path>]
ai-workspace-bridge logs [--install-root <path>]
Registry form:
npx --yes ${PACKAGE_NAME} setup <setup-code> --gateway ${DEFAULT_GATEWAY}
`);
}
async function redeemSetupCode(gateway, setupCode) {
let response;
try {
response = await fetch(`${gateway}/api/ai-workspace/setup-codes/redeem`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ setupCode, setup_code: setupCode }),
});
} catch (error) {
throw new Error(`Setup code redeem failed: ${error.message}`);
}
const bodyText = await response.text();
let data = {};
try {
data = bodyText ? JSON.parse(bodyText) : {};
} catch {
throw new Error(`Setup code redeem failed: HTTP ${response.status} ${bodyText}`);
}
if (!response.ok || data.ok === false) {
throw new Error(`Setup code redeem failed: ${data.error || `HTTP ${response.status}`}`);
}
return data;
}
function normalizeSetupPayload(payload) {
const setup = isPlainObject(payload?.setup) ? payload.setup : {};
const bridge = isPlainObject(setup.bridge) ? setup.bridge : {};
const pairingCode = cleanString(bridge.pairingCode || bridge.pairing_code, 80);
const hubUrl = cleanWsEndpoint(bridge.hubUrl || bridge.hub_url);
const hubUrls = uniqueStrings([
hubUrl,
...(Array.isArray(bridge.hubUrls) ? bridge.hubUrls : []),
...(Array.isArray(bridge.hub_urls) ? bridge.hub_urls : []),
].map(cleanWsEndpoint));
if (!pairingCode) throw new Error("Setup payload is missing pairing code.");
if (!hubUrls.length) throw new Error("Setup payload is missing Hub URL.");
return {
bridge: {
hubUrl: hubUrls[0],
hubUrls,
pairingCode,
machineName: cleanString(bridge.machineName || bridge.machine_name, 120) || os.hostname(),
workspace: cleanString(bridge.workspace || bridge.workspacePath || bridge.workspace_path, 1000),
port: sanitizePort(bridge.port || 8787),
appMcpServers: sanitizeMcpServers(bridge.appMcpServers || bridge.app_mcp_servers),
},
};
}
async function writeStartScript(startScriptPath) {
const source = `#!/usr/bin/env node
import { mkdir, open, readFile, writeFile } from "node:fs/promises";
import fssync from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.dirname(fileURLToPath(import.meta.url));
const configPath = path.join(root, "config.json");
const config = JSON.parse(await readFile(configPath, "utf8"));
await mkdir(path.dirname(config.logPath), { recursive: true });
await writeFile(config.pidPath, String(process.pid), "utf8").catch(() => {});
let stopping = false;
let child = null;
process.on("SIGTERM", () => stop("SIGTERM"));
process.on("SIGINT", () => stop("SIGINT"));
function stop(signal) {
stopping = true;
if (child && !child.killed) {
try { child.kill(signal); } catch {}
}
setTimeout(() => process.exit(0), 1500).unref();
}
function buildEnv() {
const delimiter = process.platform === "win32" ? ";" : ":";
const pathDirs = Array.isArray(config.pathDirs) ? config.pathDirs.filter(Boolean) : [];
return {
...process.env,
CODEX_HOME: config.codexHome,
AI_BRIDGE_HOST: "0.0.0.0",
AI_BRIDGE_PORT: String(config.port || 8787),
AI_BRIDGE_CODEX_CWD: config.workspace,
AI_BRIDGE_CODEX_BIN: "codex",
AI_BRIDGE_CODEX_ARGS: ${JSON.stringify(JSON.stringify(DEFAULT_CODEX_ARGS))},
AI_BRIDGE_ALLOW_ORIGIN: "*",
AI_BRIDGE_HUB_URL: config.hubUrl || "",
AI_BRIDGE_HUB_URLS: Array.isArray(config.hubUrls) ? config.hubUrls.join(",") : "",
AI_BRIDGE_PAIRING_CODE: config.pairingCode || "",
AI_BRIDGE_MACHINE_NAME: config.machineName || "",
AI_BRIDGE_ACTIVE_MIRROR_DIR: path.join(config.workspace || root, ".nodedc", "ai-workspace"),
AI_BRIDGE_ACTIVE_MIRROR_OPEN_IDE: "1",
PATH: [...pathDirs, process.env.PATH || ""].filter(Boolean).join(delimiter),
Path: [...pathDirs, process.env.Path || process.env.PATH || ""].filter(Boolean).join(delimiter),
};
}
async function runOnce() {
const log = await open(config.logPath, "a");
try {
child = spawn(config.nodePath || process.execPath, [config.worker], {
cwd: config.installRoot || root,
env: buildEnv(),
stdio: ["ignore", log.fd, log.fd],
windowsHide: true,
});
const code = await new Promise((resolve) => {
child.on("exit", (exitCode, signal) => resolve(signal || exitCode || 0));
child.on("error", (error) => resolve(error.message));
});
child = null;
return code;
} finally {
await log.close().catch(() => {});
}
}
while (!stopping) {
const code = await runOnce();
if (stopping) break;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
`;
await writeFile(startScriptPath, source, "utf8");
await chmodIfPossible(startScriptPath, 0o755);
}
async function writeConfig(configPath, config) {
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
}
async function setupCodexHome(codexHome, appMcpServers) {
await mkdir(codexHome, { recursive: true });
await copyIfExists(path.join(os.homedir(), ".codex", "auth.json"), path.join(codexHome, "auth.json"));
await mergeCodexMcpConfig(path.join(codexHome, "config.toml"), appMcpServers);
}
async function mergeCodexMcpConfig(configPath, appMcpServers) {
const servers = sanitizeMcpServers(appMcpServers);
if (!servers.length) {
await touchFile(configPath);
return;
}
let raw = await readFile(configPath, "utf8").catch(() => "");
for (const server of servers) {
raw = removeTomlSection(raw, `[mcp_servers.${server.serverName}]`);
raw = removeTomlSection(raw, `[mcp_servers.${server.serverName}.headers]`);
raw = removeTomlSection(raw, `[mcp_servers.${server.serverName}.http_headers]`);
const lines = [
`[mcp_servers.${server.serverName}]`,
`url = ${tomlString(server.url)}`,
`startup_timeout_sec = ${server.startupTimeoutSec}`,
`tool_timeout_sec = ${server.toolTimeoutSec}`,
`required = ${server.required ? "true" : "false"}`,
];
if (Object.keys(server.httpHeaders).length) {
lines.push("", `[mcp_servers.${server.serverName}.http_headers]`);
for (const [key, value] of Object.entries(server.httpHeaders)) {
lines.push(`${tomlBareKey(key)} = ${tomlString(value)}`);
}
}
raw = `${raw.trim()}\n\n${lines.join("\n")}\n`;
}
await mkdir(path.dirname(configPath), { recursive: true });
await writeFile(configPath, raw.trimStart(), "utf8");
}
function removeTomlSection(raw, header) {
const lines = String(raw || "").split(/\r?\n/);
const out = [];
let skipping = false;
const normalizedHeader = normalizeTomlHeader(header);
for (const line of lines) {
const current = normalizeTomlHeader(line);
if (current) {
skipping = current === normalizedHeader;
if (skipping) continue;
}
if (!skipping) out.push(line);
}
return out.join("\n").trimEnd();
}
function normalizeTomlHeader(line) {
const text = String(line || "").trim();
if (!text.startsWith("[") || !text.endsWith("]")) return "";
return text.replace(/\s+/g, "").replace(/["']/g, "");
}
async function ensureCodexCli({ pathDirs }) {
const before = runCommandQuiet("codex", ["--version"], { pathDirs });
if (before.ok) return;
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
console.log("Installing Codex CLI with npm...");
await runCommand(npm, ["install", "-g", "@openai/codex", "--no-audit", "--no-fund"], { pathDirs });
const after = runCommandQuiet("codex", ["--version"], { pathDirs: resolvePathDirs() });
if (!after.ok) throw new Error("Codex CLI was installed, but codex is not available in PATH.");
}
async function ensureCodexLogin(codexHome, { pathDirs }) {
if (fssync.existsSync(path.join(codexHome, "auth.json"))) return;
console.log("Codex auth is missing. Opening codex login...");
await runCommand("codex", ["login"], {
pathDirs,
env: { CODEX_HOME: codexHome },
stdio: "inherit",
}).catch((error) => {
console.warn(`Codex login did not complete: ${error.message}`);
});
}
function inspectCodex(codexHome, pathDirs) {
const version = runCommandQuiet("codex", ["--version"], { pathDirs });
return {
codexOk: version.ok,
codexVersion: version.stdout.trim(),
authOk: fssync.existsSync(path.join(codexHome || "", "auth.json")),
error: version.ok ? "" : version.error,
};
}
async function installAutostart(startScriptPath, installRoot) {
if (process.platform === "win32") return installWindowsTask(startScriptPath);
if (process.platform === "darwin") return installLaunchAgent(startScriptPath);
return installSystemdUserService(startScriptPath, installRoot);
}
async function installWindowsTask(startScriptPath) {
const taskRun = `"${process.execPath}" "${startScriptPath}"`;
runCommandQuiet("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"], {});
await runCommand("schtasks", [
"/Create",
"/TN",
WINDOWS_TASK_NAME,
"/SC",
"ONLOGON",
"/TR",
taskRun,
"/RL",
"HIGHEST",
"/F",
], {});
}
async function installLaunchAgent(startScriptPath) {
const label = "ru.nodedc.ai-workspace-bridge";
const plistDir = path.join(os.homedir(), "Library", "LaunchAgents");
const plistPath = path.join(plistDir, `${label}.plist`);
await mkdir(plistDir, { recursive: true });
await writeFile(plistPath, `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>${label}</string>
<key>ProgramArguments</key><array><string>${xmlEscape(process.execPath)}</string><string>${xmlEscape(startScriptPath)}</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>${xmlEscape(path.join(path.dirname(startScriptPath), "logs", "launchd.out.log"))}</string>
<key>StandardErrorPath</key><string>${xmlEscape(path.join(path.dirname(startScriptPath), "logs", "launchd.err.log"))}</string>
</dict></plist>
`, "utf8");
runCommandQuiet("launchctl", ["bootout", `gui/${process.getuid?.()}`, plistPath], {});
await runCommand("launchctl", ["bootstrap", `gui/${process.getuid?.()}`, plistPath], {});
await runCommand("launchctl", ["enable", `gui/${process.getuid?.()}/${label}`], {});
}
async function installSystemdUserService(startScriptPath, installRoot) {
const serviceDir = path.join(os.homedir(), ".config", "systemd", "user");
const servicePath = path.join(serviceDir, "nodedc-ai-workspace-bridge.service");
await mkdir(serviceDir, { recursive: true });
await writeFile(servicePath, `[Unit]
Description=NDC AI Workspace Bridge
[Service]
Type=simple
ExecStart=${process.execPath} ${startScriptPath}
Restart=always
RestartSec=5
WorkingDirectory=${installRoot}
[Install]
WantedBy=default.target
`, "utf8");
await runCommand("systemctl", ["--user", "daemon-reload"], {});
await runCommand("systemctl", ["--user", "enable", "--now", "nodedc-ai-workspace-bridge.service"], {});
}
async function startBridge(installRoot) {
const config = await readLocalConfig(installRoot);
if (!config?.startScript) throw new Error(`${SERVICE_NAME} is not installed at ${installRoot}`);
const child = spawn(config.nodePath || process.execPath, [config.startScript], {
cwd: installRoot,
detached: true,
stdio: "ignore",
windowsHide: true,
});
child.unref();
}
async function stopBridge(installRoot) {
const config = await readLocalConfig(installRoot);
const pidPath = config?.pidPath || path.join(installRoot, "bridge.pid");
const pidText = await readFile(pidPath, "utf8").catch(() => "");
const pid = Number(pidText.trim());
if (!Number.isInteger(pid) || pid <= 0) return;
if (process.platform === "win32") {
runCommandQuiet("taskkill", ["/PID", String(pid), "/T", "/F"], {});
} else {
try {
process.kill(pid, "SIGTERM");
} catch {}
}
await rm(pidPath, { force: true }).catch(() => {});
}
async function waitForHealth(port, timeoutMs) {
const started = Date.now();
let latest = { ok: false, error: "not_checked" };
while (Date.now() - started < timeoutMs) {
latest = await fetchHealth(port);
if (latest.ok) return latest;
await sleep(700);
}
return latest;
}
async function fetchHealth(port) {
try {
const response = await fetch(`http://127.0.0.1:${sanitizePort(port)}${HEALTH_PATH}`);
const data = await response.json().catch(() => ({}));
return { ok: response.ok && data?.ok !== false, data };
} catch (error) {
return { ok: false, error: String(error?.message || error) };
}
}
async function readLocalConfig(installRoot) {
try {
return JSON.parse(await readFile(path.join(installRoot, "config.json"), "utf8"));
} catch {
return null;
}
}
function buildInstallPaths(installRoot) {
return {
logsDir: path.join(installRoot, "logs"),
logPath: path.join(installRoot, "logs", "bridge.log"),
pidPath: path.join(installRoot, "bridge.pid"),
configPath: path.join(installRoot, "config.json"),
workerPath: path.join(installRoot, "worker.mjs"),
ndcAgentMcpServerPath: path.join(installRoot, "ndcAgentMcpServer.mjs"),
startScriptPath: path.join(installRoot, "start-bridge.mjs"),
};
}
function defaultInstallRoot() {
if (process.platform === "win32") {
return path.join(process.env.LOCALAPPDATA || os.homedir(), "NDC", "AIWorkspaceBridge");
}
if (process.platform === "darwin") {
return path.join(os.homedir(), "Library", "Application Support", "NDC", "AIWorkspaceBridge");
}
return path.join(os.homedir(), ".local", "share", "nodedc", "ai-workspace-bridge");
}
function resolvePathDirs() {
const dirs = [path.dirname(process.execPath)];
const prefix = runCommandQuiet(process.platform === "win32" ? "npm.cmd" : "npm", ["prefix", "-g"], {}).stdout.trim();
if (prefix) {
dirs.push(process.platform === "win32" ? prefix : path.join(prefix, "bin"));
}
return uniqueStrings(dirs);
}
function runCommandQuiet(command, args, options = {}) {
try {
const result = spawnSync(command, args, {
env: commandEnv(options),
encoding: "utf8",
shell: process.platform === "win32",
windowsHide: true,
});
return {
ok: result.status === 0,
stdout: result.stdout || "",
stderr: result.stderr || "",
error: result.error ? result.error.message : result.stderr || "",
};
} catch (error) {
return { ok: false, stdout: "", stderr: "", error: String(error?.message || error) };
}
}
function runCommand(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
env: commandEnv(options),
stdio: options.stdio || "inherit",
shell: process.platform === "win32",
windowsHide: true,
});
child.on("error", reject);
child.on("exit", (code) => {
if (code === 0) resolve();
else reject(new Error(`${command} exited with code ${code}`));
});
});
}
function commandEnv(options = {}) {
const delimiter = process.platform === "win32" ? ";" : ":";
const pathDirs = Array.isArray(options.pathDirs) ? options.pathDirs.filter(Boolean) : [];
const basePath = process.env.PATH || process.env.Path || "";
return {
...process.env,
...(options.env || {}),
PATH: [...pathDirs, basePath].filter(Boolean).join(delimiter),
Path: [...pathDirs, process.env.Path || basePath].filter(Boolean).join(delimiter),
};
}
function ensureNodeRuntime() {
const major = Number(process.versions.node.split(".")[0]);
if (!Number.isInteger(major) || major < 22) {
throw new Error(`${SERVICE_NAME} requires Node.js 22+ for WebSocket bridge mode. Current: ${process.version}`);
}
}
function sanitizeMcpServers(value) {
const items = Array.isArray(value) ? value : [];
return items.map((item) => {
if (!isPlainObject(item)) return null;
const serverName = safeMcpServerName(item.serverName || item.server_name || item.name);
const url = cleanHttpEndpoint(item.url);
if (!serverName || !url) return null;
return {
serverName,
url,
required: item.required === true,
startupTimeoutSec: sanitizeInteger(item.startupTimeoutSec || item.startup_timeout_sec, 20, 1, 300),
toolTimeoutSec: sanitizeInteger(item.toolTimeoutSec || item.tool_timeout_sec, 60, 1, 3600),
httpHeaders: sanitizeHeaders(item.httpHeaders || item.http_headers || item.headers),
};
}).filter(Boolean);
}
function sanitizeHeaders(value) {
if (!isPlainObject(value)) return {};
const headers = {};
for (const [key, raw] of Object.entries(value)) {
const name = cleanString(key, 120);
const text = cleanString(raw, 2000);
if (!name || !text || /[\r\n\0]/.test(name) || /[\r\n\0]/.test(text)) continue;
headers[name] = text;
}
return headers;
}
function safeMcpServerName(value) {
const text = String(value || "").trim().replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
return text || "";
}
function cleanHttpEndpoint(value) {
const text = cleanString(value, 1000);
if (!text) return "";
try {
const url = new URL(text);
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
url.username = "";
url.password = "";
return url.toString().replace(/\/+$/, "");
} catch {
return "";
}
}
function cleanWsEndpoint(value) {
const text = cleanString(value, 1000);
if (!text) return "";
try {
const url = new URL(text);
if (url.protocol !== "ws:" && url.protocol !== "wss:") return "";
url.username = "";
url.password = "";
return url.toString().replace(/\/+$/, "");
} catch {
return "";
}
}
function sanitizePort(value) {
const port = Number(value || 8787);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new UsageError("port_invalid");
return port;
}
function sanitizeInteger(value, fallback, min, max) {
const number = Number(value || fallback);
if (!Number.isFinite(number)) return fallback;
return Math.min(Math.max(Math.trunc(number), min), max);
}
function cleanString(value, max) {
return String(value || "").trim().slice(0, max);
}
function uniqueStrings(values) {
return Array.from(new Set((values || []).map((item) => String(item || "").trim()).filter(Boolean)));
}
function expandHome(value) {
const text = String(value || "");
if (text === "~") return os.homedir();
if (text.startsWith("~/") || text.startsWith("~\\")) return path.join(os.homedir(), text.slice(2));
return text;
}
function tomlString(value) {
return JSON.stringify(String(value || ""));
}
function tomlBareKey(value) {
const key = String(value || "");
return /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
}
function xmlEscape(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
async function copyIfExists(from, to) {
try {
await stat(from);
await mkdir(path.dirname(to), { recursive: true });
await copyFile(from, to);
} catch {}
}
async function touchFile(filePath) {
await mkdir(path.dirname(filePath), { recursive: true });
if (!fssync.existsSync(filePath)) await writeFile(filePath, "", "utf8");
}
async function chmodIfPossible(filePath, mode) {
try {
await fssync.promises.chmod(filePath, mode);
} catch {}
}
function tailLines(text, count) {
return String(text || "").split(/\r?\n/).slice(-Math.max(1, count || 120)).join("\n");
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isPlainObject(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
main().catch((error) => {
console.error(error instanceof UsageError ? error.message : `Error: ${error.message || error}`);
process.exitCode = 1;
});
@@ -0,0 +1,29 @@
{
"name": "@nodedc/ai-workspace-bridge",
"version": "0.1.0",
"description": "Install NODE.DC AI Workspace Bridge worker.",
"type": "module",
"bin": {
"ai-workspace-bridge": "./bin/nodedc-ai-workspace-bridge.mjs",
"nodedc-ai-workspace-bridge": "./bin/nodedc-ai-workspace-bridge.mjs"
},
"engines": {
"node": ">=22"
},
"files": [
"assets/",
"bin/",
"README.md"
],
"keywords": [
"nodedc",
"ai-workspace",
"codex",
"bridge"
],
"license": "UNLICENSED",
"private": false,
"publishConfig": {
"access": "public"
}
}