basemind 0.2.6 → 0.4.0-rc.1

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.
Files changed (2) hide show
  1. package/install.js +120 -15
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -3,6 +3,7 @@ const os = require("node:os");
3
3
  const path = require("node:path");
4
4
  const https = require("node:https");
5
5
  const http = require("node:http");
6
+ const crypto = require("node:crypto");
6
7
  const tar = require("tar");
7
8
  const AdmZip = require("adm-zip");
8
9
 
@@ -13,7 +14,7 @@ function getPlatformTriple() {
13
14
  const arch = os.arch();
14
15
 
15
16
  if (type === "Windows_NT") {
16
- if (arch === "x64") return "x86_64-pc-windows-gnu";
17
+ if (arch === "x64") return "x86_64-pc-windows-msvc";
17
18
  if (arch === "ia32") throw new Error("32-bit Windows is not supported");
18
19
  }
19
20
 
@@ -32,11 +33,16 @@ function getPlatformTriple() {
32
33
  throw new Error(`Unsupported platform: ${type} ${arch}`);
33
34
  }
34
35
 
35
- function getBinaryUrl() {
36
+ function getReleaseAssets() {
36
37
  const platform = getPlatformTriple();
37
38
  const baseUrl = `https://github.com/Goldziher/basemind/releases/download/v${version}`;
38
39
  const ext = platform.includes("windows") ? "zip" : "tar.gz";
39
- return `${baseUrl}/basemind-${platform}.${ext}`;
40
+ const assetName = `basemind-${platform}.${ext}`;
41
+ return {
42
+ assetName,
43
+ archiveUrl: `${baseUrl}/${assetName}`,
44
+ checksumsUrl: `${baseUrl}/basemind_${version}_checksums.txt`,
45
+ };
40
46
  }
41
47
 
42
48
  function downloadWithRedirects(url, dest, maxRedirects = 5) {
@@ -89,12 +95,105 @@ function downloadWithRedirects(url, dest, maxRedirects = 5) {
89
95
  });
90
96
  }
91
97
 
98
+ // Download a (small) text resource into memory, following redirects.
99
+ function fetchTextWithRedirects(url, maxRedirects = 5) {
100
+ return new Promise((resolve, reject) => {
101
+ if (maxRedirects <= 0) {
102
+ return reject(new Error("Too many redirects"));
103
+ }
104
+
105
+ const urlObj = new URL(url);
106
+ const client = urlObj.protocol === "https:" ? https : http;
107
+
108
+ const req = client.get(
109
+ url,
110
+ {
111
+ headers: {
112
+ "User-Agent": "basemind-npm-wrapper",
113
+ },
114
+ },
115
+ (res) => {
116
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
117
+ return fetchTextWithRedirects(res.headers.location, maxRedirects - 1)
118
+ .then(resolve)
119
+ .catch(reject);
120
+ }
121
+
122
+ if (res.statusCode !== 200) {
123
+ return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
124
+ }
125
+
126
+ const chunks = [];
127
+ res.on("data", (chunk) => chunks.push(chunk));
128
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
129
+ res.on("error", reject);
130
+ },
131
+ );
132
+
133
+ req.on("error", reject);
134
+ req.setTimeout(30000, () => {
135
+ req.destroy();
136
+ reject(new Error("Download timeout"));
137
+ });
138
+ });
139
+ }
140
+
141
+ function sha256File(filePath) {
142
+ const hash = crypto.createHash("sha256");
143
+ hash.update(fs.readFileSync(filePath));
144
+ return hash.digest("hex");
145
+ }
146
+
147
+ // Parse a `sha256<space>filename` checksums file and return the digest for
148
+ // `assetName`, or null if absent. Lines may use one or two spaces (GNU coreutils
149
+ // uses two: binary-mode marker "* "), so split on whitespace.
150
+ function expectedDigest(checksumsText, assetName) {
151
+ for (const line of checksumsText.split(/\r?\n/)) {
152
+ const trimmed = line.trim();
153
+ if (!trimmed) continue;
154
+ const parts = trimmed.split(/\s+/);
155
+ if (parts.length < 2) continue;
156
+ const digest = parts[0];
157
+ const name = parts[parts.length - 1].replace(/^\*/, "");
158
+ if (name === assetName) return digest.toLowerCase();
159
+ }
160
+ return null;
161
+ }
162
+
163
+ // Verify the downloaded archive against the release checksums file. Fails CLOSED:
164
+ // any failure to fetch the checksums, locate the entry, or match the digest
165
+ // aborts the install.
166
+ async function verifyChecksum(archivePath, assetName, checksumsUrl) {
167
+ let checksumsText;
168
+ try {
169
+ checksumsText = await fetchTextWithRedirects(checksumsUrl);
170
+ } catch (error) {
171
+ throw new Error(
172
+ `could not fetch checksums (${checksumsUrl}): ${error.message} — refusing to install unverified binary`,
173
+ );
174
+ }
175
+
176
+ const expected = expectedDigest(checksumsText, assetName);
177
+ if (!expected) {
178
+ throw new Error(
179
+ `no checksum entry for ${assetName} in ${checksumsUrl} — refusing to install unverified binary`,
180
+ );
181
+ }
182
+
183
+ const actual = sha256File(archivePath).toLowerCase();
184
+ if (actual !== expected) {
185
+ throw new Error(`checksum mismatch for ${assetName} (expected ${expected}, got ${actual})`);
186
+ }
187
+
188
+ console.log("Checksum verified.");
189
+ }
190
+
92
191
  async function installBinary() {
93
192
  try {
94
- const url = getBinaryUrl();
95
- const isZip = url.endsWith(".zip");
193
+ const { assetName, archiveUrl, checksumsUrl } = getReleaseAssets();
194
+ const isZip = archiveUrl.endsWith(".zip");
96
195
  const binDir = path.join(__dirname, "bin");
97
- const archivePath = path.join(binDir, isZip ? "basemind.zip" : "basemind.tar.gz");
196
+ const archivePath = path.join(binDir, assetName);
98
197
  const binaryName = os.type() === "Windows_NT" ? "basemind.exe" : "basemind";
99
198
  const binaryPath = path.join(binDir, binaryName);
100
199
 
@@ -106,29 +205,35 @@ async function installBinary() {
106
205
  return;
107
206
  }
108
207
 
109
- console.log(`Downloading basemind binary from ${url}...`);
208
+ console.log(`Downloading basemind binary from ${archiveUrl}...`);
110
209
 
111
- await downloadWithRedirects(url, archivePath);
210
+ await downloadWithRedirects(archiveUrl, archivePath);
112
211
 
113
- console.log("Extracting binary...");
212
+ // Fail CLOSED: verify the archive against the release checksums before
213
+ // extracting anything. Any fetch/parse/mismatch failure aborts the install.
214
+ await verifyChecksum(archivePath, assetName, checksumsUrl);
114
215
 
216
+ console.log("Extracting archive (binary + bundled libraries)...");
217
+
218
+ // Archives now contain the binary plus a lib/ tree of bundled native
219
+ // libraries (the binary finds them via rpath; Windows co-locates DLLs next
220
+ // to the exe). Extract the whole tree into bin/, not just the binary.
115
221
  if (isZip) {
116
222
  const zip = new AdmZip(archivePath);
117
- const entry = zip.getEntries().find((e) => e.entryName.endsWith(binaryName));
118
- if (!entry) {
119
- throw new Error("Binary not found in downloaded archive");
120
- }
121
- zip.extractEntryTo(entry, binDir, false, true);
223
+ zip.extractAllTo(binDir, true);
122
224
  } else {
123
225
  await tar.extract({
124
226
  file: archivePath,
125
227
  cwd: binDir,
126
- filter: (entryPath) => entryPath.endsWith(binaryName),
127
228
  });
128
229
  }
129
230
 
130
231
  fs.unlinkSync(archivePath);
131
232
 
233
+ if (!fs.existsSync(binaryPath)) {
234
+ throw new Error(`binary ${binaryName} not found after extracting ${assetName}`);
235
+ }
236
+
132
237
  if (os.type() !== "Windows_NT") {
133
238
  fs.chmodSync(binaryPath, 0o755);
134
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basemind",
3
- "version": "0.2.6",
3
+ "version": "0.4.0-rc.1",
4
4
  "description": "Full AI context layer over MCP — tree-sitter code-map, document RAG (PDF/Office/HTML/email + OCR + reranker), shared agent memory, on-demand web crawl, git history + blame + per-symbol diff. 300+ languages, 8 coding-agent harnesses, content-addressed Fjall + LanceDB.",
5
5
  "keywords": [
6
6
  "agent-context",