Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 168 additions & 33 deletions dist/cli.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/cli.js.map

Large diffs are not rendered by default.

10 changes: 0 additions & 10 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1143,16 +1143,6 @@ type BlockRunModel = {
flatPrice?: number;
};
declare const BLOCKRUN_MODELS: BlockRunModel[];
/**
* All BlockRun models in OpenClaw format (including aliases).
* Used for proxy-side resolution (alias → target ID), tool routing, etc.
*
* Catalog entries shadowed by an identically-keyed alias are excluded:
* resolveModelAlias checks MODEL_ALIASES first, so those catalog entries are
* unreachable and their metadata (name/pricing) would misadvertise what
* callers actually get. The alias-derived entry carries the redirect
* target's real metadata instead.
*/
declare const OPENCLAW_MODELS: ModelDefinitionConfig[];
/**
* Build a ModelProviderConfig for BlockRun.
Expand Down
72 changes: 46 additions & 26 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createRequire as __cjs_createRequire } from 'node:module'; const require = __cjs_createRequire(import.meta.url);
import { createRequire as __blockrun_createRequire } from 'node:module'; const require = __blockrun_createRequire(import.meta.url);
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
Expand Down Expand Up @@ -176083,31 +176083,31 @@ var top_models_default = [
"anthropic/claude-sonnet-5",
"anthropic/claude-sonnet-4.6",
"anthropic/claude-haiku-4.5",
"openai/gpt-5.6-terra",
"openai/gpt-5.6-sol",
"openai/gpt-5.6-terra",
"openai/gpt-5.6-luna",
"openai/gpt-5.5",
"openai/gpt-5.4-pro",
"openai/gpt-5.4",
"openai/gpt-5.4-mini",
"openai/gpt-5.4-nano",
"openai/gpt-5.3-codex",
"google/gemini-3.1-pro",
"google/gemini-3.5-flash",
"google/gemini-3.1-pro",
"google/gemini-3.1-flash-lite",
"google/gemini-3-flash-preview",
"xai/grok-4.3",
"xai/grok-4-0709",
"xai/grok-4-1-fast-reasoning",
"xai/grok-3",
"xai/grok-4-0709",
"xai/grok-build-0.1",
"xai/grok-3",
"zai/glm-5.2",
"zai/glm-5.1",
"zai/glm-5-turbo",
"zai/glm-5",
"zai/glm-5-turbo",
"moonshot/kimi-k2.7",
"minimax/minimax-m3",
"minimax/minimax-m2.7",
"moonshot/kimi-k2.7",
"deepseek/deepseek-v4-pro",
"deepseek/deepseek-chat",
"deepseek/deepseek-reasoner",
Expand Down Expand Up @@ -176233,6 +176233,9 @@ var MODEL_ALIASES = {
// Google
// gemini-3-pro-preview delisted by Google 2026-06-06 — mirror the gateway
// redirect to its successor so pinned callers land on 3.1-pro, not an error.
"gemini-pro": "google/gemini-3.1-pro",
"gemini-3-pro": "google/gemini-3.1-pro",
"gemini-3.1-pro": "google/gemini-3.1-pro",
"google/gemini-3-pro-preview": "google/gemini-3.1-pro",
"gemini-3-pro-preview": "google/gemini-3.1-pro",
gemini: "google/gemini-2.5-pro",
Expand Down Expand Up @@ -177502,14 +177505,17 @@ function toOpenClawModel(m) {
maxTokens: m.maxOutput
};
}
var ALIAS_MODELS = Object.entries(MODEL_ALIASES).filter(([alias]) => !alias.includes("/")).map(([alias, targetId]) => {
const target = BLOCKRUN_MODELS.find((m) => m.id === targetId);
if (!target) return null;
return toOpenClawModel({ ...target, id: alias, name: `${alias} \u2192 ${target.name}` });
}).filter((m) => m !== null);
var ALL_OPENCLAW_MODELS = BLOCKRUN_MODELS.filter(
(m) => !(m.id.includes("/") && m.id in MODEL_ALIASES)
).map(toOpenClawModel);
var TOP_MODEL_IDS = new Set(TOP_MODELS);
var ALL_OPENCLAW_MODEL_BY_ID = new Map(ALL_OPENCLAW_MODELS.map((m) => [m.id, m]));
var OPENCLAW_MODELS = [
...BLOCKRUN_MODELS.filter((m) => !(m.id in MODEL_ALIASES)).map(toOpenClawModel),
...ALIAS_MODELS
...TOP_MODELS.flatMap((id2) => {
const model = ALL_OPENCLAW_MODEL_BY_ID.get(id2);
return model ? [model] : [];
}),
...ALL_OPENCLAW_MODELS.filter((m) => !TOP_MODEL_IDS.has(m.id))
];
var OPENCLAW_MODEL_BY_ID = new Map(OPENCLAW_MODELS.map((m) => [m.id, m]));
var VISIBLE_OPENCLAW_MODELS = TOP_MODELS.flatMap((id2) => {
Expand Down Expand Up @@ -273763,14 +273769,14 @@ function toOpenClawModel2(m) {
maxTokens: m.maxOutput
};
}
var ALIAS_MODELS2 = Object.entries(MODEL_ALIASES2).filter(([alias]) => !alias.includes("/")).map(([alias, targetId]) => {
var ALIAS_MODELS = Object.entries(MODEL_ALIASES2).filter(([alias]) => !alias.includes("/")).map(([alias, targetId]) => {
const target = BLOCKRUN_MODELS2.find((m) => m.id === targetId);
if (!target) return null;
return toOpenClawModel2({ ...target, id: alias, name: `${alias} \u2192 ${target.name}` });
}).filter((m) => m !== null);
var OPENCLAW_MODELS2 = [
...BLOCKRUN_MODELS2.filter((m) => !(m.id in MODEL_ALIASES2)).map(toOpenClawModel2),
...ALIAS_MODELS2
...ALIAS_MODELS
];
var OPENCLAW_MODEL_BY_ID2 = new Map(OPENCLAW_MODELS2.map((m) => [m.id, m]));
var VISIBLE_OPENCLAW_MODELS2 = TOP_MODELS2.flatMap((id2) => {
Expand Down Expand Up @@ -284238,12 +284244,8 @@ function injectModelsConfig(logger48, options = {}) {
fixed = true;
}
const currentModels = blockrun.models;
const currentModelIds = new Set(
Array.isArray(currentModels) ? currentModels.map((m) => m?.id).filter((id2) => typeof id2 === "string") : []
);
const expectedModelIds = VISIBLE_OPENCLAW_MODELS.map((m) => m.id);
const expectedSet = new Set(expectedModelIds);
const needsModelUpdate = !currentModels || !Array.isArray(currentModels) || currentModels.length !== VISIBLE_OPENCLAW_MODELS.length || expectedModelIds.some((id2) => !currentModelIds.has(id2)) || Array.from(currentModelIds).some((id2) => !expectedSet.has(id2));
const needsModelUpdate = !currentModels || !Array.isArray(currentModels) || currentModels.length !== VISIBLE_OPENCLAW_MODELS.length || currentModels.some((m, index2) => m?.id !== expectedModelIds[index2]);
if (needsModelUpdate) {
blockrun.models = VISIBLE_OPENCLAW_MODELS;
fixed = true;
Expand Down Expand Up @@ -284292,30 +284294,48 @@ function injectModelsConfig(logger48, options = {}) {
needsWrite = true;
logger48.info(`Removed ${removedDeprecatedCount} deprecated model entries from allowlist`);
}
const expectedBlockrunKeys = new Set(TOP_MODELS.map((id2) => `blockrun/${id2}`));
const expectedBlockrunKeys = TOP_MODELS.map((id2) => `blockrun/${id2}`);
const expectedBlockrunKeySet = new Set(expectedBlockrunKeys);
const existingNonBlockrunAllowlist = Object.fromEntries(
Object.entries(allowlist).filter(([key2]) => !key2.startsWith("blockrun/"))
);
let addedCount = 0;
let prunedCount = 0;
for (const key2 of Object.keys(allowlist)) {
if (key2.startsWith("blockrun/") && !expectedBlockrunKeys.has(key2)) {
if (key2.startsWith("blockrun/") && !expectedBlockrunKeySet.has(key2)) {
delete allowlist[key2];
prunedCount++;
}
}
for (const id2 of TOP_MODELS) {
const key2 = `blockrun/${id2}`;
const currentBlockrunKeys = Object.keys(allowlist).filter((key2) => key2.startsWith("blockrun/"));
const needsAllowlistOrderUpdate = expectedBlockrunKeys.some(
(key2, index2) => currentBlockrunKeys[index2] !== key2
);
for (const key2 of expectedBlockrunKeys) {
if (!allowlist[key2]) {
allowlist[key2] = {};
addedCount++;
}
}
if (addedCount > 0 || prunedCount > 0) {
if (addedCount > 0 || prunedCount > 0 || needsAllowlistOrderUpdate) {
for (const key2 of Object.keys(allowlist)) {
delete allowlist[key2];
}
Object.assign(
allowlist,
Object.fromEntries(expectedBlockrunKeys.map((key2) => [key2, {}])),
existingNonBlockrunAllowlist
);
needsWrite = true;
if (prunedCount > 0) {
logger48.info(`Pruned ${prunedCount} stale blockrun/* entries from allowlist`);
}
if (addedCount > 0) {
logger48.info(`Added ${addedCount} models to allowlist (${TOP_MODELS.length} total)`);
}
if (needsAllowlistOrderUpdate) {
logger48.info(`Reordered ${TOP_MODELS.length} blockrun/* allowlist entries`);
}
}
if (!config.tools || typeof config.tools !== "object" || Array.isArray(config.tools)) {
config.tools = {};
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
],
"scripts": {
"build": "tsup",
"postinstall": "node -e \"const s='./scripts/postinstall.mjs';require('fs').existsSync(s)&&import(s)\"",
"dev": "tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
Expand Down
29 changes: 29 additions & 0 deletions scripts/postinstall.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env node

import { existsSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";

if (process.env.BLOCKRUN_CLAWROUTER_SKIP_POSTINSTALL === "1") {
process.exit(0);
}

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const cliPath = resolve(root, "dist", "cli.js");

if (!existsSync(cliPath)) {
process.exit(0);
}

const result = spawnSync(process.execPath, [cliPath, "setup"], {
stdio: "inherit",
env: {
...process.env,
BLOCKRUN_CLAWROUTER_SKIP_POSTINSTALL: "1",
},
});

if (result.status && result.status !== 0) {
console.warn("[ClawRouter] OpenClaw setup did not complete; run `clawrouter setup` manually.");
}
123 changes: 119 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,17 +539,51 @@ async function cmdCache(port: number): Promise<void> {
*/
async function cmdSetup(): Promise<void> {
const { execFileSync, execSync } = await import("node:child_process");
const { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } =
await import("node:fs");
const { dirname, join } = await import("node:path");
const { homedir } = await import("node:os");
const { injectModelsConfig, injectAuthProfile } = await import("./index.js");
const { VISIBLE_OPENCLAW_MODELS } = await import("./models.js");

console.log("🦞 ClawRouter setup\n");

// Step 1: detect OpenClaw on PATH
let openclawPath: string;
try {
openclawPath = execSync("command -v openclaw", {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
const candidates: string[] = [];

try {
const pathOpenClaw = execSync("command -v openclaw", {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (pathOpenClaw) candidates.push(pathOpenClaw);
} catch {
// npm install scripts often run with a stripped PATH.
}

if (process.env.npm_config_prefix) {
candidates.push(join(process.env.npm_config_prefix, "bin", "openclaw"));
}

try {
const npmRoot = execSync("npm root -g", {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (npmRoot) candidates.push(join(dirname(dirname(npmRoot)), "bin", "openclaw"));
} catch {
// npm may be unavailable in embedded runtimes.
}

candidates.push(
join(homedir(), ".npm-global", "bin", "openclaw"),
join(homedir(), ".local", "bin", "openclaw"),
"/usr/local/bin/openclaw",
);

openclawPath = candidates.find((candidate) => existsSync(candidate)) ?? "";
Comment on lines +556 to +586

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No timeout on synchronous child-process calls in the auto-triggered postinstall chain.

scripts/postinstall.mjs runs on every npm install of this package and synchronously spawns cli.js setup, which itself shells out multiple times via execSync. None of these calls set a timeout, so a hang anywhere in this chain (corporate proxy, broken PATH lookup, stalled npm root -g) blocks the consumer's npm install indefinitely.

  • src/cli.ts#L556-L586: add a timeout (and killSignal) option to the execSync("command -v openclaw", ...) and execSync("npm root -g", ...) calls used for OpenClaw path detection.
  • scripts/postinstall.mjs#L19-L29: add a timeout option to the spawnSync call as the outermost guard so a hang inside cli.js setup can't block npm install indefinitely, and also check result.signal/result.error (not just result.status) so a killed/crashed child process isn't silently treated as success.
📍 Affects 2 files
  • src/cli.ts#L556-L586 (this comment)
  • scripts/postinstall.mjs#L19-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 556 - 586, In src/cli.ts lines 556-586, add bounded
timeout and killSignal options to both execSync calls used for OpenClaw path
detection. In scripts/postinstall.mjs lines 19-29, add a timeout to the
spawnSync call wrapping cli.js setup and handle result.signal or result.error in
addition to result.status so terminated or failed children are not treated as
successful.

if (!openclawPath) throw new Error("not found");
console.log(` ✓ Found openclaw at ${openclawPath}`);
} catch {
Expand Down Expand Up @@ -594,6 +628,87 @@ async function cmdSetup(): Promise<void> {
try {
injectModelsConfig(setupLogger, { forceWrite: true });
injectAuthProfile(setupLogger);
const modelCachePath = join(homedir(), ".openclaw", "agents", "main", "agent", "models.json");
if (existsSync(modelCachePath)) {
try {
const modelCache = JSON.parse(readFileSync(modelCachePath, "utf8"));
if (modelCache && typeof modelCache === "object" && !Array.isArray(modelCache)) {
const cacheRecord = modelCache as { providers?: Record<string, unknown> };
const existingProviders = cacheRecord.providers ?? {};
cacheRecord.providers = {
blockrun: {
...(existingProviders.blockrun &&
typeof existingProviders.blockrun === "object" &&
!Array.isArray(existingProviders.blockrun)
? existingProviders.blockrun
: {}),
models: VISIBLE_OPENCLAW_MODELS,
},
...Object.fromEntries(
Object.entries(existingProviders).filter(([provider]) => provider !== "blockrun"),
),
};
writeFileSync(modelCachePath, JSON.stringify(modelCache, null, 2));
setupLogger.info("Updated OpenClaw agent model cache for BlockRun");
}
} catch (err) {
setupLogger.info(
`Skipped OpenClaw model cache sync: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
try {
const openclawRealPath = realpathSync(openclawPath);
const openclawDist = join(dirname(openclawRealPath), "dist");
if (existsSync(openclawDist)) {
for (const file of readdirSync(openclawDist)) {
if (!file.endsWith(".js")) continue;
const filePath = join(openclawDist, file);
let source = readFileSync(filePath, "utf8");
const patchedMessages: string[] = [];
const original = source;

if (source.includes("const models = [...modelSet].toSorted")) {
source = source.replace(
"const models = [...modelSet].toSorted((left, right) => left.localeCompare(right));",
"const models = [...modelSet];",
);
if (source !== original) patchedMessages.push("Telegram model ordering");
}

if (source.includes("async function buildModelsProviderData")) {
const blockrunSeed =
'const configuredModelRefs = Object.keys(cfg.agents?.defaults?.models ?? {});\n\tfor (const raw of configuredModelRefs) {\n\t\tconst normalizedRaw = typeof raw === "string" ? raw.trim() : "";\n\t\tif (normalizedRaw.startsWith("blockrun/")) add("blockrun", normalizedRaw.slice("blockrun/".length));\n\t\telse addRawModelRef(raw);\n\t}';
source = source.replace(
/(?:\t?for \(const raw of Object\.keys\(cfg\.agents\?\.defaults\?\.models \?\? \{\}\)\) addRawModelRef\(raw\);\n)+\tfor \(const entry of visibleCatalog\) if \(normalizeProviderId\(entry\.provider\) !== "blockrun"\) add\(entry\.provider, entry\.id\);/,
`${blockrunSeed}\n\tfor (const entry of visibleCatalog) if (normalizeProviderId(entry.provider) !== "blockrun") add(entry.provider, entry.id);`,
);
source = source.replace(
"for (const entry of visibleCatalog) add(entry.provider, entry.id);",
`${blockrunSeed}\n\tfor (const entry of visibleCatalog) if (normalizeProviderId(entry.provider) !== "blockrun") add(entry.provider, entry.id);`,
);
source = source.replace(
"for (const entry of catalog) if (usesUnfilteredCatalogModels(entry.provider, cliRuntimeProviders) && await hasAuth(entry.provider)) add(entry.provider, entry.id);",
'for (const entry of catalog) if (normalizeProviderId(entry.provider) !== "blockrun" && usesUnfilteredCatalogModels(entry.provider, cliRuntimeProviders) && await hasAuth(entry.provider)) add(entry.provider, entry.id);',
);
source = source.replace(
"const providers = [...byProvider.keys()].toSorted();",
"const configuredProviderOrder = Object.keys(cfg.models?.providers ?? {}).map((provider) => normalizeProviderId(provider));\n\tconst providers = [...byProvider.keys()].sort((a, b) => {\n\t\tconst ai = configuredProviderOrder.indexOf(a);\n\t\tconst bi = configuredProviderOrder.indexOf(b);\n\t\tif (ai !== -1 || bi !== -1) return (ai === -1 ? Number.MAX_SAFE_INTEGER : ai) - (bi === -1 ? Number.MAX_SAFE_INTEGER : bi);\n\t\treturn a.localeCompare(b);\n\t});",
);
if (source !== original) patchedMessages.push("model picker ordering");
}

if (source !== original) {
writeFileSync(filePath, source);
setupLogger.info(`Patched OpenClaw ${patchedMessages.join(" + ")} (${file})`);
}
}
}
} catch (err) {
console.warn(
` ⚠ OpenClaw bundle patch skipped: ${err instanceof Error ? err.message : String(err)}`,
);
}
} catch (err) {
console.error(` ✗ Config sync failed: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
Expand Down
Loading
Loading