#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import { execFileSync } from "node:child_process"; const modeArg = process.argv[2]; if (modeArg !== "--install" && modeArg !== "--uninstall") { console.error("Usage: install-dirigent-openclaw.mjs --install | --uninstall"); process.exit(2); } const mode = modeArg === "--install" ? "install" : "uninstall"; const env = process.env; const OPENCLAW_CONFIG_PATH = env.OPENCLAW_CONFIG_PATH || path.join(os.homedir(), ".openclaw", "openclaw.json"); const __dirname = path.dirname(new URL(import.meta.url).pathname); const PLUGIN_PATH = env.PLUGIN_PATH || path.resolve(__dirname, "..", "dist", "dirigent"); const NO_REPLY_PROVIDER_ID = env.NO_REPLY_PROVIDER_ID || "dirigentway"; const NO_REPLY_MODEL_ID = env.NO_REPLY_MODEL_ID || "no-reply"; const NO_REPLY_BASE_URL = env.NO_REPLY_BASE_URL || "http://127.0.0.1:8787/v1"; const NO_REPLY_API_KEY = env.NO_REPLY_API_KEY || "wg-local-test-token"; const LIST_MODE = env.LIST_MODE || "human-list"; const HUMAN_LIST_JSON = env.HUMAN_LIST_JSON || '["561921120408698910","1474088632750047324"]'; const AGENT_LIST_JSON = env.AGENT_LIST_JSON || "[]"; const CHANNEL_POLICIES_FILE = (env.CHANNEL_POLICIES_FILE || "~/.openclaw/dirigent-channel-policies.json").replace(/^~(?=$|\/)/, os.homedir()); const CHANNEL_POLICIES_JSON = env.CHANNEL_POLICIES_JSON || "{}"; const END_SYMBOLS_JSON = env.END_SYMBOLS_JSON || '["🔚"]'; const STATE_DIR = (env.STATE_DIR || "~/.openclaw/dirigent-install-records").replace(/^~(?=$|\/)/, os.homedir()); const LATEST_RECORD_LINK = (env.LATEST_RECORD_LINK || "~/.openclaw/dirigent-install-record-latest.json").replace(/^~(?=$|\/)/, os.homedir()); const ts = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14); const BACKUP_PATH = `${OPENCLAW_CONFIG_PATH}.bak-dirigent-${mode}-${ts}`; const RECORD_PATH = path.join(STATE_DIR, `dirigent-${ts}.json`); const PATH_PLUGINS_LOAD = "plugins.load.paths"; const PATH_PLUGIN_ENTRY = "plugins.entries.dirigent"; const PATH_PROVIDERS = "models.providers"; 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"], { allowFail: true }); if (out == null || out === "") return { exists: false }; return { exists: true, value: JSON.parse(out) }; } function setJson(pathKey, value) { runOpenclaw(["config", "set", pathKey, JSON.stringify(value), "--json"]); } function unsetPath(pathKey) { runOpenclaw(["config", "unset", pathKey], { allowFail: true }); } function writeRecord(modeName, before, after) { fs.mkdirSync(STATE_DIR, { recursive: true }); const rec = { mode: modeName, timestamp: ts, openclawConfigPath: OPENCLAW_CONFIG_PATH, backupPath: BACKUP_PATH, paths: before, applied: after, }; fs.writeFileSync(RECORD_PATH, JSON.stringify(rec, null, 2)); fs.copyFileSync(RECORD_PATH, LATEST_RECORD_LINK); } function readRecord(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } function findLatestInstallRecord() { if (!fs.existsSync(STATE_DIR)) return ""; const files = fs .readdirSync(STATE_DIR) .filter((f) => /^dirigent-\d+\.json$/.test(f)) .sort() .reverse(); for (const f of files) { const p = path.join(STATE_DIR, f); try { const rec = readRecord(p); if (rec?.mode === "install") return p; } catch { // ignore broken records } } return ""; } if (!fs.existsSync(OPENCLAW_CONFIG_PATH)) { console.error(`[dirigent] config not found: ${OPENCLAW_CONFIG_PATH}`); process.exit(1); } if (mode === "install") { fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH); console.log(`[dirigent] backup: ${BACKUP_PATH}`); if (!fs.existsSync(CHANNEL_POLICIES_FILE)) { fs.mkdirSync(path.dirname(CHANNEL_POLICIES_FILE), { recursive: true }); fs.writeFileSync(CHANNEL_POLICIES_FILE, `${CHANNEL_POLICIES_JSON}\n`); console.log(`[dirigent] initialized channel policies file: ${CHANNEL_POLICIES_FILE}`); } const before = { [PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD), [PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY), [PATH_PROVIDERS]: getJson(PATH_PROVIDERS), }; try { const pluginsNow = getJson("plugins").value || {}; const plugins = typeof pluginsNow === "object" ? pluginsNow : {}; plugins.load = plugins.load && typeof plugins.load === "object" ? plugins.load : {}; const paths = Array.isArray(plugins.load.paths) ? plugins.load.paths : []; if (!paths.includes(PLUGIN_PATH)) paths.push(PLUGIN_PATH); plugins.load.paths = paths; plugins.entries = plugins.entries && typeof plugins.entries === "object" ? 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), noReplyProvider: NO_REPLY_PROVIDER_ID, noReplyModel: NO_REPLY_MODEL_ID, }, }; setJson("plugins", plugins); const providersNow = getJson(PATH_PROVIDERS).value || {}; const providers = typeof providersNow === "object" ? providersNow : {}; 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(PATH_PROVIDERS, providers); const after = { [PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD), [PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY), [PATH_PROVIDERS]: getJson(PATH_PROVIDERS), }; writeRecord("install", before, after); console.log("[dirigent] install ok (config written)"); console.log(`[dirigent] record: ${RECORD_PATH}`); console.log("[dirigent] >>> restart gateway to apply: openclaw gateway restart"); } catch (e) { fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH); console.error(`[dirigent] install failed; rollback complete: ${String(e)}`); process.exit(1); } } else { const recFile = env.RECORD_FILE || findLatestInstallRecord(); if (!recFile || !fs.existsSync(recFile)) { console.error("[dirigent] no install record found. set RECORD_FILE= to an install record."); process.exit(1); } fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH); console.log(`[dirigent] backup before uninstall: ${BACKUP_PATH}`); const rec = readRecord(recFile); const before = rec.applied || {}; const target = rec.paths || {}; try { const pluginsNow = getJson("plugins").value || {}; const plugins = typeof pluginsNow === "object" ? pluginsNow : {}; plugins.load = plugins.load && typeof plugins.load === "object" ? plugins.load : {}; plugins.entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : {}; if (target[PATH_PLUGINS_LOAD]?.exists) plugins.load.paths = target[PATH_PLUGINS_LOAD].value; else delete plugins.load.paths; if (target[PATH_PLUGIN_ENTRY]?.exists) plugins.entries.dirigent = target[PATH_PLUGIN_ENTRY].value; else delete plugins.entries.dirigent; setJson("plugins", plugins); if (target[PATH_PROVIDERS]?.exists) setJson(PATH_PROVIDERS, target[PATH_PROVIDERS].value); else unsetPath(PATH_PROVIDERS); const after = { [PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD), [PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY), [PATH_PROVIDERS]: getJson(PATH_PROVIDERS), }; writeRecord("uninstall", before, after); console.log("[dirigent] uninstall ok"); console.log(`[dirigent] record: ${RECORD_PATH}`); } catch (e) { fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH); console.error(`[dirigent] uninstall failed; rollback complete: ${String(e)}`); process.exit(1); } }