Files
ocp/scripts/sync-openclaw.mjs
T
9e25160527 refactor: hoist port literal to lib/constants.mjs + CI gate (v3.16.4) (#98)
Closes the structural side of the port-drift cascade addressed by
v3.16.2/v3.16.3. Those releases reverted the literal line-by-line; this
one removes the invitation to drift.

Changes:
  * NEW lib/constants.mjs — exports DEFAULT_PORT=3456, LOCAL_HOST,
    OPENAI_API_BASE, LOCAL_PROXY_URL.
  * server.mjs / setup.mjs / scripts/upgrade.mjs / scripts/doctor.mjs
    (x2) / scripts/sync-openclaw.mjs all import DEFAULT_PORT from
    lib/constants.mjs instead of hardcoding "3456".
  * .github/workflows/alignment.yml:
    - path filter extended to setup.mjs, scripts/**, lib/**,
      ocp, ocp-connect.
    - NEW job port-spot hard-fails any PR that introduces a hardcoded
      "3478" or "3456" literal outside EXEMPT_REGEX (lib/constants.mjs,
      test-features.mjs, ocp/ocp-connect bash CLIs, docs, the workflow
      itself).
  * Doc-comment rewording so CI grep finds zero hits.

No behavior change for any user. CLAUDE_PROXY_PORT env var still wins
at runtime; only the unset-env fallback now flows through one constant.

ALIGNMENT.md note: server.mjs change is one import + one literal swap,
mechanical. No cli.js operation changed; the citation requirement does
not apply.

cli.js: not applicable — mechanical refactor, no behavior change.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-13 06:42:15 +10:00

99 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
// Idempotently sync OCP's claude-local provider models into OpenClaw's registry.
// Only touches:
// - config.models.providers["claude-local"].models
// - config.agents.defaults.models["claude-local/*"] keys
// All other fields and providers are preserved.
import { readFileSync, writeFileSync, existsSync, copyFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
import { DEFAULT_PORT, LOCAL_HOST, OPENAI_API_BASE } from "../lib/constants.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, "..");
const OPENCLAW_CONFIG = join(homedir(), ".openclaw", "openclaw.json");
const PROVIDER_NAME = "claude-local";
const QUIET = process.argv.includes("--quiet");
function log(msg) { if (!QUIET) console.log(` ✓ ${msg}`); }
function warn(msg) { console.warn(` ⚠ ${msg}`); }
if (!existsSync(OPENCLAW_CONFIG)) {
log(`OpenClaw not installed at ${OPENCLAW_CONFIG} — skipping (this is fine for non-OpenClaw users)`);
process.exit(0);
}
const modelsConfig = JSON.parse(readFileSync(join(REPO_ROOT, "models.json"), "utf-8"));
const desiredModels = modelsConfig.models.map(m => ({
id: m.id,
name: m.openclawName,
reasoning: m.reasoning,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: m.contextWindow,
maxTokens: m.maxTokens,
}));
const desiredAliases = Object.fromEntries(
modelsConfig.models.map(m => [`${PROVIDER_NAME}/${m.id}`, { alias: m.displayName }])
);
const config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf-8"));
// Compute diff before writing
const existingModels = config?.models?.providers?.[PROVIDER_NAME]?.models ?? [];
const existingIds = new Set(existingModels.map(m => m.id));
const desiredIds = new Set(desiredModels.map(m => m.id));
const added = [...desiredIds].filter(id => !existingIds.has(id));
const removed = [...existingIds].filter(id => !desiredIds.has(id));
if (added.length === 0 && removed.length === 0 && existingModels.length === desiredModels.length) {
// Check deep equality too in case names/maxTokens changed
const changed = desiredModels.some((d) => {
const e = existingModels.find(x => x.id === d.id);
return !e || e.name !== d.name || e.maxTokens !== d.maxTokens;
});
if (!changed) {
log("OpenClaw registry already in sync");
process.exit(0);
}
}
// Backup
const backupPath = `${OPENCLAW_CONFIG}.bak.${Date.now()}`;
copyFileSync(OPENCLAW_CONFIG, backupPath);
log(`Backed up to ${backupPath}`);
// Surgical patch: only touch claude-local provider and claude-local/* aliases
if (!config.models) config.models = {};
if (!config.models.providers) config.models.providers = {};
if (!config.models.providers[PROVIDER_NAME]) {
// First-time registration
config.models.providers[PROVIDER_NAME] = {
baseUrl: `http://${LOCAL_HOST}:${DEFAULT_PORT}${OPENAI_API_BASE}`,
api: "openai-completions",
authHeader: false,
models: desiredModels,
};
} else {
// Update only the models array; leave baseUrl/api/authHeader untouched (user may have customized port)
config.models.providers[PROVIDER_NAME].models = desiredModels;
}
if (!config.agents) config.agents = {};
if (!config.agents.defaults) config.agents.defaults = {};
if (!config.agents.defaults.models) config.agents.defaults.models = {};
// Remove stale claude-local/* aliases, then add desired ones
for (const key of Object.keys(config.agents.defaults.models)) {
if (key.startsWith(`${PROVIDER_NAME}/`)) delete config.agents.defaults.models[key];
}
Object.assign(config.agents.defaults.models, desiredAliases);
writeFileSync(OPENCLAW_CONFIG, JSON.stringify(config, null, 2) + "\n");
if (added.length > 0) log(`Added: ${added.join(", ")}`);
if (removed.length > 0) log(`Removed (no longer in models.json): ${removed.join(", ")}`);
log(`OpenClaw registry synced: ${desiredModels.length} models registered`);