Files
ContractorAgent/scripts/install.mjs
zhi 0b24330787 fix(bridge): emit empty content delta as heartbeat; preserve user provider fields on reinstall
OpenClaw's LLM idle watchdog (default 120s) fires on lack of *model
progress*, not lack of bytes — an SSE comment frame (": keepalive\n\n")
keeps the TCP socket alive but isn't recognized as progress, so a long
quiet tool-call phase still idles out. When that happens OpenClaw falls
back to re-sending the prior turn's assistant text (pi-embedded:1308
fallbackAnswerText), producing duplicate-Discord-message symptoms.

Heartbeat now emits a real chat.completion.chunk with an empty content
delta every 30s. Clients drop empty deltas; the upstream idle watchdog
should count it as model progress because it's a real event on the
canonical streaming channel.

scripts/install.mjs now spreads the existing provider entry before
overriding script-managed fields, so user-added fields like
timeoutSeconds survive reinstall.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 08:53:22 +00:00

189 lines
6.6 KiB
JavaScript

#!/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 — spread existing first so user-added fields
// (e.g. timeoutSeconds, extraHeaders) survive reinstall. Script-managed
// fields (baseUrl/apiKey/api/models) are then overridden authoritatively
// since they're tied to the constants and model catalog above.
cfg.models = cfg.models ?? {};
cfg.models.providers = cfg.models.providers ?? {};
const existingProvider = cfg.models.providers[PLUGIN_ID] ?? {};
cfg.models.providers[PLUGIN_ID] = {
...existingProvider,
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 <id> --workspace <path> --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);
}