@blockrun/clawrouter 0.12.264 → 0.12.266

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.
@@ -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
- );