@blockrun/clawrouter 0.12.264 → 0.12.265
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +47 -5
- package/dist/index.d.ts +8 -1
- package/dist/index.js +47 -5
- package/openclaw.plugin.json +5 -5
- package/package.json +2 -1
- package/scripts/sync-brand-numbers.mjs +0 -270
package/dist/cli.js
CHANGED
|
@@ -226835,6 +226835,7 @@ var index_exports = {};
|
|
|
226835
226835
|
__export(index_exports, {
|
|
226836
226836
|
BALANCE_THRESHOLDS: () => BALANCE_THRESHOLDS,
|
|
226837
226837
|
BLOCKRUN_MODELS: () => BLOCKRUN_MODELS,
|
|
226838
|
+
BLOCKRUN_PLUGIN_ID: () => BLOCKRUN_PLUGIN_ID,
|
|
226838
226839
|
BalanceMonitor: () => BalanceMonitor,
|
|
226839
226840
|
CAIP2_BASE: () => CAIP2_BASE,
|
|
226840
226841
|
CAIP2_SOLANA_MAINNET: () => CAIP2_SOLANA_MAINNET,
|
|
@@ -227149,6 +227150,35 @@ function injectModelsConfig(logger48, options = {}) {
|
|
|
227149
227150
|
logger48.info(`Added ${addedCount} models to allowlist (${TOP_MODELS.length} total)`);
|
|
227150
227151
|
}
|
|
227151
227152
|
}
|
|
227153
|
+
const pluginEntries = config.plugins?.entries;
|
|
227154
|
+
if (pluginEntries && pluginEntries.clawrouter && !pluginEntries[BLOCKRUN_PLUGIN_ID]) {
|
|
227155
|
+
pluginEntries[BLOCKRUN_PLUGIN_ID] = pluginEntries.clawrouter;
|
|
227156
|
+
delete pluginEntries.clawrouter;
|
|
227157
|
+
needsWrite = true;
|
|
227158
|
+
logger48.info(
|
|
227159
|
+
"Migrated plugins.entries.clawrouter -> blockrun-clawrouter (OpenClaw bundles its own `clawrouter` plugin; see #305)"
|
|
227160
|
+
);
|
|
227161
|
+
}
|
|
227162
|
+
const pluginsCfg = config.plugins;
|
|
227163
|
+
const allow = pluginsCfg?.allow;
|
|
227164
|
+
if (Array.isArray(allow) && allow.includes("clawrouter") && !allow.includes(BLOCKRUN_PLUGIN_ID)) {
|
|
227165
|
+
allow.push(BLOCKRUN_PLUGIN_ID);
|
|
227166
|
+
needsWrite = true;
|
|
227167
|
+
logger48.info(`Added ${BLOCKRUN_PLUGIN_ID} to plugins.allow (renamed from clawrouter; see #305)`);
|
|
227168
|
+
}
|
|
227169
|
+
const deny = pluginsCfg?.deny;
|
|
227170
|
+
if (Array.isArray(deny) && deny.includes("clawrouter") && !deny.includes(BLOCKRUN_PLUGIN_ID)) {
|
|
227171
|
+
deny.push(BLOCKRUN_PLUGIN_ID);
|
|
227172
|
+
needsWrite = true;
|
|
227173
|
+
logger48.info(`Added ${BLOCKRUN_PLUGIN_ID} to plugins.deny (renamed from clawrouter; see #305)`);
|
|
227174
|
+
}
|
|
227175
|
+
const installs = pluginsCfg?.installs;
|
|
227176
|
+
if (installs && installs.clawrouter && !installs[BLOCKRUN_PLUGIN_ID]) {
|
|
227177
|
+
installs[BLOCKRUN_PLUGIN_ID] = installs.clawrouter;
|
|
227178
|
+
delete installs.clawrouter;
|
|
227179
|
+
needsWrite = true;
|
|
227180
|
+
logger48.info(`Migrated plugins.installs.clawrouter -> ${BLOCKRUN_PLUGIN_ID} (see #305)`);
|
|
227181
|
+
}
|
|
227152
227182
|
if (!config.tools || typeof config.tools !== "object" || Array.isArray(config.tools)) {
|
|
227153
227183
|
config.tools = {};
|
|
227154
227184
|
needsWrite = true;
|
|
@@ -228141,7 +228171,7 @@ Run \`openclaw plugins install @blockrun/clawrouter\` to generate a wallet.`,
|
|
|
228141
228171
|
}
|
|
228142
228172
|
};
|
|
228143
228173
|
}
|
|
228144
|
-
var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
|
|
228174
|
+
var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, BLOCKRUN_PLUGIN_ID, plugin, index_default2;
|
|
228145
228175
|
var init_index = __esm({
|
|
228146
228176
|
"src/index.ts"() {
|
|
228147
228177
|
"use strict";
|
|
@@ -228184,9 +228214,16 @@ var init_index = __esm({
|
|
|
228184
228214
|
IMAGE_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "images");
|
|
228185
228215
|
AUDIO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "audio");
|
|
228186
228216
|
VIDEO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "videos");
|
|
228217
|
+
BLOCKRUN_PLUGIN_ID = "blockrun-clawrouter";
|
|
228187
228218
|
plugin = {
|
|
228188
|
-
|
|
228189
|
-
|
|
228219
|
+
// NOT "clawrouter". OpenClaw bundles its own plugin under that id since the
|
|
228220
|
+
// 2026.7.1 line (vendored at dist/extensions/clawrouter, provider `clawrouter/*`,
|
|
228221
|
+
// CLAWROUTER_API_KEY, clawrouter.openclaw.ai). A duplicate id loses to the
|
|
228222
|
+
// bundled one — "global plugin will be overridden by bundled plugin" — so this
|
|
228223
|
+
// plugin silently never loaded and the local proxy never started. Different
|
|
228224
|
+
// product, different id. See #305.
|
|
228225
|
+
id: BLOCKRUN_PLUGIN_ID,
|
|
228226
|
+
name: "BlockRun ClawRouter",
|
|
228190
228227
|
description: "Smart LLM router \u2014 55+ models, x402 micropayments, 78% cost savings",
|
|
228191
228228
|
version: VERSION,
|
|
228192
228229
|
register(api) {
|
|
@@ -228595,13 +228632,18 @@ ${errText}`
|
|
|
228595
228632
|
delete config.models.providers.blockrun;
|
|
228596
228633
|
}
|
|
228597
228634
|
removeManagedBlockrunMcpServerConfig(config);
|
|
228598
|
-
for (const key2 of [
|
|
228635
|
+
for (const key2 of [
|
|
228636
|
+
BLOCKRUN_PLUGIN_ID,
|
|
228637
|
+
"clawrouter",
|
|
228638
|
+
"ClawRouter",
|
|
228639
|
+
"@blockrun/clawrouter"
|
|
228640
|
+
]) {
|
|
228599
228641
|
if (config.plugins?.entries?.[key2]) delete config.plugins.entries[key2];
|
|
228600
228642
|
if (config.plugins?.installs?.[key2]) delete config.plugins.installs[key2];
|
|
228601
228643
|
}
|
|
228602
228644
|
if (Array.isArray(config.plugins?.allow)) {
|
|
228603
228645
|
config.plugins.allow = config.plugins.allow.filter(
|
|
228604
|
-
(p) => p !== "clawrouter" && p !== "ClawRouter" && p !== "@blockrun/clawrouter"
|
|
228646
|
+
(p) => p !== BLOCKRUN_PLUGIN_ID && p !== "clawrouter" && p !== "ClawRouter" && p !== "@blockrun/clawrouter"
|
|
228605
228647
|
);
|
|
228606
228648
|
}
|
|
228607
228649
|
if (config.agents?.defaults?.models) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1729,6 +1729,13 @@ declare function parseCallArgs(raw: string): {
|
|
|
1729
1729
|
* Delegates to the local proxy (which handles x402 payment).
|
|
1730
1730
|
*/
|
|
1731
1731
|
declare function buildImageGenerationProvider(): ImageGenerationProviderPlugin;
|
|
1732
|
+
/**
|
|
1733
|
+
* This plugin's OpenClaw id. NOT "clawrouter" — OpenClaw bundles its own plugin
|
|
1734
|
+
* under that id since the 2026.7.1 line, and a duplicate loses to the bundled
|
|
1735
|
+
* one. Kept as one constant so the manifest, the plugin definition and the
|
|
1736
|
+
* config migration can never drift apart. See #305.
|
|
1737
|
+
*/
|
|
1738
|
+
declare const BLOCKRUN_PLUGIN_ID = "blockrun-clawrouter";
|
|
1732
1739
|
declare const plugin: OpenClawPluginDefinition;
|
|
1733
1740
|
|
|
1734
|
-
export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, CAIP2_BASE, CAIP2_SOLANA_MAINNET, type CachedLLMResponse, type CachedResponse, type CheckResult, type CounterpartyInfo, DEFAULT_RETRY_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, type DerivedKeys, EmptyWalletError, FileSpendControlStorage, InMemorySpendControlStorage, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, MalformedSpendPolicyError, OPENCLAW_MODELS, PARTNER_SERVICES, type PartnerServiceDefinition, type PartnerToolDefinition, type PaymentChain, type PolicyList, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SolanaBalanceInfo, SolanaBalanceMonitor, SpendControl, type SpendControlOptions, type SpendControlStorage, type SpendLimits, SpendPolicyError, type SpendRecord, type SpendWindow, type SpendingStatus, type SufficiencyResult, type UsageEntry, VISIBLE_OPENCLAW_MODELS, type WalletConfig, type WalletResolution, blockrunProvider, buildImageGenerationProvider, buildPartnerTools, buildProviderModels, clearStats, plugin as default, deriveAllKeys, deriveEvmKey, deriveSolanaKeyBytes, fetchWithRetry, formatDuration, formatStatsAscii, generateWalletMnemonic, getAgenticModels, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, injectAuthProfile, injectModelsConfig, isAgenticModel, isBalanceError, isBlockrunWebSearchDisabled, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, isValidMnemonic, loadPaymentChain, logUsage, parseCallArgs, registerSpendPolicyHook, resolveModelAlias, resolvePaymentChain, savePaymentChain, setupSolana, startProxy, syncAgentModelCache };
|
|
1741
|
+
export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, BLOCKRUN_PLUGIN_ID, type BalanceInfo, BalanceMonitor, CAIP2_BASE, CAIP2_SOLANA_MAINNET, type CachedLLMResponse, type CachedResponse, type CheckResult, type CounterpartyInfo, DEFAULT_RETRY_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, type DerivedKeys, EmptyWalletError, FileSpendControlStorage, InMemorySpendControlStorage, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, MalformedSpendPolicyError, OPENCLAW_MODELS, PARTNER_SERVICES, type PartnerServiceDefinition, type PartnerToolDefinition, type PaymentChain, type PolicyList, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SolanaBalanceInfo, SolanaBalanceMonitor, SpendControl, type SpendControlOptions, type SpendControlStorage, type SpendLimits, SpendPolicyError, type SpendRecord, type SpendWindow, type SpendingStatus, type SufficiencyResult, type UsageEntry, VISIBLE_OPENCLAW_MODELS, type WalletConfig, type WalletResolution, blockrunProvider, buildImageGenerationProvider, buildPartnerTools, buildProviderModels, clearStats, plugin as default, deriveAllKeys, deriveEvmKey, deriveSolanaKeyBytes, fetchWithRetry, formatDuration, formatStatsAscii, generateWalletMnemonic, getAgenticModels, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, injectAuthProfile, injectModelsConfig, isAgenticModel, isBalanceError, isBlockrunWebSearchDisabled, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, isValidMnemonic, loadPaymentChain, logUsage, parseCallArgs, registerSpendPolicyHook, resolveModelAlias, resolvePaymentChain, savePaymentChain, setupSolana, startProxy, syncAgentModelCache };
|
package/dist/index.js
CHANGED
|
@@ -226895,6 +226895,35 @@ function injectModelsConfig(logger48, options = {}) {
|
|
|
226895
226895
|
logger48.info(`Added ${addedCount} models to allowlist (${TOP_MODELS.length} total)`);
|
|
226896
226896
|
}
|
|
226897
226897
|
}
|
|
226898
|
+
const pluginEntries = config.plugins?.entries;
|
|
226899
|
+
if (pluginEntries && pluginEntries.clawrouter && !pluginEntries[BLOCKRUN_PLUGIN_ID]) {
|
|
226900
|
+
pluginEntries[BLOCKRUN_PLUGIN_ID] = pluginEntries.clawrouter;
|
|
226901
|
+
delete pluginEntries.clawrouter;
|
|
226902
|
+
needsWrite = true;
|
|
226903
|
+
logger48.info(
|
|
226904
|
+
"Migrated plugins.entries.clawrouter -> blockrun-clawrouter (OpenClaw bundles its own `clawrouter` plugin; see #305)"
|
|
226905
|
+
);
|
|
226906
|
+
}
|
|
226907
|
+
const pluginsCfg = config.plugins;
|
|
226908
|
+
const allow = pluginsCfg?.allow;
|
|
226909
|
+
if (Array.isArray(allow) && allow.includes("clawrouter") && !allow.includes(BLOCKRUN_PLUGIN_ID)) {
|
|
226910
|
+
allow.push(BLOCKRUN_PLUGIN_ID);
|
|
226911
|
+
needsWrite = true;
|
|
226912
|
+
logger48.info(`Added ${BLOCKRUN_PLUGIN_ID} to plugins.allow (renamed from clawrouter; see #305)`);
|
|
226913
|
+
}
|
|
226914
|
+
const deny = pluginsCfg?.deny;
|
|
226915
|
+
if (Array.isArray(deny) && deny.includes("clawrouter") && !deny.includes(BLOCKRUN_PLUGIN_ID)) {
|
|
226916
|
+
deny.push(BLOCKRUN_PLUGIN_ID);
|
|
226917
|
+
needsWrite = true;
|
|
226918
|
+
logger48.info(`Added ${BLOCKRUN_PLUGIN_ID} to plugins.deny (renamed from clawrouter; see #305)`);
|
|
226919
|
+
}
|
|
226920
|
+
const installs = pluginsCfg?.installs;
|
|
226921
|
+
if (installs && installs.clawrouter && !installs[BLOCKRUN_PLUGIN_ID]) {
|
|
226922
|
+
installs[BLOCKRUN_PLUGIN_ID] = installs.clawrouter;
|
|
226923
|
+
delete installs.clawrouter;
|
|
226924
|
+
needsWrite = true;
|
|
226925
|
+
logger48.info(`Migrated plugins.installs.clawrouter -> ${BLOCKRUN_PLUGIN_ID} (see #305)`);
|
|
226926
|
+
}
|
|
226898
226927
|
if (!config.tools || typeof config.tools !== "object" || Array.isArray(config.tools)) {
|
|
226899
226928
|
config.tools = {};
|
|
226900
226929
|
needsWrite = true;
|
|
@@ -227887,7 +227916,7 @@ Run \`openclaw plugins install @blockrun/clawrouter\` to generate a wallet.`,
|
|
|
227887
227916
|
}
|
|
227888
227917
|
};
|
|
227889
227918
|
}
|
|
227890
|
-
var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
|
|
227919
|
+
var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, BLOCKRUN_PLUGIN_ID, plugin, index_default2;
|
|
227891
227920
|
var init_index = __esm({
|
|
227892
227921
|
"src/index.ts"() {
|
|
227893
227922
|
init_provider();
|
|
@@ -227929,9 +227958,16 @@ var init_index = __esm({
|
|
|
227929
227958
|
IMAGE_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "images");
|
|
227930
227959
|
AUDIO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "audio");
|
|
227931
227960
|
VIDEO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "videos");
|
|
227961
|
+
BLOCKRUN_PLUGIN_ID = "blockrun-clawrouter";
|
|
227932
227962
|
plugin = {
|
|
227933
|
-
|
|
227934
|
-
|
|
227963
|
+
// NOT "clawrouter". OpenClaw bundles its own plugin under that id since the
|
|
227964
|
+
// 2026.7.1 line (vendored at dist/extensions/clawrouter, provider `clawrouter/*`,
|
|
227965
|
+
// CLAWROUTER_API_KEY, clawrouter.openclaw.ai). A duplicate id loses to the
|
|
227966
|
+
// bundled one — "global plugin will be overridden by bundled plugin" — so this
|
|
227967
|
+
// plugin silently never loaded and the local proxy never started. Different
|
|
227968
|
+
// product, different id. See #305.
|
|
227969
|
+
id: BLOCKRUN_PLUGIN_ID,
|
|
227970
|
+
name: "BlockRun ClawRouter",
|
|
227935
227971
|
description: "Smart LLM router \u2014 55+ models, x402 micropayments, 78% cost savings",
|
|
227936
227972
|
version: VERSION,
|
|
227937
227973
|
register(api) {
|
|
@@ -228340,13 +228376,18 @@ ${errText}`
|
|
|
228340
228376
|
delete config.models.providers.blockrun;
|
|
228341
228377
|
}
|
|
228342
228378
|
removeManagedBlockrunMcpServerConfig(config);
|
|
228343
|
-
for (const key2 of [
|
|
228379
|
+
for (const key2 of [
|
|
228380
|
+
BLOCKRUN_PLUGIN_ID,
|
|
228381
|
+
"clawrouter",
|
|
228382
|
+
"ClawRouter",
|
|
228383
|
+
"@blockrun/clawrouter"
|
|
228384
|
+
]) {
|
|
228344
228385
|
if (config.plugins?.entries?.[key2]) delete config.plugins.entries[key2];
|
|
228345
228386
|
if (config.plugins?.installs?.[key2]) delete config.plugins.installs[key2];
|
|
228346
228387
|
}
|
|
228347
228388
|
if (Array.isArray(config.plugins?.allow)) {
|
|
228348
228389
|
config.plugins.allow = config.plugins.allow.filter(
|
|
228349
|
-
(p) => p !== "clawrouter" && p !== "ClawRouter" && p !== "@blockrun/clawrouter"
|
|
228390
|
+
(p) => p !== BLOCKRUN_PLUGIN_ID && p !== "clawrouter" && p !== "ClawRouter" && p !== "@blockrun/clawrouter"
|
|
228350
228391
|
);
|
|
228351
228392
|
}
|
|
228352
228393
|
if (config.agents?.defaults?.models) {
|
|
@@ -228397,6 +228438,7 @@ init_index();
|
|
|
228397
228438
|
export {
|
|
228398
228439
|
BALANCE_THRESHOLDS,
|
|
228399
228440
|
BLOCKRUN_MODELS,
|
|
228441
|
+
BLOCKRUN_PLUGIN_ID,
|
|
228400
228442
|
BalanceMonitor,
|
|
228401
228443
|
CAIP2_BASE,
|
|
228402
228444
|
CAIP2_SOLANA_MAINNET,
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "clawrouter",
|
|
3
|
-
"name": "ClawRouter",
|
|
4
|
-
"description": "Smart LLM router
|
|
2
|
+
"id": "blockrun-clawrouter",
|
|
3
|
+
"name": "BlockRun ClawRouter",
|
|
4
|
+
"description": "Smart LLM router \u2014 55+ models, x402 micropayments, 92% cost savings. Phone lookup + AI voice calls (Twilio + Bland.ai). Surf crypto-data API (84 endpoints, on-chain SQL, 100M+ labeled wallets) via skill.",
|
|
5
5
|
"skills": ["./skills"],
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"properties": {
|
|
45
45
|
"walletKey": {
|
|
46
46
|
"type": "string",
|
|
47
|
-
"description": "EVM wallet private key (0x...). Optional
|
|
47
|
+
"description": "EVM wallet private key (0x...). Optional \u2014 auto-generated if not set."
|
|
48
48
|
},
|
|
49
49
|
"routing": {
|
|
50
50
|
"type": "object",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"walletKey": {
|
|
57
57
|
"label": "Wallet Private Key",
|
|
58
58
|
"sensitive": true,
|
|
59
|
-
"placeholder": "0x... (optional
|
|
59
|
+
"placeholder": "0x... (optional \u2014 auto-generated)"
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/clawrouter",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.265",
|
|
4
4
|
"description": "Smart LLM router \u2014 save 84% on inference costs. 76 models (7 free), one wallet, x402 micropayments.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"docs/*.png",
|
|
35
35
|
"scripts",
|
|
36
36
|
"!scripts/smoke-dist.mjs",
|
|
37
|
+
"!scripts/sync-brand-numbers.mjs",
|
|
37
38
|
"skills",
|
|
38
39
|
"src/top-models.json",
|
|
39
40
|
"openclaw.plugin.json"
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Sync marketing numbers from BlockRun's canonical brand artifact.
|
|
4
|
-
*
|
|
5
|
-
* This file is copied byte-for-byte into every public repo as
|
|
6
|
-
* scripts/sync-brand-numbers.mjs. It is a copy rather than an npm package on
|
|
7
|
-
* purpose: a package would mean 37 dependency bumps, and several consuming
|
|
8
|
-
* repos have no package.json at all. Zero dependencies, plain Node.
|
|
9
|
-
*
|
|
10
|
-
* node scripts/sync-brand-numbers.mjs rewrite markers in place
|
|
11
|
-
* node scripts/sync-brand-numbers.mjs --check exit 1 on drift, write nothing
|
|
12
|
-
* node scripts/sync-brand-numbers.mjs --refresh re-fetch the artifact first
|
|
13
|
-
*
|
|
14
|
-
* --check NEVER touches the network. PR CI must be deterministic and offline:
|
|
15
|
-
* if it fetched, a deploy in progress would fail every repo in the org at once.
|
|
16
|
-
* Freshness is the fan-out job's problem, not the pull request's.
|
|
17
|
-
*
|
|
18
|
-
* Markers look like: <!-- br:models.chatVisible -->66<!-- /br:models.chatVisible -->
|
|
19
|
-
* and wrap the WHOLE token, so a badge URL, its alt text and the prose number
|
|
20
|
-
* can all regenerate from one key.
|
|
21
|
-
*/
|
|
22
|
-
import { existsSync, lstatSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
23
|
-
import { join, relative, extname } from "node:path";
|
|
24
|
-
|
|
25
|
-
const ROOT = process.cwd();
|
|
26
|
-
const SNAPSHOT = join(ROOT, "brand-numbers.json");
|
|
27
|
-
// ORIGIN is tried first because it IS the truth — the mirror can only ever be
|
|
28
|
-
// as fresh as the last time someone refreshed it. The mirror exists so a repo
|
|
29
|
-
// can still sync while blockrun.ai is down, not to front the origin.
|
|
30
|
-
//
|
|
31
|
-
// The mirror is awesome-blockrun's own brand-numbers.json: that repo consumes
|
|
32
|
-
// the artifact like every other, and its snapshot doubles as the org's copy.
|
|
33
|
-
// One file, one role per repo, nothing to keep in step by hand.
|
|
34
|
-
const ORIGIN = "https://blockrun.ai/brand/numbers.json";
|
|
35
|
-
const MIRROR =
|
|
36
|
-
"https://raw.githubusercontent.com/BlockRunAI/awesome-blockrun/main/brand-numbers.json";
|
|
37
|
-
|
|
38
|
-
const argv = new Set(process.argv.slice(2));
|
|
39
|
-
const check = argv.has("--check");
|
|
40
|
-
const refresh = argv.has("--refresh");
|
|
41
|
-
|
|
42
|
-
const SKIP_DIRS = new Set([
|
|
43
|
-
"node_modules", ".git", "dist", "build", "out", ".next", "coverage",
|
|
44
|
-
"vendor", "target", "__pycache__", ".venv", "venv",
|
|
45
|
-
]);
|
|
46
|
-
// .txt is here for llms.txt, which is a first-class marketing surface: it is
|
|
47
|
-
// what agents read to find out what BlockRun serves. Scanning other .txt files
|
|
48
|
-
// costs a read and changes nothing — only files with markers are ever written.
|
|
49
|
-
const TEXT_EXT = new Set([".md", ".mdx", ".txt"]);
|
|
50
|
-
|
|
51
|
-
/* ── 1. numbers ──────────────────────────────────────────────────────────── */
|
|
52
|
-
|
|
53
|
-
async function loadNumbers() {
|
|
54
|
-
if (!refresh) {
|
|
55
|
-
try {
|
|
56
|
-
return JSON.parse(readFileSync(SNAPSHOT, "utf8"));
|
|
57
|
-
} catch {
|
|
58
|
-
fail(
|
|
59
|
-
`no brand-numbers.json in ${ROOT}\n` +
|
|
60
|
-
` run with --refresh once to seed it from ${ORIGIN}`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
for (const url of [ORIGIN, MIRROR]) {
|
|
65
|
-
try {
|
|
66
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
|
67
|
-
if (!res.ok) continue;
|
|
68
|
-
const json = await res.json();
|
|
69
|
-
writeFileSync(SNAPSHOT, `${JSON.stringify(json, null, 2)}\n`);
|
|
70
|
-
return json;
|
|
71
|
-
} catch {
|
|
72
|
-
/* try the next source */
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
fail(`could not refresh from ${MIRROR} or ${ORIGIN}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Flatten nested numbers into dotted keys, ignoring $comment / rationale prose. */
|
|
79
|
-
function flatten(obj, prefix = "") {
|
|
80
|
-
return Object.entries(obj).flatMap(([k, v]) => {
|
|
81
|
-
if (k.startsWith("$")) return [];
|
|
82
|
-
const key = `${prefix}${k}`;
|
|
83
|
-
if (v && typeof v === "object" && !Array.isArray(v)) return flatten(v, `${key}.`);
|
|
84
|
-
if (v === null) return [];
|
|
85
|
-
return [[key, v]];
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/* ── 2. renderers ────────────────────────────────────────────────────────── */
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* How a key becomes text. Default is the bare value.
|
|
93
|
-
*
|
|
94
|
-
* A marker may carry an `@modifier` — `<!-- br:mcp.tools@badge -->` — which
|
|
95
|
-
* selects a renderer without changing which number is looked up. The modifier
|
|
96
|
-
* is what makes a key reusable: the same mcp.tools appears as a shields badge
|
|
97
|
-
* at the top of a README and as a bare "19 tools" in a table two screens down,
|
|
98
|
-
* and one marker still keeps the badge URL, its alt text and the label in step.
|
|
99
|
-
*
|
|
100
|
-
* Renderers are registered under the FULL marker name so a badge's label is
|
|
101
|
-
* written out rather than guessed from the key.
|
|
102
|
-
*/
|
|
103
|
-
const badge = (label) => (n) =>
|
|
104
|
-
`<img src="https://img.shields.io/badge/${label}-${n}-5B9BF6?style=flat-square&labelColor=0B0A0F" alt="${n} ${label}">`;
|
|
105
|
-
|
|
106
|
-
const RENDER = {
|
|
107
|
-
"mcp.tools@badge": badge("tools"),
|
|
108
|
-
"models.totalVisible@badge": badge("models"),
|
|
109
|
-
"models.chatVisible@badge": badge("models"),
|
|
110
|
-
};
|
|
111
|
-
const render = (marker, value) => (RENDER[marker] ?? String)(value);
|
|
112
|
-
|
|
113
|
-
/** `mcp.tools@badge` looks up `mcp.tools`. Unmodified markers are unaffected. */
|
|
114
|
-
const keyOf = (marker) => marker.split("@")[0];
|
|
115
|
-
|
|
116
|
-
/* ── 3. marker rewriting ─────────────────────────────────────────────────── */
|
|
117
|
-
|
|
118
|
-
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
119
|
-
const OPEN_ANY = /<!--\s*br:([A-Za-z0-9_.@]+)\s*-->/g;
|
|
120
|
-
const CLOSE_ANY = /<!--\s*\/br:([A-Za-z0-9_.@]+)\s*-->/g;
|
|
121
|
-
|
|
122
|
-
/** Byte ranges of fenced code blocks — markers inside them are documentation. */
|
|
123
|
-
function fencedRanges(text) {
|
|
124
|
-
const ranges = [];
|
|
125
|
-
const fence = /^(\s*)(`{3,}|~{3,})[^\n]*$/gm;
|
|
126
|
-
let open = null;
|
|
127
|
-
for (let m; (m = fence.exec(text)); ) {
|
|
128
|
-
if (open === null) open = m.index;
|
|
129
|
-
else {
|
|
130
|
-
ranges.push([open, m.index + m[0].length]);
|
|
131
|
-
open = null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return ranges;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function syncFile(file, numbers, problems) {
|
|
138
|
-
const before = readFileSync(file, "utf8");
|
|
139
|
-
const rel = relative(ROOT, file);
|
|
140
|
-
const fenced = fencedRanges(before);
|
|
141
|
-
const inFence = (i) => fenced.some(([a, b]) => i >= a && i < b);
|
|
142
|
-
const known = new Map(numbers);
|
|
143
|
-
const used = new Set();
|
|
144
|
-
|
|
145
|
-
// Markers actually present, so a file is only ever rewritten for what it uses
|
|
146
|
-
// and an @modifier is carried through to the renderer verbatim.
|
|
147
|
-
const markers = new Set();
|
|
148
|
-
// A marker naming a key that does not exist is an error, never a silent
|
|
149
|
-
// no-op: a typo'd marker would otherwise sit there looking synced forever.
|
|
150
|
-
for (const [re, shown] of [
|
|
151
|
-
[OPEN_ANY, (n) => `<!-- br:${n} -->`],
|
|
152
|
-
[CLOSE_ANY, (n) => `<!-- /br:${n} -->`],
|
|
153
|
-
]) {
|
|
154
|
-
for (const m of before.matchAll(re)) {
|
|
155
|
-
if (inFence(m.index)) continue;
|
|
156
|
-
markers.add(m[1]);
|
|
157
|
-
if (!known.has(keyOf(m[1]))) problems.push(`${rel}: unknown key ${shown(m[1])}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let after = before;
|
|
162
|
-
for (const marker of markers) {
|
|
163
|
-
const key = keyOf(marker);
|
|
164
|
-
if (!known.has(key)) continue;
|
|
165
|
-
const value = known.get(key);
|
|
166
|
-
const pair = new RegExp(
|
|
167
|
-
`(<!--\\s*br:${esc(marker)}\\s*-->)([\\s\\S]*?)(<!--\\s*/br:${esc(marker)}\\s*-->)`,
|
|
168
|
-
"g",
|
|
169
|
-
);
|
|
170
|
-
after = after.replace(pair, (whole, open, inner, close, offset) => {
|
|
171
|
-
if (inFence(offset)) return whole;
|
|
172
|
-
// Nesting means the closing tag of an inner marker would be consumed by
|
|
173
|
-
// the outer one. Refuse rather than produce mangled output.
|
|
174
|
-
if (/<!--\s*\/?br:/.test(inner)) {
|
|
175
|
-
problems.push(`${rel}: nested marker inside br:${marker}`);
|
|
176
|
-
return whole;
|
|
177
|
-
}
|
|
178
|
-
used.add(marker);
|
|
179
|
-
return open + render(marker, value) + close;
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// An opening tag with no partner silently swallows the rest of the file on
|
|
183
|
-
// a naive regex, so catch it explicitly.
|
|
184
|
-
const opens = [...before.matchAll(new RegExp(`<!--\\s*br:${esc(marker)}\\s*-->`, "g"))]
|
|
185
|
-
.filter((m) => !inFence(m.index)).length;
|
|
186
|
-
const closes = [...before.matchAll(new RegExp(`<!--\\s*/br:${esc(marker)}\\s*-->`, "g"))]
|
|
187
|
-
.filter((m) => !inFence(m.index)).length;
|
|
188
|
-
if (opens !== closes) problems.push(`${rel}: unbalanced marker br:${marker} (${opens} open, ${closes} close)`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return { before, after, changed: before !== after, used };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/* ── 4. walk ─────────────────────────────────────────────────────────────── */
|
|
195
|
-
|
|
196
|
-
function* walk(dir) {
|
|
197
|
-
for (const name of readdirSync(dir)) {
|
|
198
|
-
if (SKIP_DIRS.has(name)) continue;
|
|
199
|
-
const p = join(dir, name);
|
|
200
|
-
// lstat, not stat: a symlinked directory is reached by its real path or not
|
|
201
|
-
// at all. blockrun's docs/ -> awesome-blockrun/docs is exactly the case that
|
|
202
|
-
// matters — following it would edit a submodule's files behind the skip
|
|
203
|
-
// below, and a link pointing at an ancestor would recurse forever.
|
|
204
|
-
const s = lstatSync(p);
|
|
205
|
-
if (s.isSymbolicLink()) continue;
|
|
206
|
-
if (s.isDirectory()) {
|
|
207
|
-
// A nested repo is a submodule or vendored checkout: it carries its own
|
|
208
|
-
// brand-numbers.json and syncs itself. Rewriting its markers from THIS
|
|
209
|
-
// repo's snapshot would dirty a submodule nobody asked us to touch, and
|
|
210
|
-
// would report drift that belongs to another repo's CI.
|
|
211
|
-
if (existsSync(join(p, ".git"))) continue;
|
|
212
|
-
yield* walk(p);
|
|
213
|
-
} else if (TEXT_EXT.has(extname(name))) yield p;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function fail(msg) {
|
|
218
|
-
console.error(`brand-numbers: ${msg}`);
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/* ── 5. run ──────────────────────────────────────────────────────────────── */
|
|
223
|
-
|
|
224
|
-
const raw = await loadNumbers();
|
|
225
|
-
const numbers = flatten(raw);
|
|
226
|
-
const problems = [];
|
|
227
|
-
const drifted = [];
|
|
228
|
-
const everUsed = new Set();
|
|
229
|
-
|
|
230
|
-
for (const file of walk(ROOT)) {
|
|
231
|
-
const { before, after, changed, used } = syncFile(file, numbers, problems);
|
|
232
|
-
used.forEach((k) => everUsed.add(k));
|
|
233
|
-
if (!changed) continue;
|
|
234
|
-
drifted.push({ file: relative(ROOT, file), before, after });
|
|
235
|
-
if (!check) writeFileSync(file, after);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (problems.length) {
|
|
239
|
-
for (const p of problems) console.error(` ${p}`);
|
|
240
|
-
fail(`${problems.length} marker problem(s)`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (check) {
|
|
244
|
-
if (drifted.length === 0) {
|
|
245
|
-
console.log(`brand-numbers: up to date (${everUsed.size} keys in use)`);
|
|
246
|
-
process.exit(0);
|
|
247
|
-
}
|
|
248
|
-
console.error("brand-numbers: these files disagree with brand-numbers.json\n");
|
|
249
|
-
for (const { file, before, after } of drifted) {
|
|
250
|
-
const b = before.split("\n");
|
|
251
|
-
const a = after.split("\n");
|
|
252
|
-
for (let i = 0; i < Math.max(b.length, a.length); i++) {
|
|
253
|
-
if (b[i] !== a[i]) {
|
|
254
|
-
console.error(` ${file}:${i + 1}`);
|
|
255
|
-
console.error(` - ${(b[i] ?? "").trim()}`);
|
|
256
|
-
console.error(` + ${(a[i] ?? "").trim()}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
console.error(
|
|
261
|
-
"\n fix with: node scripts/sync-brand-numbers.mjs && git commit -am 'chore: sync brand numbers'",
|
|
262
|
-
);
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
console.log(
|
|
267
|
-
drifted.length
|
|
268
|
-
? `brand-numbers: updated ${drifted.length} file(s)`
|
|
269
|
-
: `brand-numbers: already up to date (${everUsed.size} keys in use)`,
|
|
270
|
-
);
|