#!/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 }); // OpenClaw expects the plugin entry (index.ts) at the plugin root, // matching the convention used by existing plugins like dirigent. // We flatten src/ to the plugin root and copy supporting files alongside it. // Flatten src/ → plugin root const srcDir = path.join(PROJECT_ROOT, "src"); fs.cpSync(srcDir, PLUGIN_INSTALL_DIR, { recursive: true }); // Copy package.json (update main to point to index.ts at root) const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf8")); pkg.main = "index.ts"; fs.writeFileSync( path.join(PLUGIN_INSTALL_DIR, "package.json"), JSON.stringify(pkg, null, 2) + "\n", ); // Copy openclaw.plugin.json — set main to root index.ts const manifest = JSON.parse( fs.readFileSync(path.join(PROJECT_ROOT, "openclaw.plugin.json"), "utf8"), ); manifest.main = "index.ts"; fs.writeFileSync( path.join(PLUGIN_INSTALL_DIR, "openclaw.plugin.json"), JSON.stringify(manifest, null, 2) + "\n", ); // 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, }, ], }; // 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); }