From 46e56c6760a2be035670256cb4e6eb4b3ead3f0c Mon Sep 17 00:00:00 2001 From: orion Date: Thu, 26 Feb 2026 00:41:56 +0000 Subject: [PATCH] refactor(installer): replace bash installer logic with node-only implementation --- docs/INTEGRATION.md | 8 +- scripts/install-whispergate-openclaw.mjs | 202 +++++++++++++ scripts/install-whispergate-openclaw.sh | 369 +---------------------- 3 files changed, 211 insertions(+), 368 deletions(-) create mode 100755 scripts/install-whispergate-openclaw.mjs diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index a428d7c..a79c405 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -29,19 +29,23 @@ You can merge this snippet manually into your `openclaw.json`. ## Installer script (with rollback) -For production-like install with automatic rollback on error: +For production-like install with automatic rollback on error (Node-only installer): ```bash +node ./scripts/install-whispergate-openclaw.mjs --install +# or wrapper ./scripts/install-whispergate-openclaw.sh --install ``` Uninstall (revert all recorded config changes): ```bash +node ./scripts/install-whispergate-openclaw.mjs --uninstall +# or wrapper ./scripts/install-whispergate-openclaw.sh --uninstall # or specify a record explicitly # RECORD_FILE=~/.openclaw/whispergate-install-records/whispergate-YYYYmmddHHMMSS.json \ -# ./scripts/install-whispergate-openclaw.sh --uninstall +# node ./scripts/install-whispergate-openclaw.mjs --uninstall ``` Environment overrides: diff --git a/scripts/install-whispergate-openclaw.mjs b/scripts/install-whispergate-openclaw.mjs new file mode 100755 index 0000000..ec0da4d --- /dev/null +++ b/scripts/install-whispergate-openclaw.mjs @@ -0,0 +1,202 @@ +#!/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-whispergate-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 PLUGIN_PATH = env.PLUGIN_PATH || "/root/.openclaw/workspace-operator/WhisperGate/dist/whispergate"; +const NO_REPLY_PROVIDER_ID = env.NO_REPLY_PROVIDER_ID || "whisper-gateway"; +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/whispergate-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/whispergate-install-records").replace(/^~(?=$|\/)/, os.homedir()); +const LATEST_RECORD_LINK = (env.LATEST_RECORD_LINK || "~/.openclaw/whispergate-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-whispergate-${mode}-${ts}`; +const RECORD_PATH = path.join(STATE_DIR, `whispergate-${ts}.json`); + +const PATH_PLUGINS_LOAD = "plugins.load.paths"; +const PATH_PLUGIN_ENTRY = "plugins.entries.whispergate"; +const PROVIDER_PATH = `models.providers["${NO_REPLY_PROVIDER_ID}"]`; + +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")); +} + +if (!fs.existsSync(OPENCLAW_CONFIG_PATH)) { + console.error(`[whispergate] config not found: ${OPENCLAW_CONFIG_PATH}`); + process.exit(1); +} + +if (mode === "install") { + fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH); + console.log(`[whispergate] 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(`[whispergate] initialized channel policies file: ${CHANNEL_POLICIES_FILE}`); + } + + const before = { + [PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD), + [PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY), + [PROVIDER_PATH]: getJson(PROVIDER_PATH), + }; + + 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.whispergate = { + 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); + + setJson(PROVIDER_PATH, { + 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: 4096, + maxTokens: 4096, + }, + ], + }); + + const after = { + [PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD), + [PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY), + [PROVIDER_PATH]: getJson(PROVIDER_PATH), + }; + writeRecord("install", before, after); + console.log("[whispergate] install ok"); + console.log(`[whispergate] record: ${RECORD_PATH}`); + } catch (e) { + fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH); + console.error(`[whispergate] install failed; rollback complete: ${String(e)}`); + process.exit(1); + } +} else { + const recFile = env.RECORD_FILE || (fs.existsSync(LATEST_RECORD_LINK) ? LATEST_RECORD_LINK : ""); + if (!recFile || !fs.existsSync(recFile)) { + console.error("[whispergate] no record found. set RECORD_FILE= or install first."); + process.exit(1); + } + + fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH); + console.log(`[whispergate] 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.whispergate = target[PATH_PLUGIN_ENTRY].value; + else delete plugins.entries.whispergate; + + setJson("plugins", plugins); + + for (const k of Object.keys(target)) { + if (!k.startsWith("models.providers[")) continue; + if (target[k]?.exists) setJson(k, target[k].value); + else unsetPath(k); + } + + const after = { + [PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD), + [PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY), + [PROVIDER_PATH]: getJson(PROVIDER_PATH), + }; + writeRecord("uninstall", before, after); + console.log("[whispergate] uninstall ok"); + console.log(`[whispergate] record: ${RECORD_PATH}`); + } catch (e) { + fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH); + console.error(`[whispergate] uninstall failed; rollback complete: ${String(e)}`); + process.exit(1); + } +} diff --git a/scripts/install-whispergate-openclaw.sh b/scripts/install-whispergate-openclaw.sh index fe7f6a4..8ba5a65 100755 --- a/scripts/install-whispergate-openclaw.sh +++ b/scripts/install-whispergate-openclaw.sh @@ -1,367 +1,4 @@ #!/usr/bin/env bash -set -Eeuo pipefail - -# WhisperGate installer/uninstaller for OpenClaw -# Requirements: -# - all writes via `openclaw config set ... --json` -# - install supports rollback on failure -# - uninstall reverts ALL recorded changes -# - every install writes a change record - -OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}" -PLUGIN_PATH="${PLUGIN_PATH:-/root/.openclaw/workspace-operator/WhisperGate/dist/whispergate}" -NO_REPLY_PROVIDER_ID="${NO_REPLY_PROVIDER_ID:-whisper-gateway}" -NO_REPLY_MODEL_ID="${NO_REPLY_MODEL_ID:-no-reply}" -NO_REPLY_BASE_URL="${NO_REPLY_BASE_URL:-http://127.0.0.1:8787/v1}" -NO_REPLY_API_KEY="${NO_REPLY_API_KEY:-wg-local-test-token}" -LIST_MODE="${LIST_MODE:-human-list}" -HUMAN_LIST_JSON="${HUMAN_LIST_JSON:-[\"561921120408698910\",\"1474088632750047324\"]}" -AGENT_LIST_JSON="${AGENT_LIST_JSON:-[]}" -CHANNEL_POLICIES_FILE="${CHANNEL_POLICIES_FILE:-$HOME/.openclaw/whispergate-channel-policies.json}" -CHANNEL_POLICIES_JSON="${CHANNEL_POLICIES_JSON:-{}}" -END_SYMBOLS_JSON="${END_SYMBOLS_JSON:-[\"🔚\"]}" - -STATE_DIR="${STATE_DIR:-$HOME/.openclaw/whispergate-install-records}" -LATEST_RECORD_LINK="${LATEST_RECORD_LINK:-$HOME/.openclaw/whispergate-install-record-latest.json}" - -MODE="" -if [[ "${1:-}" == "--install" ]]; then - MODE="install" -elif [[ "${1:-}" == "--uninstall" ]]; then - MODE="uninstall" -else - echo "Usage: $0 --install | --uninstall" - exit 2 -fi - -TIMESTAMP="$(date +%Y%m%d%H%M%S)" -BACKUP_PATH="${OPENCLAW_CONFIG_PATH}.bak-whispergate-${MODE}-${TIMESTAMP}" -RECORD_PATH="${STATE_DIR}/whispergate-${TIMESTAMP}.json" - -PROVIDER_PATH="models.providers[\"${NO_REPLY_PROVIDER_ID}\"]" -PATH_PLUGINS_LOAD="plugins.load.paths" -PATH_PLUGIN_ENTRY="plugins.entries.whispergate" - -INSTALLED_OK=0 - -require_cmd() { - command -v "$1" >/dev/null 2>&1 || { - echo "[whispergate] missing command: $1" >&2 - exit 1 - } -} - -oc_get_json_or_missing() { - local path="$1" - if openclaw config get "$path" --json >/tmp/wg_get.json 2>/dev/null; then - printf '{"exists":true,"value":%s}\n' "$(cat /tmp/wg_get.json)" - else - printf '{"exists":false}' - fi -} - -oc_set_json() { - local path="$1" - local json="$2" - openclaw config set "$path" "$json" --json >/dev/null -} - -oc_unset() { - local path="$1" - openclaw config unset "$path" >/dev/null -} - -write_record() { - local mode="$1" - local prev_paths_json="$2" - local next_paths_json="$3" - mkdir -p "$STATE_DIR" - python3 - <<'PY' -import json, os -record={ - 'mode': os.environ['REC_MODE'], - 'timestamp': os.environ['TIMESTAMP'], - 'openclawConfigPath': os.environ['OPENCLAW_CONFIG_PATH'], - 'backupPath': os.environ['BACKUP_PATH'], - 'paths': json.loads(os.environ['PREV_PATHS_JSON']), - 'applied': json.loads(os.environ['NEXT_PATHS_JSON']), -} -with open(os.environ['RECORD_PATH'],'w',encoding='utf-8') as f: - json.dump(record,f,ensure_ascii=False,indent=2) -PY - cp -f "$RECORD_PATH" "$LATEST_RECORD_LINK" -} - -rollback_install() { - local ec=$? - if [[ $INSTALLED_OK -eq 1 ]]; then - return - fi - echo "[whispergate] install failed (exit=$ec), rolling back..." - if [[ -f "$BACKUP_PATH" ]]; then - cp -f "$BACKUP_PATH" "$OPENCLAW_CONFIG_PATH" - echo "[whispergate] rollback complete" - else - echo "[whispergate] WARNING: backup missing; rollback skipped" - fi - exit "$ec" -} - -run_install() { - trap rollback_install ERR - - cp -f "$OPENCLAW_CONFIG_PATH" "$BACKUP_PATH" - echo "[whispergate] backup: $BACKUP_PATH" - - # initialize standalone channel policies file if missing - CHANNEL_POLICIES_FILE_RESOLVED="$(CHANNEL_POLICIES_FILE="$CHANNEL_POLICIES_FILE" python3 - <<'PY' -import os -print(os.path.expanduser(os.environ['CHANNEL_POLICIES_FILE'])) -PY -)" - if [[ ! -f "$CHANNEL_POLICIES_FILE_RESOLVED" ]]; then - mkdir -p "$(dirname "$CHANNEL_POLICIES_FILE_RESOLVED")" - printf '%s\n' "$CHANNEL_POLICIES_JSON" > "$CHANNEL_POLICIES_FILE_RESOLVED" - echo "[whispergate] initialized channel policies file: $CHANNEL_POLICIES_FILE_RESOLVED" - fi - - local prev_paths_json - prev_paths_json="$(PATH_PLUGINS_LOAD="$PATH_PLUGINS_LOAD" PATH_PLUGIN_ENTRY="$PATH_PLUGIN_ENTRY" PROVIDER_PATH="$PROVIDER_PATH" python3 - <<'PY' -import json, os, subprocess - -def get(path): - p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True) - if p.returncode==0: - return {'exists':True,'value':json.loads(p.stdout)} - return {'exists':False} - -payload={ - os.environ['PATH_PLUGINS_LOAD']: get(os.environ['PATH_PLUGINS_LOAD']), - os.environ['PATH_PLUGIN_ENTRY']: get(os.environ['PATH_PLUGIN_ENTRY']), - os.environ['PROVIDER_PATH']: get(os.environ['PROVIDER_PATH']), -} -print(json.dumps(payload,ensure_ascii=False)) -PY -)" - - # 1+2) set plugins object in one write (avoid transient schema failure) - local current_plugins_json new_plugins_json - if openclaw config get plugins --json >/tmp/wg_plugins.json 2>/dev/null; then - current_plugins_json="$(cat /tmp/wg_plugins.json)" - else - current_plugins_json='{}' - fi - - new_plugins_json="$(CURRENT_PLUGINS_JSON="$current_plugins_json" PLUGIN_PATH="$PLUGIN_PATH" LIST_MODE="$LIST_MODE" HUMAN_LIST_JSON="$HUMAN_LIST_JSON" AGENT_LIST_JSON="$AGENT_LIST_JSON" CHANNEL_POLICIES_FILE="$CHANNEL_POLICIES_FILE" END_SYMBOLS_JSON="$END_SYMBOLS_JSON" NO_REPLY_PROVIDER_ID="$NO_REPLY_PROVIDER_ID" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY' -import json, os -plugins=json.loads(os.environ['CURRENT_PLUGINS_JSON']) -if not isinstance(plugins,dict): - plugins={} - -load=plugins.setdefault('load',{}) -paths=load.get('paths') -if not isinstance(paths,list): - paths=[] -pp=os.environ['PLUGIN_PATH'] -if pp not in paths: - paths.append(pp) -load['paths']=paths - -entries=plugins.setdefault('entries',{}) -entries['whispergate']={ - 'enabled': True, - 'config': { - 'enabled': True, - 'discordOnly': True, - 'listMode': os.environ['LIST_MODE'], - 'humanList': json.loads(os.environ['HUMAN_LIST_JSON']), - 'agentList': json.loads(os.environ['AGENT_LIST_JSON']), - 'channelPoliciesFile': os.environ['CHANNEL_POLICIES_FILE'], - 'endSymbols': json.loads(os.environ['END_SYMBOLS_JSON']), - 'noReplyProvider': os.environ['NO_REPLY_PROVIDER_ID'], - 'noReplyModel': os.environ['NO_REPLY_MODEL_ID'], - } -} - -print(json.dumps(plugins,ensure_ascii=False)) -PY -)" - oc_set_json "plugins" "$new_plugins_json" - - # 3) provider - local provider_json - provider_json="$(NO_REPLY_BASE_URL="$NO_REPLY_BASE_URL" NO_REPLY_API_KEY="$NO_REPLY_API_KEY" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY' -import json, os -provider={ - 'baseUrl': os.environ['NO_REPLY_BASE_URL'], - 'apiKey': os.environ['NO_REPLY_API_KEY'], - 'api': 'openai-completions', - 'models': [{ - 'id': os.environ['NO_REPLY_MODEL_ID'], - 'name': f"{os.environ['NO_REPLY_MODEL_ID']} (Custom Provider)", - 'reasoning': False, - 'input': ['text'], - 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, - 'contextWindow': 4096, - 'maxTokens': 4096, - }] -} -print(json.dumps(provider,ensure_ascii=False)) -PY -)" - oc_set_json "$PROVIDER_PATH" "$provider_json" - - # validate writes - openclaw config get "$PATH_PLUGINS_LOAD" --json >/dev/null - openclaw config get "$PATH_PLUGIN_ENTRY" --json >/dev/null - openclaw config get "$PROVIDER_PATH" --json >/dev/null - - local next_paths_json - next_paths_json="$(PATH_PLUGINS_LOAD="$PATH_PLUGINS_LOAD" PATH_PLUGIN_ENTRY="$PATH_PLUGIN_ENTRY" PROVIDER_PATH="$PROVIDER_PATH" python3 - <<'PY' -import json, os, subprocess - -def get(path): - p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True) - if p.returncode==0: - return {'exists':True,'value':json.loads(p.stdout)} - return {'exists':False} - -payload={ - os.environ['PATH_PLUGINS_LOAD']: get(os.environ['PATH_PLUGINS_LOAD']), - os.environ['PATH_PLUGIN_ENTRY']: get(os.environ['PATH_PLUGIN_ENTRY']), - os.environ['PROVIDER_PATH']: get(os.environ['PROVIDER_PATH']), -} -print(json.dumps(payload,ensure_ascii=False)) -PY -)" - - REC_MODE="install" PREV_PATHS_JSON="$prev_paths_json" NEXT_PATHS_JSON="$next_paths_json" TIMESTAMP="$TIMESTAMP" OPENCLAW_CONFIG_PATH="$OPENCLAW_CONFIG_PATH" BACKUP_PATH="$BACKUP_PATH" RECORD_PATH="$RECORD_PATH" write_record install "$prev_paths_json" "$next_paths_json" - - INSTALLED_OK=1 - trap - ERR - echo "[whispergate] install ok" - echo "[whispergate] record: $RECORD_PATH" -} - -run_uninstall() { - local rec_file="" - if [[ -n "${RECORD_FILE:-}" ]]; then - rec_file="$RECORD_FILE" - elif [[ -f "$LATEST_RECORD_LINK" ]]; then - rec_file="$LATEST_RECORD_LINK" - else - echo "[whispergate] no record found. set RECORD_FILE= or install first." >&2 - exit 1 - fi - - if [[ ! -f "$rec_file" ]]; then - echo "[whispergate] record file not found: $rec_file" >&2 - exit 1 - fi - - cp -f "$OPENCLAW_CONFIG_PATH" "$BACKUP_PATH" - echo "[whispergate] backup before uninstall: $BACKUP_PATH" - - python3 - <<'PY' -import json, os, subprocess, sys - -rec=json.load(open(os.environ['REC_FILE'],encoding='utf-8')) -paths=rec.get('paths',{}) - -PLUGINS_LOAD='plugins.load.paths' -PLUGINS_ENTRY='plugins.entries.whispergate' -PROVIDER_PATHS=[k for k in paths.keys() if k.startswith('models.providers[')] - -# 1) restore plugins atomically to avoid transient schema failures -pcur=subprocess.run(['openclaw','config','get','plugins','--json'],capture_output=True,text=True) -plugins={} -if pcur.returncode==0: - plugins=json.loads(pcur.stdout) -if not isinstance(plugins,dict): - plugins={} - -load=plugins.get('load') if isinstance(plugins.get('load'),dict) else {} -entries=plugins.get('entries') if isinstance(plugins.get('entries'),dict) else {} - -# restore plugins.load.paths from record -info=paths.get(PLUGINS_LOAD,{'exists':False}) -if info.get('exists'): - load['paths']=info.get('value') -else: - load.pop('paths',None) - -# restore plugins.entries.whispergate from record -info=paths.get(PLUGINS_ENTRY,{'exists':False}) -if info.get('exists'): - entries['whispergate']=info.get('value') -else: - entries.pop('whispergate',None) - -plugins['load']=load -plugins['entries']=entries - -pset=subprocess.run(['openclaw','config','set','plugins',json.dumps(plugins,ensure_ascii=False),'--json'],capture_output=True,text=True) -if pset.returncode!=0: - sys.stderr.write(pset.stderr or pset.stdout) - raise SystemExit(1) - -# 2) restore provider paths (usually custom no-reply provider) -for path in PROVIDER_PATHS: - info=paths.get(path,{'exists':False}) - if info.get('exists'): - val=json.dumps(info.get('value'),ensure_ascii=False) - p=subprocess.run(['openclaw','config','set',path,val,'--json'],capture_output=True,text=True) - if p.returncode!=0: - sys.stderr.write(p.stderr or p.stdout) - raise SystemExit(1) - else: - p=subprocess.run(['openclaw','config','unset',path],capture_output=True,text=True) - if p.returncode!=0: - txt=(p.stderr or p.stdout or '').lower() - if 'not found' not in txt and 'missing' not in txt and 'does not exist' not in txt: - sys.stderr.write(p.stderr or p.stdout) - raise SystemExit(1) - -print('ok') -PY - - local next_paths_json - next_paths_json="$(python3 - <<'PY' -import json, os, subprocess -paths=json.load(open(os.environ['REC_FILE'],encoding='utf-8')).get('paths',{}).keys() -def get(path): - p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True) - if p.returncode==0: - return {'exists':True,'value':json.loads(p.stdout)} - return {'exists':False} -payload={k:get(k) for k in paths} -print(json.dumps(payload,ensure_ascii=False)) -PY -)" - - local prev_paths_json - prev_paths_json="$(python3 - <<'PY' -import json, os -print(json.dumps(json.load(open(os.environ['REC_FILE'],encoding='utf-8')).get('applied',{}),ensure_ascii=False)) -PY -)" - - REC_MODE="uninstall" PREV_PATHS_JSON="$prev_paths_json" NEXT_PATHS_JSON="$next_paths_json" TIMESTAMP="$TIMESTAMP" OPENCLAW_CONFIG_PATH="$OPENCLAW_CONFIG_PATH" BACKUP_PATH="$BACKUP_PATH" RECORD_PATH="$RECORD_PATH" write_record uninstall "$prev_paths_json" "$next_paths_json" - - echo "[whispergate] uninstall ok" - echo "[whispergate] record: $RECORD_PATH" -} - -main() { - require_cmd openclaw - require_cmd python3 - [[ -f "$OPENCLAW_CONFIG_PATH" ]] || { echo "[whispergate] config not found: $OPENCLAW_CONFIG_PATH"; exit 1; } - - if [[ "$MODE" == "install" ]]; then - run_install - else - REC_FILE="${RECORD_FILE:-$LATEST_RECORD_LINK}" run_uninstall - fi -} - -main +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +exec node "$SCRIPT_DIR/install-whispergate-openclaw.mjs" "$@"