ruvnet-brain 3.9.84-dev → 3.9.129-dev
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/README.md +17 -15
- package/bin/install.mjs +698 -70
- package/kb/brain-profile.mjs +145 -0
- package/kb/model-requirements.mjs +72 -0
- package/kb/zip-extract.mjs +297 -0
- package/package.json +32 -4
- package/plugin/mcp/managed-cli-interface.mjs +236 -0
- package/plugin/mcp/server.mjs +133 -32
- package/plugin/scripts/codex-hook-wrapper.mjs +37 -0
- package/scripts/dual-host-deliberation.mjs +284 -0
- package/scripts/dual-host-suggest.mjs +58 -0
- package/scripts/hook-registry.mjs +567 -0
- package/scripts/install-scope.mjs +708 -0
- package/scripts/model-router-outcome.mjs +19 -7
- package/scripts/selfcheck.mjs +646 -0
- package/scripts/subscription-hosts.mjs +105 -0
- package/scripts/upgrade-notice.mjs +465 -0
- package/scripts/user-settings.mjs +640 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// brain-profile.mjs — enforce the user's installed knowledge profile by artifact family.
|
|
2
|
+
//
|
|
3
|
+
// Each repository is already one independent RVF + sidecar family. "RuVector only" therefore means
|
|
4
|
+
// keeping the shared reader plus the ruvector family, not rebuilding or copying a second brain.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
export const PROFILE_COMPLETE = 'complete';
|
|
11
|
+
export const PROFILE_RUVECTOR = 'ruvector';
|
|
12
|
+
export const BRAIN_PROFILES = Object.freeze([PROFILE_COMPLETE, PROFILE_RUVECTOR]);
|
|
13
|
+
|
|
14
|
+
export function settingsPath(env = process.env) {
|
|
15
|
+
return env.RUVNET_SETTINGS_FILE
|
|
16
|
+
|| path.join(os.homedir(), '.config', 'ruvnet-brain', 'settings.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readBrainProfile({ env = process.env } = {}) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(settingsPath(env), 'utf8'));
|
|
22
|
+
const value = parsed?.settings?.brainProfile;
|
|
23
|
+
return BRAIN_PROFILES.includes(value) ? value : PROFILE_COMPLETE;
|
|
24
|
+
} catch {
|
|
25
|
+
return PROFILE_COMPLETE;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function discoverStoreFamilies(dir) {
|
|
30
|
+
const names = new Set();
|
|
31
|
+
let entries = [];
|
|
32
|
+
try { entries = fs.readdirSync(dir); } catch { return []; }
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const match = entry.match(/^(.+?)(?:\.big)?\.rvf$/);
|
|
35
|
+
if (match && !/\.(?:idmap|embed)\b/.test(entry)) names.add(match[1]);
|
|
36
|
+
}
|
|
37
|
+
return [...names].sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function familyEntries(dir, store) {
|
|
41
|
+
return fs.readdirSync(dir).filter((entry) =>
|
|
42
|
+
entry === `${store}-primer.md` || entry === store || entry.startsWith(`${store}.`));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function filterCapabilityCards(dir) {
|
|
46
|
+
const file = path.join(dir, 'capability-cards.md');
|
|
47
|
+
if (!fs.existsSync(file)) return;
|
|
48
|
+
const backup = path.join(dir, 'capability-cards.complete.md');
|
|
49
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
50
|
+
const cardCount = (current.match(/^## /gm) || []).length;
|
|
51
|
+
if (!fs.existsSync(backup) || cardCount > 1) fs.copyFileSync(file, backup);
|
|
52
|
+
const full = fs.readFileSync(backup, 'utf8');
|
|
53
|
+
const firstCard = full.search(/^## /m);
|
|
54
|
+
const start = full.search(/^## ruvector\s*$/m);
|
|
55
|
+
if (firstCard < 0 || start < 0) throw new Error('could not isolate the RuVector capability card');
|
|
56
|
+
const rest = full.slice(start);
|
|
57
|
+
const next = rest.slice(1).search(/^## /m);
|
|
58
|
+
const card = next < 0 ? rest : rest.slice(0, next + 1);
|
|
59
|
+
fs.writeFileSync(file, `${full.slice(0, firstCard)}${card.trimEnd()}\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function restoreCapabilityCards(dir) {
|
|
63
|
+
const backup = path.join(dir, 'capability-cards.complete.md');
|
|
64
|
+
if (fs.existsSync(backup)) fs.copyFileSync(backup, path.join(dir, 'capability-cards.md'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function filterGenerationLedger(dir, allowed) {
|
|
68
|
+
const file = path.join(dir, 'RVF-GENERATIONS.json');
|
|
69
|
+
if (!fs.existsSync(file)) return;
|
|
70
|
+
const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
71
|
+
doc.stores = Object.fromEntries(Object.entries(doc.stores || {}).filter(([name]) => allowed.has(name)));
|
|
72
|
+
fs.writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function applyBrainProfile(dir, profile) {
|
|
76
|
+
if (!BRAIN_PROFILES.includes(profile)) throw new Error(`unknown brain profile: ${profile}`);
|
|
77
|
+
if (profile === PROFILE_COMPLETE) {
|
|
78
|
+
restoreCapabilityCards(dir);
|
|
79
|
+
return { profile, removed: [], bytesFreed: 0, stores: discoverStoreFamilies(dir) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const allowed = new Set([PROFILE_RUVECTOR]);
|
|
83
|
+
const removed = [];
|
|
84
|
+
const removedStores = [];
|
|
85
|
+
let bytesFreed = 0;
|
|
86
|
+
for (const store of discoverStoreFamilies(dir)) {
|
|
87
|
+
if (allowed.has(store)) continue;
|
|
88
|
+
removedStores.push(store);
|
|
89
|
+
for (const entry of familyEntries(dir, store)) {
|
|
90
|
+
const target = path.join(dir, entry);
|
|
91
|
+
try { bytesFreed += fs.statSync(target).size; } catch { /* report only measured bytes */ }
|
|
92
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
93
|
+
removed.push(entry);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
filterCapabilityCards(dir);
|
|
97
|
+
filterGenerationLedger(dir, allowed);
|
|
98
|
+
return { profile, removed, removedStores, bytesFreed, stores: discoverStoreFamilies(dir) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function restoreCompleteProfile(targetDir, sourceDir) {
|
|
102
|
+
if (!discoverStoreFamilies(sourceDir).includes(PROFILE_RUVECTOR)) {
|
|
103
|
+
throw new Error(`complete bundle source is unavailable at ${sourceDir}`);
|
|
104
|
+
}
|
|
105
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
106
|
+
const stores = discoverStoreFamilies(sourceDir);
|
|
107
|
+
for (const store of stores) {
|
|
108
|
+
for (const entry of familyEntries(sourceDir, store)) {
|
|
109
|
+
fs.cpSync(path.join(sourceDir, entry), path.join(targetDir, entry), {
|
|
110
|
+
recursive: true,
|
|
111
|
+
force: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const entry of ['capability-cards.md', 'RVF-GENERATIONS.json']) {
|
|
116
|
+
const source = path.join(sourceDir, entry);
|
|
117
|
+
if (fs.existsSync(source)) fs.copyFileSync(source, path.join(targetDir, entry));
|
|
118
|
+
}
|
|
119
|
+
const cards = path.join(sourceDir, 'capability-cards.md');
|
|
120
|
+
if (fs.existsSync(cards)) {
|
|
121
|
+
fs.copyFileSync(cards, path.join(targetDir, 'capability-cards.complete.md'));
|
|
122
|
+
}
|
|
123
|
+
return { profile: PROFILE_COMPLETE, stores: discoverStoreFamilies(targetDir) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function measureBrainProfile(dir) {
|
|
127
|
+
const stores = discoverStoreFamilies(dir);
|
|
128
|
+
const byStore = {};
|
|
129
|
+
let bytes = 0;
|
|
130
|
+
for (const store of stores) {
|
|
131
|
+
let storeBytes = 0;
|
|
132
|
+
for (const entry of familyEntries(dir, store)) {
|
|
133
|
+
const target = path.join(dir, entry);
|
|
134
|
+
const walk = (p) => {
|
|
135
|
+
const stat = fs.statSync(p);
|
|
136
|
+
if (!stat.isDirectory()) { storeBytes += stat.size; return; }
|
|
137
|
+
for (const child of fs.readdirSync(p)) walk(path.join(p, child));
|
|
138
|
+
};
|
|
139
|
+
try { walk(target); } catch { /* a changing file is omitted from the measurement */ }
|
|
140
|
+
}
|
|
141
|
+
byStore[store] = storeBytes;
|
|
142
|
+
bytes += storeBytes;
|
|
143
|
+
}
|
|
144
|
+
return { stores, storeCount: stores.length, bytes, byStore };
|
|
145
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const MINILM_MODEL = 'Xenova/all-MiniLM-L6-v2';
|
|
5
|
+
export const BGE_MODEL = 'Xenova/bge-base-en-v1.5';
|
|
6
|
+
|
|
7
|
+
export function modelPath(modelCache, model) {
|
|
8
|
+
return path.join(modelCache, ...model.split('/'));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function modelCacheReady(modelCache, model, revision = null) {
|
|
12
|
+
const root = revision
|
|
13
|
+
? path.join(modelPath(modelCache, model), revision)
|
|
14
|
+
: modelPath(modelCache, model);
|
|
15
|
+
return [
|
|
16
|
+
path.join(root, 'tokenizer.json'),
|
|
17
|
+
path.join(root, 'config.json'),
|
|
18
|
+
path.join(root, 'onnx', 'model_quantized.onnx'),
|
|
19
|
+
].every((file) => fs.existsSync(file));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Revision-pinned downloads live below <model>/<revision>/, while strict offline reads resolve
|
|
23
|
+
// <model>/ directly. Promote only the exact pinned files into that canonical offline location.
|
|
24
|
+
export function materializeModelRevision(modelCache, model, revision) {
|
|
25
|
+
if (!revision || !modelCacheReady(modelCache, model, revision)) return false;
|
|
26
|
+
const source = path.join(modelPath(modelCache, model), revision);
|
|
27
|
+
const destination = modelPath(modelCache, model);
|
|
28
|
+
for (const entry of fs.readdirSync(source)) {
|
|
29
|
+
fs.cpSync(path.join(source, entry), path.join(destination, entry), {
|
|
30
|
+
recursive: true,
|
|
31
|
+
force: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return modelCacheReady(modelCache, model);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The RVF sidecar is the source of truth for the query embedder. Only sidecars with a matching
|
|
38
|
+
// installed RVF count; stale/orphan metadata must not make a healthy installation look cold.
|
|
39
|
+
export function requiredEmbedderModels(kbDir) {
|
|
40
|
+
const models = new Set();
|
|
41
|
+
try {
|
|
42
|
+
for (const file of fs.readdirSync(kbDir)) {
|
|
43
|
+
if (!file.endsWith('.rvf.embed.json')) continue;
|
|
44
|
+
const rvf = file.slice(0, -'.embed.json'.length);
|
|
45
|
+
if (!fs.existsSync(path.join(kbDir, rvf))) continue;
|
|
46
|
+
if (!rvf.endsWith('.big.rvf')) {
|
|
47
|
+
const name = rvf.slice(0, -'.rvf'.length);
|
|
48
|
+
if (fs.existsSync(path.join(kbDir, `${name}.big.rvf`))) continue;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const model = JSON.parse(fs.readFileSync(path.join(kbDir, file), 'utf8'))?.model;
|
|
52
|
+
if (typeof model === 'string' && model.includes('/')) models.add(model);
|
|
53
|
+
} catch { /* unreadable metadata proves no requirement */ }
|
|
54
|
+
}
|
|
55
|
+
} catch { /* absent KB falls through to the legacy small-store default */ }
|
|
56
|
+
return models.size ? [...models].sort() : [MINILM_MODEL];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function missingEmbedderModels(modelCache, models) {
|
|
60
|
+
return models.filter((model) => !fs.existsSync(modelPath(modelCache, model)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function configureTransformersModel(T, modelCache, model) {
|
|
64
|
+
const local = modelCacheReady(modelCache, model);
|
|
65
|
+
T.env.localModelPath = modelCache;
|
|
66
|
+
// Transformers.js otherwise downloads into its package-local `.cache`, while every doctor and
|
|
67
|
+
// release detector inspects `modelCache`. A download invisible to the runtime's own detector
|
|
68
|
+
// causes repeated cold starts and false release failures.
|
|
69
|
+
T.env.cacheDir = modelCache;
|
|
70
|
+
T.env.allowRemoteModels = !local;
|
|
71
|
+
return { local };
|
|
72
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// kb/zip-extract.mjs — extract a .zip using ONLY Node built-ins. No `unzip` binary, no shell.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS (a real, measured stranger-machine failure, not a hypothetical):
|
|
4
|
+
// The stranger-machine CI matrix (.github/workflows/stranger-matrix.yml) ran the PACKED installer on
|
|
5
|
+
// a virgin windows-latest runner and the install aborted:
|
|
6
|
+
//
|
|
7
|
+
// ✗ install stopped: extraction failed
|
|
8
|
+
// (`unzip -q -o D:\a\_temp\proj\node_modules\ruvnet-brain\dist\ruvnet-brain.zip
|
|
9
|
+
// -d D:\a\_temp\home-healthy\.cache\ruvnet-brain\kb` exited with code 1).
|
|
10
|
+
//
|
|
11
|
+
// Shelling out to `unzip` is fragile on Windows for two INDEPENDENT reasons, and a stranger hits
|
|
12
|
+
// whichever one their machine hands them:
|
|
13
|
+
// 1. PowerShell has no `unzip` at all — nothing named `unzip` is on a stock Windows PATH.
|
|
14
|
+
// 2. Git Bash DOES put an MSYS2 `unzip` on PATH, and bin/install.mjs spawns with `shell: true` on
|
|
15
|
+
// Windows (it must, for .cmd shims), so the command line goes through cmd.exe carrying native
|
|
16
|
+
// `D:\a\...` paths into a POSIX-ish tool that treats `\` as an escape character. Same class of
|
|
17
|
+
// bug this repo already fixed once for ESM specifiers (raw `C:\` paths -> pathToFileURL).
|
|
18
|
+
// A path handed to a shell must be correct for THAT shell. The most reliable way to get that right
|
|
19
|
+
// is to never involve a shell: read the archive in-process.
|
|
20
|
+
//
|
|
21
|
+
// WHY NOT A LIBRARY: `npm ls --omit=dev --all` on this repo shows exactly two production deps
|
|
22
|
+
// (@metaharness/flywheel, @metaharness/router) and NO zip library anywhere in the tree — measured,
|
|
23
|
+
// not assumed. Node ships no archive reader either, so there was nothing already present to reuse:
|
|
24
|
+
// adding yauzl/adm-zip/fflate would put a new third-party package (and its transitive tree) in the
|
|
25
|
+
// path of `npx ruvnet-brain`, which bin/install.mjs's own header rule forbids ("dependency-free —
|
|
26
|
+
// Node built-ins only"). node:zlib's inflateRawSync/createInflateRaw is the deflate half of a zip;
|
|
27
|
+
// what is written below is only the container parsing around it.
|
|
28
|
+
//
|
|
29
|
+
// SCOPE, stated honestly: this reads the zip features our own bundles actually use — stored (0) and
|
|
30
|
+
// deflate (8), ZIP64, data descriptors, unix modes, symlinks. It refuses, LOUDLY and by name,
|
|
31
|
+
// anything it does not implement (encryption, other compression methods). It never guesses.
|
|
32
|
+
//
|
|
33
|
+
// FAIL LOUD. Every failure path throws an Error naming the archive, the entry, and what was wrong.
|
|
34
|
+
// A silent partial extraction that leaves an empty directory behind is worse than the original bug,
|
|
35
|
+
// so entry bytes are counted and (on Node >= 20.15 / 22.2, where zlib.crc32 exists) CRC-checked
|
|
36
|
+
// against the archive's own central-directory values.
|
|
37
|
+
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
import zlib from 'node:zlib';
|
|
41
|
+
import { Transform } from 'node:stream';
|
|
42
|
+
import { pipeline } from 'node:stream/promises';
|
|
43
|
+
|
|
44
|
+
const SIG_EOCD = 0x06054b50;
|
|
45
|
+
const SIG_EOCD64 = 0x06064b50;
|
|
46
|
+
const SIG_EOCD64_LOC = 0x07064b50;
|
|
47
|
+
const SIG_CENTRAL = 0x02014b50;
|
|
48
|
+
const SIG_LOCAL = 0x04034b50;
|
|
49
|
+
|
|
50
|
+
// zlib.crc32 landed in Node 20.15.0 / 22.2.0. package.json engines allow >=18, so it is OPTIONAL —
|
|
51
|
+
// present it is used, absent the size check still stands and the caller is told which happened.
|
|
52
|
+
// Reimplementing CRC-32 here would be hand-rolling something the runtime ships.
|
|
53
|
+
const crc32 = typeof zlib.crc32 === 'function' ? zlib.crc32 : null;
|
|
54
|
+
|
|
55
|
+
class Tally extends Transform {
|
|
56
|
+
constructor() { super(); this.bytes = 0; this.crc = 0; }
|
|
57
|
+
_transform(chunk, _enc, cb) {
|
|
58
|
+
this.bytes += chunk.length;
|
|
59
|
+
if (crc32) this.crc = crc32(chunk, this.crc);
|
|
60
|
+
this.push(chunk);
|
|
61
|
+
cb();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readAt(fd, length, position) {
|
|
66
|
+
const buf = Buffer.allocUnsafe(length);
|
|
67
|
+
let got = 0;
|
|
68
|
+
while (got < length) {
|
|
69
|
+
const n = fs.readSync(fd, buf, got, length - got, position + got);
|
|
70
|
+
if (n === 0) break;
|
|
71
|
+
got += n;
|
|
72
|
+
}
|
|
73
|
+
if (got !== length) throw new Error(`truncated archive: wanted ${length} bytes at offset ${position}, got ${got}`);
|
|
74
|
+
return buf;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Locate the End Of Central Directory record, tolerating a trailing zip comment (<=64KB). */
|
|
78
|
+
function findEocd(fd, size) {
|
|
79
|
+
const window = Math.min(size, 0xffff + 22);
|
|
80
|
+
const buf = readAt(fd, window, size - window);
|
|
81
|
+
for (let i = buf.length - 22; i >= 0; i--) {
|
|
82
|
+
if (buf.readUInt32LE(i) !== SIG_EOCD) continue;
|
|
83
|
+
return { buf, i, absolute: size - window + i };
|
|
84
|
+
}
|
|
85
|
+
throw new Error('not a zip archive (no End Of Central Directory record found)');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Where the central directory lives, honoring ZIP64 when the 32-bit fields are saturated.
|
|
90
|
+
* Bundles built by `zip -r` cross into ZIP64 on size or entry count without announcing it, so this
|
|
91
|
+
* is not optional defensive coding — a 4GB/65535-entry brain bundle is a realistic shape.
|
|
92
|
+
*/
|
|
93
|
+
function centralDirectoryLocation(fd, size) {
|
|
94
|
+
const { buf, i, absolute } = findEocd(fd, size);
|
|
95
|
+
let entries = buf.readUInt16LE(i + 10);
|
|
96
|
+
let cdSize = buf.readUInt32LE(i + 12);
|
|
97
|
+
let cdOffset = buf.readUInt32LE(i + 16);
|
|
98
|
+
|
|
99
|
+
if (entries === 0xffff || cdSize === 0xffffffff || cdOffset === 0xffffffff) {
|
|
100
|
+
const locAt = absolute - 20;
|
|
101
|
+
if (locAt < 0) throw new Error('archive claims ZIP64 but has no ZIP64 locator');
|
|
102
|
+
const loc = readAt(fd, 20, locAt);
|
|
103
|
+
if (loc.readUInt32LE(0) !== SIG_EOCD64_LOC) throw new Error('archive claims ZIP64 but the ZIP64 locator signature is wrong');
|
|
104
|
+
const eocd64At = Number(loc.readBigUInt64LE(8));
|
|
105
|
+
const rec = readAt(fd, 56, eocd64At);
|
|
106
|
+
if (rec.readUInt32LE(0) !== SIG_EOCD64) throw new Error('ZIP64 End Of Central Directory signature is wrong');
|
|
107
|
+
entries = Number(rec.readBigUInt64LE(32));
|
|
108
|
+
cdSize = Number(rec.readBigUInt64LE(40));
|
|
109
|
+
cdOffset = Number(rec.readBigUInt64LE(48));
|
|
110
|
+
}
|
|
111
|
+
return { entries, cdSize, cdOffset };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** ZIP64 extra field (0x0001): only the fields whose 32-bit form is saturated are present, in order. */
|
|
115
|
+
function applyZip64Extra(extra, entry) {
|
|
116
|
+
let p = 0;
|
|
117
|
+
while (p + 4 <= extra.length) {
|
|
118
|
+
const id = extra.readUInt16LE(p);
|
|
119
|
+
const len = extra.readUInt16LE(p + 2);
|
|
120
|
+
const body = extra.subarray(p + 4, p + 4 + len);
|
|
121
|
+
if (id === 0x0001) {
|
|
122
|
+
let q = 0;
|
|
123
|
+
if (entry.uncompSize === 0xffffffff && q + 8 <= body.length) { entry.uncompSize = Number(body.readBigUInt64LE(q)); q += 8; }
|
|
124
|
+
if (entry.compSize === 0xffffffff && q + 8 <= body.length) { entry.compSize = Number(body.readBigUInt64LE(q)); q += 8; }
|
|
125
|
+
if (entry.localOffset === 0xffffffff && q + 8 <= body.length) { entry.localOffset = Number(body.readBigUInt64LE(q)); q += 8; }
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
p += 4 + len;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseCentralDirectory(cd) {
|
|
133
|
+
const out = [];
|
|
134
|
+
let p = 0;
|
|
135
|
+
while (p + 46 <= cd.length) {
|
|
136
|
+
if (cd.readUInt32LE(p) !== SIG_CENTRAL) break;
|
|
137
|
+
const nameLen = cd.readUInt16LE(p + 28);
|
|
138
|
+
const extraLen = cd.readUInt16LE(p + 30);
|
|
139
|
+
const commentLen = cd.readUInt16LE(p + 32);
|
|
140
|
+
const entry = {
|
|
141
|
+
versionMadeBy: cd.readUInt16LE(p + 4),
|
|
142
|
+
flags: cd.readUInt16LE(p + 8),
|
|
143
|
+
method: cd.readUInt16LE(p + 10),
|
|
144
|
+
crc: cd.readUInt32LE(p + 16),
|
|
145
|
+
compSize: cd.readUInt32LE(p + 20),
|
|
146
|
+
uncompSize: cd.readUInt32LE(p + 24),
|
|
147
|
+
externalAttrs: cd.readUInt32LE(p + 38),
|
|
148
|
+
localOffset: cd.readUInt32LE(p + 42),
|
|
149
|
+
name: cd.subarray(p + 46, p + 46 + nameLen).toString('utf8'),
|
|
150
|
+
};
|
|
151
|
+
applyZip64Extra(cd.subarray(p + 46 + nameLen, p + 46 + nameLen + extraLen), entry);
|
|
152
|
+
out.push(entry);
|
|
153
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Reject anything that would write outside destDir. A downloaded archive is untrusted input, and
|
|
160
|
+
* `unzip` itself refuses `../` traversal — so this is parity with the tool being replaced, not an
|
|
161
|
+
* extra. Backslash is normalized to `/` first: a zip written on Windows may legitimately use it,
|
|
162
|
+
* and treating `a\..\..\b` as one opaque segment is exactly how traversal guards get bypassed.
|
|
163
|
+
*/
|
|
164
|
+
function safeJoin(destDir, name) {
|
|
165
|
+
const rel = name.replace(/\\/g, '/');
|
|
166
|
+
if (rel.startsWith('/') || /^[a-zA-Z]:/.test(rel)) return null;
|
|
167
|
+
if (rel.split('/').some((seg) => seg === '..')) return null;
|
|
168
|
+
const target = path.resolve(destDir, rel);
|
|
169
|
+
const root = path.resolve(destDir);
|
|
170
|
+
if (target !== root && !target.startsWith(root + path.sep)) return null;
|
|
171
|
+
return target;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Extract `zipPath` into `destDir`, overwriting existing files (idempotent — the `-o` of
|
|
176
|
+
* `unzip -q -o`). Returns { entries, files, bytes, crcChecked }.
|
|
177
|
+
* Throws an Error naming the archive and the offending entry on ANY problem.
|
|
178
|
+
*/
|
|
179
|
+
export async function extractZip(zipPath, destDir) {
|
|
180
|
+
const fd = fs.openSync(zipPath, 'r');
|
|
181
|
+
try {
|
|
182
|
+
const size = fs.fstatSync(fd).size;
|
|
183
|
+
if (size === 0) throw new Error('archive is empty (0 bytes)');
|
|
184
|
+
const { cdSize, cdOffset } = centralDirectoryLocation(fd, size);
|
|
185
|
+
if (cdOffset + cdSize > size) throw new Error(`central directory runs past end of file (offset ${cdOffset} + ${cdSize} > ${size})`);
|
|
186
|
+
const entries = parseCentralDirectory(readAt(fd, cdSize, cdOffset));
|
|
187
|
+
if (entries.length === 0) throw new Error('central directory contains no entries');
|
|
188
|
+
|
|
189
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
190
|
+
let files = 0;
|
|
191
|
+
let bytes = 0;
|
|
192
|
+
|
|
193
|
+
const normalizedEntryNames = entries.map((entry) => entry.name.replace(/\\/g, '/'));
|
|
194
|
+
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
|
|
195
|
+
const e = entries[entryIndex];
|
|
196
|
+
if (e.flags & 0x1) throw new Error(`entry "${e.name}" is encrypted — this archive cannot be extracted without a password`);
|
|
197
|
+
const target = safeJoin(destDir, e.name);
|
|
198
|
+
if (target === null) throw new Error(`entry "${e.name}" would escape the destination directory — refusing to extract (possible zip-slip)`);
|
|
199
|
+
|
|
200
|
+
// PowerShell Compress-Archive can encode directories without a trailing slash and marks them
|
|
201
|
+
// inconsistently across PowerShell/.NET versions: some archives carry the DOS directory bit,
|
|
202
|
+
// while others carry neither that bit nor a trailing slash. A zero-byte entry that is an
|
|
203
|
+
// ancestor of another central-directory entry is unambiguously a directory, so use the
|
|
204
|
+
// archive's own tree as the final signal. Treating it as a file poisons the next nested entry
|
|
205
|
+
// (`vendor` file, then `vendor/package.json` → ENOTDIR).
|
|
206
|
+
const unixType = (e.externalAttrs >>> 16) & 0xf000;
|
|
207
|
+
const normalizedName = normalizedEntryNames[entryIndex].replace(/\/+$/, '');
|
|
208
|
+
const hasDescendant = e.uncompSize === 0 && normalizedEntryNames.some(
|
|
209
|
+
(candidate, candidateIndex) => candidateIndex !== entryIndex
|
|
210
|
+
&& candidate.startsWith(`${normalizedName}/`),
|
|
211
|
+
);
|
|
212
|
+
const isDirectory = e.name.endsWith('/')
|
|
213
|
+
|| (e.externalAttrs & 0x10) !== 0
|
|
214
|
+
|| ((e.versionMadeBy >> 8) === 3 && unixType === 0x4000)
|
|
215
|
+
|| hasDescendant;
|
|
216
|
+
if (isDirectory) {
|
|
217
|
+
try {
|
|
218
|
+
if (fs.existsSync(target) && !fs.lstatSync(target).isDirectory()) {
|
|
219
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
} catch { /* mkdir below reports any unrecoverable collision with the exact path */ }
|
|
222
|
+
fs.mkdirSync(target, { recursive: true });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
226
|
+
|
|
227
|
+
// Sizes/offsets come from the CENTRAL directory, never the local header: with a data
|
|
228
|
+
// descriptor (flag bit 3, what streaming zippers emit) the local header's sizes are zeros.
|
|
229
|
+
const local = readAt(fd, 30, e.localOffset);
|
|
230
|
+
if (local.readUInt32LE(0) !== SIG_LOCAL) throw new Error(`entry "${e.name}" has a bad local header signature at offset ${e.localOffset}`);
|
|
231
|
+
const dataStart = e.localOffset + 30 + local.readUInt16LE(26) + local.readUInt16LE(28);
|
|
232
|
+
|
|
233
|
+
if (e.method !== 0 && e.method !== 8) {
|
|
234
|
+
throw new Error(`entry "${e.name}" uses unsupported compression method ${e.method} (only stored=0 and deflate=8 are implemented)`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const isSymlink = (e.versionMadeBy >> 8) === 3 && ((e.externalAttrs >>> 16) & 0xf000) === 0xa000;
|
|
238
|
+
if (isSymlink) {
|
|
239
|
+
// A symlink's payload is its target path — always tiny, so it is read whole rather than streamed.
|
|
240
|
+
const comp = e.compSize === 0 ? Buffer.alloc(0) : readAt(fd, e.compSize, dataStart);
|
|
241
|
+
const linkTarget = (e.method === 8 ? zlib.inflateRawSync(comp) : comp).toString('utf8');
|
|
242
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
243
|
+
fs.symlinkSync(linkTarget, target);
|
|
244
|
+
files++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// A zero-length entry has no data range to stream (start > end would be a nonsense read), so
|
|
249
|
+
// it is written directly. Its size/CRC assertions below still apply.
|
|
250
|
+
if (e.compSize === 0) {
|
|
251
|
+
fs.writeFileSync(target, Buffer.alloc(0));
|
|
252
|
+
if (e.uncompSize !== 0) throw new Error(`entry "${e.name}" is corrupt: 0 compressed bytes but claims ${e.uncompSize} uncompressed`);
|
|
253
|
+
files++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const tally = new Tally();
|
|
258
|
+
const stages = [fs.createReadStream(zipPath, { start: dataStart, end: dataStart + e.compSize - 1 })];
|
|
259
|
+
if (e.method === 8) stages.push(zlib.createInflateRaw());
|
|
260
|
+
stages.push(tally, fs.createWriteStream(target));
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await pipeline(...stages);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
throw new Error(`entry "${e.name}" failed to inflate (${err && err.code ? err.code : ''}${err && err.message ? ` ${err.message}` : ''}) — the archive is corrupt or truncated`);
|
|
266
|
+
}
|
|
267
|
+
if (tally.bytes !== e.uncompSize) {
|
|
268
|
+
throw new Error(`entry "${e.name}" is corrupt: expected ${e.uncompSize} bytes, extracted ${tally.bytes}`);
|
|
269
|
+
}
|
|
270
|
+
if (crc32 && (tally.crc >>> 0) !== (e.crc >>> 0)) {
|
|
271
|
+
// Zero-padded to 8 hex digits so the value is directly comparable to the one Info-ZIP's
|
|
272
|
+
// own `unzip` prints for the same damaged entry ("bad CRC 811e0dd1 (should be 0f6994e4)").
|
|
273
|
+
const hex = (n) => (n >>> 0).toString(16).padStart(8, '0');
|
|
274
|
+
throw new Error(`entry "${e.name}" failed its CRC-32 check (expected ${hex(e.crc)}, got ${hex(tally.crc)}) — the archive is corrupt`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Unix permission bits, when the archive carries them. Skipped on Windows, which has no
|
|
278
|
+
// POSIX mode and where chmod is a no-op at best.
|
|
279
|
+
if (process.platform !== 'win32' && (e.versionMadeBy >> 8) === 3) {
|
|
280
|
+
const mode = (e.externalAttrs >>> 16) & 0o7777;
|
|
281
|
+
if (mode) { try { fs.chmodSync(target, mode); } catch { /* mode is a nicety, not the payload */ } }
|
|
282
|
+
}
|
|
283
|
+
files++;
|
|
284
|
+
bytes += tally.bytes;
|
|
285
|
+
}
|
|
286
|
+
return { entries: entries.length, files, bytes, crcChecked: Boolean(crc32) };
|
|
287
|
+
} catch (err) {
|
|
288
|
+
// Always name the archive: "extraction failed" without the file is the message this whole
|
|
289
|
+
// change exists to stop shipping.
|
|
290
|
+
err.message = `${zipPath}: ${err.message}`;
|
|
291
|
+
throw err;
|
|
292
|
+
} finally {
|
|
293
|
+
fs.closeSync(fd);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export default { extractZip };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "3.9.
|
|
4
|
-
"description": "One-command installer for RuvNet Brain
|
|
3
|
+
"version": "3.9.129-dev",
|
|
4
|
+
"description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ruvnet-brain": "bin/install.mjs"
|
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
"test": "node plugin/test/run-tests.mjs",
|
|
11
11
|
"version:check": "node scripts/sync-version.mjs --check",
|
|
12
12
|
"claims:verify": "node scripts/claims-verify.mjs",
|
|
13
|
+
"claims:fix": "node scripts/claims-verify.mjs --fix",
|
|
13
14
|
"version:sync": "node scripts/sync-version.mjs",
|
|
14
15
|
"test:unit": "vitest run tests/unit",
|
|
16
|
+
"test:mesh": "vitest run tests/mesh",
|
|
17
|
+
"test:mutation": "vitest run tests/mutation",
|
|
18
|
+
"test:regression": "vitest run tests/regression",
|
|
15
19
|
"qe:ux": "node scripts/qe/ux-suite.mjs",
|
|
16
20
|
"test:cov": "vitest run tests/unit --coverage",
|
|
17
|
-
"test:all": "npm run test:unit && npm run test:integration && npm test",
|
|
21
|
+
"test:all": "npm run test:unit && npm run test:mesh && npm run test:mutation && npm run test:regression && npm run test:integration && npm test",
|
|
18
22
|
"metaharness:receipts": "node scripts/metaharness-receipts.mjs",
|
|
19
23
|
"route:cheap": "node scripts/route-cheap.mjs",
|
|
20
24
|
"route:receipt": "node scripts/dispatch-receipt.mjs",
|
|
@@ -23,6 +27,8 @@
|
|
|
23
27
|
"eval": "node scripts/eval-brain.mjs",
|
|
24
28
|
"eval:gate": "node scripts/eval-brain.mjs --gate",
|
|
25
29
|
"eval:record": "node scripts/eval-brain.mjs --record",
|
|
30
|
+
"eval:top100:build": "node scripts/top100-corpus.mjs",
|
|
31
|
+
"eval:top100": "node scripts/top100-benchmark.mjs",
|
|
26
32
|
"gists:index": "node scripts/ingest-gists.mjs --index-only",
|
|
27
33
|
"gists:sync": "node scripts/ingest-gists.mjs && node kb/forge-big.mjs both --dir kb --name ruv-gists",
|
|
28
34
|
"test:integration": "vitest run tests/integration",
|
|
@@ -31,28 +37,48 @@
|
|
|
31
37
|
"catalog:refresh": "node scripts/refresh-model-catalog.mjs",
|
|
32
38
|
"falsify": "node scripts/falsify.mjs",
|
|
33
39
|
"sbom": "npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-file sbom/ruvnet-brain.cdx.json --mc-type application --validate",
|
|
40
|
+
"learning:replay": "node scripts/learning-replay.mjs",
|
|
41
|
+
"learning:replay:check": "node scripts/learning-replay.mjs --check-portfolio",
|
|
42
|
+
"learning:replay:mutants:check": "node scripts/learning-replay.mjs --check-mutants",
|
|
43
|
+
"learning:replay:dry": "node scripts/learning-replay.mjs --dry-run",
|
|
34
44
|
"wired:check": "node scripts/wired-check.mjs --check",
|
|
35
45
|
"doc:currency": "node scripts/doc-currency.mjs --check",
|
|
36
|
-
"status:check": "node scripts/status-honesty.mjs"
|
|
46
|
+
"status:check": "node scripts/status-honesty.mjs",
|
|
47
|
+
"cap:collect": "node scripts/rerank-cap-eval.mjs --collect",
|
|
48
|
+
"cap:report": "node scripts/rerank-cap-eval.mjs --report",
|
|
49
|
+
"cap:ab": "node scripts/rerank-cap-warm-ab.mjs"
|
|
37
50
|
},
|
|
38
51
|
"files": [
|
|
39
52
|
"bin/install.mjs",
|
|
53
|
+
"plugin/mcp/managed-cli-interface.mjs",
|
|
40
54
|
"plugin/mcp/server.mjs",
|
|
55
|
+
"plugin/scripts/codex-hook-wrapper.mjs",
|
|
41
56
|
"!plugin/README.md",
|
|
42
57
|
"README.md",
|
|
43
58
|
"LICENSE",
|
|
44
59
|
"config/model-router/",
|
|
45
60
|
"kb/verify-citation.mjs",
|
|
61
|
+
"kb/zip-extract.mjs",
|
|
62
|
+
"kb/brain-profile.mjs",
|
|
63
|
+
"kb/model-requirements.mjs",
|
|
46
64
|
"scripts/model-router-engine.mjs",
|
|
47
65
|
"scripts/model-router-setup.mjs",
|
|
48
66
|
"scripts/model-router-status.mjs",
|
|
49
67
|
"scripts/model-router-outcome.mjs",
|
|
68
|
+
"scripts/subscription-hosts.mjs",
|
|
69
|
+
"scripts/dual-host-deliberation.mjs",
|
|
70
|
+
"scripts/dual-host-suggest.mjs",
|
|
50
71
|
"scripts/route-cheap.mjs",
|
|
51
72
|
"scripts/dispatch-receipt.mjs",
|
|
52
73
|
"scripts/metaharness-receipts.mjs",
|
|
53
74
|
"scripts/codex-routed.sh",
|
|
54
75
|
"scripts/metaharness-router.mjs",
|
|
55
76
|
"scripts/no-silent-substitution.mjs",
|
|
77
|
+
"scripts/user-settings.mjs",
|
|
78
|
+
"scripts/selfcheck.mjs",
|
|
79
|
+
"scripts/hook-registry.mjs",
|
|
80
|
+
"scripts/install-scope.mjs",
|
|
81
|
+
"scripts/upgrade-notice.mjs",
|
|
56
82
|
"scripts/falsify.mjs",
|
|
57
83
|
"scripts/nightly-watchdog.mjs",
|
|
58
84
|
"scripts/job-heartbeat.sh"
|
|
@@ -83,7 +109,9 @@
|
|
|
83
109
|
"author": "Stuart Kerr",
|
|
84
110
|
"devDependencies": {
|
|
85
111
|
"@metaharness/darwin": "~0.8.0",
|
|
112
|
+
"@ruvector/rvf": "^0.3.4",
|
|
86
113
|
"@vitest/coverage-v8": "^4.1.10",
|
|
114
|
+
"playwright": "^1.62.0",
|
|
87
115
|
"vitest": "^4.1.10"
|
|
88
116
|
},
|
|
89
117
|
"dependencies": {
|