#!/usr/bin/env node /** * Install / uninstall the contractor-agent plugin into OpenClaw. * * Usage: * node scripts/install.mjs --install * node scripts/install.mjs --uninstall */ import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import { execSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PROJECT_ROOT = path.resolve(__dirname, ".."); const PLUGIN_ID = "contractor-agent"; const PLUGIN_INSTALL_DIR = path.join(os.homedir(), ".openclaw", "plugins", PLUGIN_ID); const OPENCLAW_CONFIG = path.join(os.homedir(), ".openclaw", "openclaw.json"); const BRIDGE_PORT = 18800; const BRIDGE_API_KEY = "contractor-bridge-local"; // ── Helpers ─────────────────────────────────────────────────────────────────── function readConfig() { return JSON.parse(fs.readFileSync(OPENCLAW_CONFIG, "utf8")); } function writeConfig(cfg) { fs.writeFileSync(OPENCLAW_CONFIG, JSON.stringify(cfg, null, 2) + "\n", "utf8"); } function setIfMissing(obj, key, value) { if (obj[key] === undefined || obj[key] === null) obj[key] = value; } // ── Install ─────────────────────────────────────────────────────────────────── function install() { console.log(`[install] Installing ${PLUGIN_ID}...`); // 1. Copy plugin files to ~/.openclaw/plugins/contractor-agent/ if (fs.existsSync(PLUGIN_INSTALL_DIR)) { fs.rmSync(PLUGIN_INSTALL_DIR, { recursive: true }); } fs.mkdirSync(PLUGIN_INSTALL_DIR, { recursive: true }); // Copy plugin/ contents to the install root. // plugin/ contains index.ts, openclaw.plugin.json, package.json, and subdirs. const pluginDir = path.join(PROJECT_ROOT, "plugin"); fs.cpSync(pluginDir, PLUGIN_INSTALL_DIR, { recursive: true }); // Copy services/ (standalone processes used by the plugin) into the install root. const servicesDir = path.join(PROJECT_ROOT, "services"); if (fs.existsSync(servicesDir)) { fs.cpSync(servicesDir, path.join(PLUGIN_INSTALL_DIR, "services"), { recursive: true }); } // 2. Install npm dependencies inside the plugin dir console.log(`[install] Installing npm dependencies...`); execSync("npm install --omit=dev --no-audit --no-fund", { cwd: PLUGIN_INSTALL_DIR, stdio: "inherit", }); // 3. Update openclaw.json const cfg = readConfig(); // Add provider cfg.models = cfg.models ?? {}; cfg.models.providers = cfg.models.providers ?? {}; cfg.models.providers[PLUGIN_ID] = { baseUrl: `http://127.0.0.1:${BRIDGE_PORT}/v1`, apiKey: BRIDGE_API_KEY, api: "openai-completions", models: [ { id: "contractor-claude-bridge", name: "Contractor Claude Bridge", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200000, maxTokens: 16000, }, { id: "contractor-gemini-bridge", name: "Contractor Gemini Bridge", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200000, maxTokens: 16000, }, ], }; // Add to plugin allow list cfg.plugins = cfg.plugins ?? {}; cfg.plugins.allow = cfg.plugins.allow ?? []; if (!cfg.plugins.allow.includes(PLUGIN_ID)) { cfg.plugins.allow.push(PLUGIN_ID); } // Add load path cfg.plugins.load = cfg.plugins.load ?? {}; cfg.plugins.load.paths = cfg.plugins.load.paths ?? []; if (!cfg.plugins.load.paths.includes(PLUGIN_INSTALL_DIR)) { cfg.plugins.load.paths.push(PLUGIN_INSTALL_DIR); } // Add plugin entry cfg.plugins.entries = cfg.plugins.entries ?? {}; cfg.plugins.entries[PLUGIN_ID] = cfg.plugins.entries[PLUGIN_ID] ?? {}; cfg.plugins.entries[PLUGIN_ID].enabled = true; // Set default config — setIfMissing so user values are preserved const pluginCfg = cfg.plugins.entries[PLUGIN_ID].config ?? {}; setIfMissing(pluginCfg, "bridgePort", BRIDGE_PORT); setIfMissing(pluginCfg, "bridgeApiKey", BRIDGE_API_KEY); setIfMissing(pluginCfg, "permissionMode", "bypassPermissions"); cfg.plugins.entries[PLUGIN_ID].config = pluginCfg; writeConfig(cfg); console.log(`[install] Done. Restart the gateway to activate:`); console.log(` openclaw gateway restart`); console.log(` openclaw contractor-agents add --agent-id --workspace --contractor claude`); } // ── Uninstall ───────────────────────────────────────────────────────────────── function uninstall() { console.log(`[uninstall] Removing ${PLUGIN_ID}...`); const cfg = readConfig(); // Remove provider if (cfg.models?.providers?.[PLUGIN_ID]) { delete cfg.models.providers[PLUGIN_ID]; } // Remove from allow list if (Array.isArray(cfg.plugins?.allow)) { cfg.plugins.allow = cfg.plugins.allow.filter((id) => id !== PLUGIN_ID); } // Remove load path if (Array.isArray(cfg.plugins?.load?.paths)) { cfg.plugins.load.paths = cfg.plugins.load.paths.filter( (p) => p !== PLUGIN_INSTALL_DIR, ); } // Remove plugin entry if (cfg.plugins?.entries?.[PLUGIN_ID]) { delete cfg.plugins.entries[PLUGIN_ID]; } writeConfig(cfg); // Remove installed files if (fs.existsSync(PLUGIN_INSTALL_DIR)) { fs.rmSync(PLUGIN_INSTALL_DIR, { recursive: true }); console.log(`[uninstall] Removed ${PLUGIN_INSTALL_DIR}`); } console.log(`[uninstall] Done. Restart the gateway:`); console.log(` openclaw gateway restart`); } // ── Main ────────────────────────────────────────────────────────────────────── const arg = process.argv[2]; if (arg === "--install") { install(); } else if (arg === "--uninstall") { uninstall(); } else { console.error("Usage: node scripts/install.mjs --install | --uninstall"); process.exit(1); }