From 49baffe2daffc9e123352a4f058e87028757ef3d Mon Sep 17 00:00:00 2001 From: dtzp555-max Date: Fri, 8 May 2026 15:03:27 +1000 Subject: [PATCH] fix(setup): decouple OpenClaw config patch from being mandatory (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OCP markets itself as a standalone OpenAI-compatible proxy with six supported IDE clients (Cline / Cursor / Continue.dev / OpenCode / Aider / OpenClaw). The README §Server Setup says "node setup.mjs" is the installer entrypoint. But on a truly fresh box without OpenClaw installed, setup.mjs hard-fails at line 111: if (!existsSync(CONFIG_PATH)) fail(`OpenClaw config not found...`); This contradicts the standalone-proxy stance and was caught during PR #71 dogfood testing on Pi231. Root cause: the OpenClaw config patch (lines 110-148) was baked in as required because OCP started life as an OpenClaw-internal helper. The project has since rebranded to standalone OCP without decoupling the installer. Fix: gate the OpenClaw config patch (Step 2) and auth-profiles patch (Step 3) on existsSync(CONFIG_PATH). When OpenClaw is present, behavior is byte-for-byte identical to current main (verified via dry-run hash diff against ~/.openclaw/openclaw.json — UNCHANGED). When OpenClaw is absent, the installer logs a graceful warning, skips both patch sections, and continues to start.sh / launchd-plist / systemd-unit creation as before. The summary banner is also conditional: when OpenClaw is absent, it points the user at README § "Client Setup" instead of giving openclaw.json edit instructions. Also moves `import { readdirSync }` from mid-file (line 178, post-use) to the top-level imports block; this was a latent ESM-hoisting quirk that worked but is now syntactically required at the top because the readdirSync call moved inside an `if` block. Out of scope (Iron Rule 11, single-layer PR): server.mjs, models.json, scripts/sync-openclaw.mjs, README.md, ALIGNMENT.md, package.json version, the duplicate-spawn / health-verify logic at line 268+ (that's a separate fix/setup-spawn-conflict PR). Mac mini production unaffected: ~/.openclaw/openclaw.json already exists there, so the OpenClaw-present path is preserved. `ocp update` doesn't invoke setup.mjs, so running services are not touched. Smoke-tested locally: - Path 1 (OpenClaw present): dry-run output functionally identical to current main; config sha256 unchanged after run. - Path 2 (OpenClaw absent, OPENCLAW_STATE_DIR=/tmp/no-such-dir-*): graceful warn, both patch sections skipped, banner shows standalone-mode message, dry-run completes successfully. - npm test: 43/43 pass. Co-authored-by: dtzp555 Co-authored-by: Claude Opus 4.7 --- setup.mjs | 255 +++++++++++++++++++++++++++++------------------------- 1 file changed, 138 insertions(+), 117 deletions(-) diff --git a/setup.mjs b/setup.mjs index e2754dd..c5d3215 100755 --- a/setup.mjs +++ b/setup.mjs @@ -12,7 +12,7 @@ * 4. Creates start.sh for easy launch * 5. Optionally starts the proxy */ -import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs"; +import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync } from "node:fs"; import { execSync } from "node:child_process"; import { join, dirname } from "node:path"; import { homedir } from "node:os"; @@ -107,106 +107,112 @@ try { warn("Make sure you're logged in: claude login"); } -// Check openclaw config -if (!existsSync(CONFIG_PATH)) fail(`OpenClaw config not found at ${CONFIG_PATH}`); -log(`OpenClaw config: ${CONFIG_PATH}`); +// Check openclaw config (optional — OCP runs standalone without OpenClaw) +const OPENCLAW_PRESENT = existsSync(CONFIG_PATH); +if (OPENCLAW_PRESENT) { + log(`OpenClaw config: ${CONFIG_PATH}`); +} else { + warn(`OpenClaw not detected at ${CONFIG_PATH} — skipping OpenClaw integration.`); + warn(`To register OCP with OpenClaw later, install OpenClaw and re-run \`node setup.mjs\`,`); + warn(`or run \`ocp update\` if OpenClaw is installed afterward.`); +} // ── Step 2: Patch openclaw.json ───────────────────────────────────────── -console.log("\n📝 Configuring OpenClaw...\n"); +if (OPENCLAW_PRESENT) { + console.log("\n📝 Configuring OpenClaw...\n"); -const config = readJSON(CONFIG_PATH); + const config = readJSON(CONFIG_PATH); -// Ensure models.providers exists -if (!config.models) config.models = {}; -if (!config.models.providers) config.models.providers = {}; + // Ensure models.providers exists + if (!config.models) config.models = {}; + if (!config.models.providers) config.models.providers = {}; -// Add/update claude-local provider -config.models.providers[PROVIDER_NAME] = { - baseUrl: `http://127.0.0.1:${PORT}/v1`, - api: "openai-completions", - authHeader: false, - models: MODELS, -}; -log(`Provider "${PROVIDER_NAME}" → http://127.0.0.1:${PORT}/v1`); + // Add/update claude-local provider + config.models.providers[PROVIDER_NAME] = { + baseUrl: `http://127.0.0.1:${PORT}/v1`, + api: "openai-completions", + authHeader: false, + models: MODELS, + }; + log(`Provider "${PROVIDER_NAME}" → http://127.0.0.1:${PORT}/v1`); -// Ensure auth profile in config -if (!config.auth) config.auth = {}; -if (!config.auth.profiles) config.auth.profiles = {}; -config.auth.profiles[`${PROVIDER_NAME}:default`] = { - provider: PROVIDER_NAME, - mode: "api_key", -}; -log(`Auth profile "${PROVIDER_NAME}:default" registered`); + // Ensure auth profile in config + if (!config.auth) config.auth = {}; + if (!config.auth.profiles) config.auth.profiles = {}; + config.auth.profiles[`${PROVIDER_NAME}:default`] = { + provider: PROVIDER_NAME, + mode: "api_key", + }; + log(`Auth profile "${PROVIDER_NAME}:default" registered`); -// Add models to agents.defaults.models -if (!config.agents) config.agents = {}; -if (!config.agents.defaults) config.agents.defaults = {}; -if (!config.agents.defaults.models) config.agents.defaults.models = {}; -for (const [key, val] of Object.entries(MODEL_ALIASES)) { - config.agents.defaults.models[key] = val; -} -log(`Model aliases added to agents.defaults.models`); - -// Set idleTimeoutSeconds to 0 — critical for Claude tool-use. -// When Claude calls tools (Bash, Read, etc.), the token stream pauses for 30-120s. -// OpenClaw's default idleTimeoutSeconds (60s) kills the connection mid-tool-call, -// causing exit 143 (SIGTERM) and stuck sessions. Setting to 0 disables the idle timer. -if (!config.agents.defaults.llm) config.agents.defaults.llm = {}; -if (config.agents.defaults.llm.idleTimeoutSeconds === undefined || - config.agents.defaults.llm.idleTimeoutSeconds > 0) { - config.agents.defaults.llm.idleTimeoutSeconds = 0; - log(`Set agents.defaults.llm.idleTimeoutSeconds = 0 (prevents tool-call timeouts)`); -} else { - log(`idleTimeoutSeconds already configured: ${config.agents.defaults.llm.idleTimeoutSeconds}`); -} - -writeJSON(CONFIG_PATH, config); -log(`Config saved`); - -// ── Step 3: Patch auth-profiles.json ──────────────────────────────────── -console.log("\n🔑 Configuring auth profiles...\n"); - -// Find all agent auth-profiles.json files -const agentsDir = join(OPENCLAW_DIR, "agents"); -const agentDirs = existsSync(agentsDir) - ? readdirSync(agentsDir).filter((d) => { - const ap = join(agentsDir, d, "agent", "auth-profiles.json"); - return existsSync(ap); - }) - : []; - -import { readdirSync } from "node:fs"; - -for (const agentId of agentDirs) { - const apPath = join(agentsDir, agentId, "agent", "auth-profiles.json"); - try { - const ap = readJSON(apPath); - if (!ap.profiles) ap.profiles = {}; - - // Add claude-local profile if missing - if (!ap.profiles[`${PROVIDER_NAME}:default`]) { - ap.profiles[`${PROVIDER_NAME}:default`] = { - type: "api_key", - provider: PROVIDER_NAME, - key: "local-proxy-no-auth", - }; - } - - // Add to lastGood if missing - if (!ap.lastGood) ap.lastGood = {}; - if (!ap.lastGood[PROVIDER_NAME]) { - ap.lastGood[PROVIDER_NAME] = `${PROVIDER_NAME}:default`; - } - - writeJSON(apPath, ap); - log(`Agent "${agentId}" auth profile updated`); - } catch (e) { - warn(`Skipped agent "${agentId}": ${e.message}`); + // Add models to agents.defaults.models + if (!config.agents) config.agents = {}; + if (!config.agents.defaults) config.agents.defaults = {}; + if (!config.agents.defaults.models) config.agents.defaults.models = {}; + for (const [key, val] of Object.entries(MODEL_ALIASES)) { + config.agents.defaults.models[key] = val; } -} + log(`Model aliases added to agents.defaults.models`); -if (agentDirs.length === 0) { - warn("No agent auth-profiles.json found — you may need to restart the gateway first"); + // Set idleTimeoutSeconds to 0 — critical for Claude tool-use. + // When Claude calls tools (Bash, Read, etc.), the token stream pauses for 30-120s. + // OpenClaw's default idleTimeoutSeconds (60s) kills the connection mid-tool-call, + // causing exit 143 (SIGTERM) and stuck sessions. Setting to 0 disables the idle timer. + if (!config.agents.defaults.llm) config.agents.defaults.llm = {}; + if (config.agents.defaults.llm.idleTimeoutSeconds === undefined || + config.agents.defaults.llm.idleTimeoutSeconds > 0) { + config.agents.defaults.llm.idleTimeoutSeconds = 0; + log(`Set agents.defaults.llm.idleTimeoutSeconds = 0 (prevents tool-call timeouts)`); + } else { + log(`idleTimeoutSeconds already configured: ${config.agents.defaults.llm.idleTimeoutSeconds}`); + } + + writeJSON(CONFIG_PATH, config); + log(`Config saved`); + + // ── Step 3: Patch auth-profiles.json ──────────────────────────────────── + console.log("\n🔑 Configuring auth profiles...\n"); + + // Find all agent auth-profiles.json files + const agentsDir = join(OPENCLAW_DIR, "agents"); + const agentDirs = existsSync(agentsDir) + ? readdirSync(agentsDir).filter((d) => { + const ap = join(agentsDir, d, "agent", "auth-profiles.json"); + return existsSync(ap); + }) + : []; + + for (const agentId of agentDirs) { + const apPath = join(agentsDir, agentId, "agent", "auth-profiles.json"); + try { + const ap = readJSON(apPath); + if (!ap.profiles) ap.profiles = {}; + + // Add claude-local profile if missing + if (!ap.profiles[`${PROVIDER_NAME}:default`]) { + ap.profiles[`${PROVIDER_NAME}:default`] = { + type: "api_key", + provider: PROVIDER_NAME, + key: "local-proxy-no-auth", + }; + } + + // Add to lastGood if missing + if (!ap.lastGood) ap.lastGood = {}; + if (!ap.lastGood[PROVIDER_NAME]) { + ap.lastGood[PROVIDER_NAME] = `${PROVIDER_NAME}:default`; + } + + writeJSON(apPath, ap); + log(`Agent "${agentId}" auth profile updated`); + } catch (e) { + warn(`Skipped agent "${agentId}": ${e.message}`); + } + } + + if (agentDirs.length === 0) { + warn("No agent auth-profiles.json found — you may need to restart the gateway first"); + } } // ── Step 4: Create start.sh ───────────────────────────────────────────── @@ -238,31 +244,46 @@ if (!DRY_RUN) { log(`Launcher: ${startPath}`); // ── Step 5: Summary ───────────────────────────────────────────────────── -console.log(` -╔══════════════════════════════════════════════════════════════╗ -║ Setup complete! ║ -╠══════════════════════════════════════════════════════════════╣ -║ ║ -║ Provider: ${PROVIDER_NAME.padEnd(44)}║ -║ Port: ${String(PORT).padEnd(44)}║ -║ Models: ${`see models.json (${MODELS.length} available)`.padEnd(44)}║ -║ Default: ${DEFAULT_MODEL_ID.padEnd(44)}║ -║ ║ -║ Start proxy: ║ -║ bash ${startPath.replace(HOME, "~").padEnd(50)}║ -║ ║ -║ Or directly: ║ -║ node ${serverPath.replace(HOME, "~").padEnd(49)}║ -║ ║ -║ Set as default model in openclaw.json: ║ -║ agents.defaults.model.primary = ║ -║ "${PROVIDER_NAME}/${DEFAULT_MODEL_ID}"${" ".repeat(Math.max(0, 30 - PROVIDER_NAME.length - DEFAULT_MODEL_ID.length))}║ -║ ║ -║ Then restart gateway: ║ -║ openclaw gateway restart ║ -║ ║ -╚══════════════════════════════════════════════════════════════╝ -`); +const banner = [ + `╔══════════════════════════════════════════════════════════════╗`, + `║ Setup complete! ║`, + `╠══════════════════════════════════════════════════════════════╣`, + `║ ║`, + `║ Provider: ${PROVIDER_NAME.padEnd(44)}║`, + `║ Port: ${String(PORT).padEnd(44)}║`, + `║ Models: ${`see models.json (${MODELS.length} available)`.padEnd(44)}║`, + `║ Default: ${DEFAULT_MODEL_ID.padEnd(44)}║`, + `║ ║`, + `║ Start proxy: ║`, + `║ bash ${startPath.replace(HOME, "~").padEnd(50)}║`, + `║ ║`, + `║ Or directly: ║`, + `║ node ${serverPath.replace(HOME, "~").padEnd(49)}║`, + `║ ║`, +]; +if (OPENCLAW_PRESENT) { + banner.push( + `║ Set as default model in openclaw.json: ║`, + `║ agents.defaults.model.primary = ║`, + `║ "${PROVIDER_NAME}/${DEFAULT_MODEL_ID}"${" ".repeat(Math.max(0, 30 - PROVIDER_NAME.length - DEFAULT_MODEL_ID.length))}║`, + `║ ║`, + `║ Then restart gateway: ║`, + `║ openclaw gateway restart ║`, + `║ ║`, + ); +} else { + banner.push( + `║ OpenClaw not detected — running in standalone mode. ║`, + `║ Point your IDE (Cline / Cursor / Continue / OpenCode / ║`, + `║ Aider / OpenClaw) at: ║`, + `║ http://${BIND_ADDRESS}:${String(PORT)}/v1${" ".repeat(Math.max(0, 47 - BIND_ADDRESS.length - String(PORT).length))}║`, + `║ ║`, + `║ See README § "Client Setup" for per-IDE instructions. ║`, + `║ ║`, + ); +} +banner.push(`╚══════════════════════════════════════════════════════════════╝`); +console.log("\n" + banner.join("\n") + "\n"); // ── Step 7: Install auto-start on boot ────────────────────────────────── if (!DRY_RUN) {