#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import { execFileSync, spawnSync } from "node:child_process"; const VALID_MODES = new Set(["--install", "--uninstall", "--update"]); let modeArg = null; let argOpenClawDir = null; let argNoReplyPort = 8787; for (let i = 2; i < process.argv.length; i++) { const arg = process.argv[i]; if (VALID_MODES.has(arg)) { modeArg = arg; } else if (arg === "--openclaw-profile-path" && i + 1 < process.argv.length) { argOpenClawDir = process.argv[++i]; } else if (arg.startsWith("--openclaw-profile-path=")) { argOpenClawDir = arg.split("=").slice(1).join("="); } else if (arg === "--no-reply-port" && i + 1 < process.argv.length) { argNoReplyPort = Number(process.argv[++i]); } else if (arg.startsWith("--no-reply-port=")) { argNoReplyPort = Number(arg.split("=").slice(1).join("=")); } } if (!modeArg) { fail("Usage: node scripts/install.mjs --install|--uninstall|--update [--openclaw-profile-path ] [--no-reply-port ]"); process.exit(2); } if (!Number.isFinite(argNoReplyPort) || argNoReplyPort < 1 || argNoReplyPort > 65535) { fail("invalid --no-reply-port (1-65535)"); process.exit(2); } const mode = modeArg.slice(2); const C = { reset: "\x1b[0m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", cyan: "\x1b[36m", }; function color(t, c = "reset") { return `${C[c] || ""}${t}${C.reset}`; } function title(t) { console.log(color(`\n[dirigent] ${t}`, "cyan")); } function step(n, total, msg) { console.log(color(`[${n}/${total}] ${msg}`, "blue")); } function ok(msg) { console.log(color(`\t✓ ${msg}`, "green")); } function warn(msg) { console.log(color(`\t⚠ ${msg}`, "yellow")); } function fail(msg) { console.log(color(`\t✗ ${msg}`, "red")); } function resolveOpenClawDir() { if (argOpenClawDir) { const dir = argOpenClawDir.replace(/^~(?=$|\/)/, os.homedir()); if (!fs.existsSync(dir)) throw new Error(`--openclaw-profile-path not found: ${dir}`); return dir; } if (process.env.OPENCLAW_DIR) { const dir = process.env.OPENCLAW_DIR.replace(/^~(?=$|\/)/, os.homedir()); if (fs.existsSync(dir)) return dir; warn(`OPENCLAW_DIR not found: ${dir}, fallback to ~/.openclaw`); } const fallback = path.join(os.homedir(), ".openclaw"); if (!fs.existsSync(fallback)) throw new Error("cannot resolve OpenClaw dir"); return fallback; } const OPENCLAW_DIR = resolveOpenClawDir(); const OPENCLAW_CONFIG_PATH = process.env.OPENCLAW_CONFIG_PATH || path.join(OPENCLAW_DIR, "openclaw.json"); if (!fs.existsSync(OPENCLAW_CONFIG_PATH)) { fail(`config not found: ${OPENCLAW_CONFIG_PATH}`); process.exit(1); } const __dirname = path.dirname(new URL(import.meta.url).pathname); const REPO_ROOT = path.resolve(__dirname, ".."); const PLUGINS_DIR = path.join(OPENCLAW_DIR, "plugins"); const PLUGIN_INSTALL_DIR = path.join(PLUGINS_DIR, "dirigent"); const NO_REPLY_INSTALL_DIR = path.join(PLUGIN_INSTALL_DIR, "no-reply-api"); const NO_REPLY_PROVIDER_ID = process.env.NO_REPLY_PROVIDER_ID || "dirigent"; const NO_REPLY_MODEL_ID = process.env.NO_REPLY_MODEL_ID || "no-reply"; const NO_REPLY_PORT = Number(process.env.NO_REPLY_PORT || argNoReplyPort); const NO_REPLY_BASE_URL = process.env.NO_REPLY_BASE_URL || `http://127.0.0.1:${NO_REPLY_PORT}/v1`; const NO_REPLY_API_KEY = process.env.NO_REPLY_API_KEY || "wg-local-test-token"; const LIST_MODE = process.env.LIST_MODE || "human-list"; const HUMAN_LIST_JSON = process.env.HUMAN_LIST_JSON || "[]"; const AGENT_LIST_JSON = process.env.AGENT_LIST_JSON || "[]"; const CHANNEL_POLICIES_FILE = process.env.CHANNEL_POLICIES_FILE || path.join(OPENCLAW_DIR, "dirigent-channel-policies.json"); const CHANNEL_POLICIES_JSON = process.env.CHANNEL_POLICIES_JSON || "{}"; const END_SYMBOLS_JSON = process.env.END_SYMBOLS_JSON || '["🔚"]'; const SCHEDULING_IDENTIFIER = process.env.SCHEDULING_IDENTIFIER || "➡️"; function runOpenclaw(args, allowFail = false) { try { return execFileSync("openclaw", args, { encoding: "utf8" }).trim(); } catch (e) { if (allowFail) return null; throw e; } } function getJson(pathKey) { const out = runOpenclaw(["config", "get", pathKey, "--json"], true); if (!out || out === "undefined") return undefined; try { return JSON.parse(out); } catch { return undefined; } } function setJson(pathKey, value) { runOpenclaw(["config", "set", pathKey, JSON.stringify(value), "--json"]); } function unsetPath(pathKey) { runOpenclaw(["config", "unset", pathKey], true); } function syncDirRecursive(src, dest) { fs.mkdirSync(dest, { recursive: true }); fs.cpSync(src, dest, { recursive: true, force: true }); } function isRegistered() { const entry = getJson("plugins.entries.dirigent"); return !!(entry && typeof entry === "object"); } if (mode === "update") { title("Update"); const branch = process.env.DIRIGENT_GIT_BRANCH || "latest"; step(1, 2, `update source branch=${branch}`); execFileSync("git", ["fetch", "origin", branch], { cwd: REPO_ROOT, stdio: "inherit" }); execFileSync("git", ["checkout", branch], { cwd: REPO_ROOT, stdio: "inherit" }); execFileSync("git", ["pull", "origin", branch], { cwd: REPO_ROOT, stdio: "inherit" }); ok("source updated"); step(2, 2, "run install after update"); const script = path.join(REPO_ROOT, "scripts", "install.mjs"); const args = [script, "--install", "--openclaw-profile-path", OPENCLAW_DIR, "--no-reply-port", String(NO_REPLY_PORT)]; const ret = spawnSync(process.execPath, args, { cwd: REPO_ROOT, stdio: "inherit", env: process.env }); process.exit(ret.status ?? 1); } if (mode === "install") { title("Install"); step(1, 6, `environment: ${OPENCLAW_DIR}`); if (isRegistered()) { warn("plugins.entries.dirigent exists; reinstalling in-place"); } step(2, 6, "build dist assets"); const pluginSrc = path.resolve(REPO_ROOT, "plugin"); const noReplySrc = path.resolve(REPO_ROOT, "no-reply-api"); const distPlugin = path.resolve(REPO_ROOT, "dist", "dirigent"); const distNoReply = path.resolve(REPO_ROOT, "dist", "dirigent", "no-reply-api"); syncDirRecursive(pluginSrc, distPlugin); syncDirRecursive(noReplySrc, distNoReply); step(3, 6, `install files -> ${PLUGIN_INSTALL_DIR}`); fs.mkdirSync(PLUGINS_DIR, { recursive: true }); syncDirRecursive(distPlugin, PLUGIN_INSTALL_DIR); syncDirRecursive(distNoReply, NO_REPLY_INSTALL_DIR); // cleanup old layout from previous versions const oldTopLevelNoReply = path.join(PLUGINS_DIR, "no-reply-api"); if (fs.existsSync(oldTopLevelNoReply)) { fs.rmSync(oldTopLevelNoReply, { recursive: true, force: true }); ok(`removed legacy path: ${oldTopLevelNoReply}`); } if (!fs.existsSync(CHANNEL_POLICIES_FILE)) { fs.mkdirSync(path.dirname(CHANNEL_POLICIES_FILE), { recursive: true }); fs.writeFileSync(CHANNEL_POLICIES_FILE, `${CHANNEL_POLICIES_JSON}\n`); ok(`init channel policies file: ${CHANNEL_POLICIES_FILE}`); } step(4, 6, "configure plugin entry/path"); const plugins = getJson("plugins") || {}; const loadPaths = Array.isArray(plugins?.load?.paths) ? plugins.load.paths : []; if (!loadPaths.includes(PLUGIN_INSTALL_DIR)) loadPaths.push(PLUGIN_INSTALL_DIR); plugins.load = plugins.load || {}; plugins.load.paths = loadPaths; plugins.entries = plugins.entries || {}; plugins.entries.dirigent = { enabled: true, config: { enabled: true, discordOnly: true, listMode: LIST_MODE, humanList: JSON.parse(HUMAN_LIST_JSON), agentList: JSON.parse(AGENT_LIST_JSON), channelPoliciesFile: CHANNEL_POLICIES_FILE, endSymbols: JSON.parse(END_SYMBOLS_JSON), schedulingIdentifier: SCHEDULING_IDENTIFIER, noReplyProvider: NO_REPLY_PROVIDER_ID, noReplyModel: NO_REPLY_MODEL_ID, noReplyPort: NO_REPLY_PORT, }, }; setJson("plugins", plugins); step(5, 6, "configure no-reply provider"); const providers = getJson("models.providers") || {}; providers[NO_REPLY_PROVIDER_ID] = { baseUrl: NO_REPLY_BASE_URL, apiKey: NO_REPLY_API_KEY, api: "openai-completions", models: [ { id: NO_REPLY_MODEL_ID, name: `${NO_REPLY_MODEL_ID} (Custom Provider)`, reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200000, maxTokens: 8192, }, ], }; setJson("models.providers", providers); // Add no-reply model to agents.defaults.models allowlist const agentsDefaultsModels = getJson("agents.defaults.models") || {}; agentsDefaultsModels[`${NO_REPLY_PROVIDER_ID}/${NO_REPLY_MODEL_ID}`] = {}; setJson("agents.defaults.models", agentsDefaultsModels); step(6, 6, "enable plugin in allowlist"); const allow = getJson("plugins.allow") || []; if (!allow.includes("dirigent")) { allow.push("dirigent"); setJson("plugins.allow", allow); } ok(`installed (no-reply port: ${NO_REPLY_PORT})`); console.log("↻ restart gateway: openclaw gateway restart"); process.exit(0); } if (mode === "uninstall") { title("Uninstall"); step(1, 5, `environment: ${OPENCLAW_DIR}`); step(2, 5, "remove allowlist + plugin entry"); const allow = getJson("plugins.allow") || []; const idx = allow.indexOf("dirigent"); if (idx >= 0) { allow.splice(idx, 1); setJson("plugins.allow", allow); ok("removed from plugins.allow"); } unsetPath("plugins.entries.dirigent"); ok("removed plugins.entries.dirigent"); step(3, 5, "remove plugin load path"); const plugins = getJson("plugins") || {}; const paths = Array.isArray(plugins?.load?.paths) ? plugins.load.paths : []; plugins.load = plugins.load || {}; plugins.load.paths = paths.filter((p) => p !== PLUGIN_INSTALL_DIR); setJson("plugins", plugins); ok("removed plugin path from plugins.load.paths"); step(4, 5, "remove no-reply provider"); const providers = getJson("models.providers") || {}; delete providers[NO_REPLY_PROVIDER_ID]; setJson("models.providers", providers); ok(`removed provider ${NO_REPLY_PROVIDER_ID}`); step(5, 5, "remove installed files"); if (fs.existsSync(PLUGIN_INSTALL_DIR)) fs.rmSync(PLUGIN_INSTALL_DIR, { recursive: true, force: true }); if (fs.existsSync(NO_REPLY_INSTALL_DIR)) fs.rmSync(NO_REPLY_INSTALL_DIR, { recursive: true, force: true }); const legacyNoReply = path.join(PLUGINS_DIR, "dirigent-no-reply-api"); if (fs.existsSync(legacyNoReply)) fs.rmSync(legacyNoReply, { recursive: true, force: true }); const oldTopLevelNoReply = path.join(PLUGINS_DIR, "no-reply-api"); if (fs.existsSync(oldTopLevelNoReply)) fs.rmSync(oldTopLevelNoReply, { recursive: true, force: true }); ok("removed installed files"); console.log("↻ restart gateway: openclaw gateway restart"); process.exit(0); }