memtrace 0.8.0 → 0.8.2

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/install.js CHANGED
@@ -239,6 +239,50 @@ function selfHealPlatformPackage() {
239
239
  return true;
240
240
  }
241
241
 
242
+ // ── CUDA execution provider (win32 NVIDIA hosts) ─────────────────────────────
243
+ //
244
+ // v0.8.0 dropped `onnxruntime_providers_cuda.dll` from @memtrace/win32-x64
245
+ // (400.9 MB tripped npm's E413 size ceiling); it ships as a GitHub release
246
+ // asset instead. Without it, Windows NVIDIA users silently degrade to
247
+ // DirectML. This step detects an NVIDIA GPU and fetches + sha256-verifies
248
+ // the DLL into the platform package's bin dir. Package-internal
249
+ // provisioning (never touches user config), so it runs pre-consent
250
+ // alongside the optional-dep self-heal. Every failure mode is a warn +
251
+ // continue; `memtrace gpu install-cuda` is the manual retry path.
252
+ // Opt out with MEMTRACE_SKIP_CUDA_EP=1.
253
+ async function maybeInstallCudaEp() {
254
+ const cudaEp = require("./lib/cuda-ep");
255
+ const pkg = platformPackageName(getPlatformKey());
256
+ const gate = cudaEp.shouldFetchCudaEp({ packageName: pkg, env: process.env });
257
+ if (!gate.fetch) return;
258
+
259
+ let binaryPath;
260
+ try {
261
+ binaryPath = getBinaryPath();
262
+ } catch (_) {
263
+ return; // no binary — self-heal already warned, nowhere to put the DLL
264
+ }
265
+ const version = getPinnedPlatformVersion(require("./package.json"), pkg);
266
+ try {
267
+ const result = await cudaEp.installCudaEp({ binaryPath, version, log: console.log });
268
+ if (result.status === "installed") {
269
+ console.log(`memtrace: CUDA execution provider installed at ${result.dllPath}`);
270
+ } else if (result.status === "skipped") {
271
+ console.log(
272
+ `memtrace: CUDA execution provider not installed (${result.reason}). ` +
273
+ `Run 'memtrace gpu install-cuda' to fetch it manually.`
274
+ );
275
+ } else if (result.status === "error") {
276
+ console.warn(
277
+ `memtrace: CUDA execution provider install failed: ${result.reason}. ` +
278
+ `GPU embedding falls back to DirectML; retry with 'memtrace gpu install-cuda'.`
279
+ );
280
+ }
281
+ } catch (e) {
282
+ console.warn(`memtrace: CUDA execution provider install skipped: ${e.message}`);
283
+ }
284
+ }
285
+
242
286
  // ── Integration phases (post-consent) ───────────────────────────────────────
243
287
  //
244
288
  // Everything below this line mutates user configuration and therefore only
@@ -402,6 +446,9 @@ async function main() {
402
446
  } catch (e) {
403
447
  console.warn(`memtrace: ${e.message}`);
404
448
  }
449
+ // Still package-internal: drop the CUDA EP next to the binary on
450
+ // NVIDIA hosts (no-op everywhere else, idempotent on re-runs).
451
+ await maybeInstallCudaEp();
405
452
  }
406
453
 
407
454
  // Step 2 — consent gate (memtrace-public #25). Print the full mutation
@@ -459,6 +506,7 @@ module.exports = {
459
506
  getBinaryPath,
460
507
  ensureExecutableBits,
461
508
  runIntegrationPhases,
509
+ maybeInstallCudaEp,
462
510
  selfHealPlatformPackage,
463
511
  buildSelfHealEnv,
464
512
  buildSelfHealInstallArgs,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace-skills",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Memtrace skills for AI coding agents — codebase exploration, temporal evolution, impact analysis, and more.",
5
5
  "type": "module",
6
6
  "bin": {
package/lib/cuda-ep.js ADDED
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+
3
+ // On-demand CUDA execution provider fetch (Windows NVIDIA hosts).
4
+ //
5
+ // As of v0.8.0 the @memtrace/win32-x64 package no longer bundles
6
+ // `onnxruntime_providers_cuda.dll` — at 400.9 MB it pushed the tarball
7
+ // over npm's payload ceiling (E413, broke the v0.8.0 publish). The DLL
8
+ // ships as a GitHub release asset instead (`memtrace-windows-x86_64-
9
+ // cuda-ep.zip`, sha256 recorded in the release's `checksums.txt`), and
10
+ // this module fetches it at install time for hosts that can actually
11
+ // use it. The engine probes CUDA -> DirectML -> CPU at startup, so a
12
+ // missing DLL is never fatal — NVIDIA users just silently land on
13
+ // DirectML, which is what this fetch exists to avoid.
14
+ //
15
+ // Placement contract: the engine's dylib discovery looks next to the
16
+ // running binary (`bridge_ort_dylib_env` in the Rust CLI), so the DLL
17
+ // must land in the platform package's bin/ dir, beside memtrace.exe.
18
+ // That directory is owned by the platform package: an npm upgrade
19
+ // replaces it wholesale, which conveniently re-triggers this fetch on
20
+ // the next postinstall. Everything here is package-internal
21
+ // provisioning — it never touches user config, so install.js runs it
22
+ // BEFORE the consent gate, same as the optional-dep self-heal.
23
+ //
24
+ // Failure policy: best-effort, always. Offline, missing release asset,
25
+ // checksum mismatch, PowerShell missing — every failure path returns a
26
+ // {status, reason} result instead of throwing, and the caller logs a
27
+ // pointer to the manual escape hatch (`memtrace gpu install-cuda`).
28
+ // A postinstall must never fail the package install over an optional
29
+ // accelerator.
30
+ //
31
+ // All IO is dependency-injected (fetch, spawn, fs paths) so tests can
32
+ // drive offline / mismatch / no-GPU modes deterministically — same
33
+ // pattern as lib/update-check.js.
34
+
35
+ const os = require("os");
36
+ const path = require("path");
37
+ const fs = require("fs");
38
+ const crypto = require("crypto");
39
+ const { spawnSync } = require("child_process");
40
+
41
+ const CUDA_EP_ASSET = "memtrace-windows-x86_64-cuda-ep.zip";
42
+ const CUDA_EP_DLL = "onnxruntime_providers_cuda.dll";
43
+ const RELEASE_BASE_URL = "https://github.com/syncable-dev/memtrace/releases/download";
44
+ // Only the default AVX2 package. The noavx2 variant bundles a
45
+ // from-source onnxruntime.dll; pairing it with Microsoft's prebuilt
46
+ // CUDA EP is an untested ABI mix (and pre-Haswell CPU + modern NVIDIA
47
+ // GPU is not a combination worth chasing).
48
+ const CUDA_PACKAGE = "@memtrace/win32-x64";
49
+
50
+ // Abort the zip download if the socket goes silent this long. The asset
51
+ // is ~400 MB so there is deliberately NO total-time cap — slow links
52
+ // are fine, dead links are not.
53
+ const INACTIVITY_TIMEOUT_MS = 60_000;
54
+ const CHECKSUMS_TIMEOUT_MS = 15_000;
55
+
56
+ // ── Pure helpers ─────────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Release asset URLs for a memtrace version. Accepts "0.8.0" or
60
+ * "v0.8.0"; release tags are always v-prefixed.
61
+ */
62
+ function releaseAssetUrls(version, baseUrl) {
63
+ const base = (baseUrl || RELEASE_BASE_URL).replace(/\/+$/, "");
64
+ const tag = version.startsWith("v") ? version : `v${version}`;
65
+ return {
66
+ zipUrl: `${base}/${tag}/${CUDA_EP_ASSET}`,
67
+ checksumsUrl: `${base}/${tag}/checksums.txt`,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Extract the sha256 for `assetName` from `sha256sum`-format text
73
+ * (`<64 hex> <name>`, optional `*` binary marker). Returns lowercase
74
+ * hex or null when the asset has no line.
75
+ */
76
+ function parseChecksums(text, assetName) {
77
+ if (typeof text !== "string") return null;
78
+ for (const line of text.split(/\r?\n/)) {
79
+ const m = /^([0-9a-fA-F]{64})\s+\*?(.+)$/.exec(line.trim());
80
+ if (m && m[2].trim() === assetName) {
81
+ return m[1].toLowerCase();
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Gate: is fetching the CUDA EP even applicable on this install?
89
+ * Returns {fetch, reason}. Pure — platform/package/env all injected.
90
+ */
91
+ function shouldFetchCudaEp({ platform = os.platform(), packageName, env = process.env } = {}) {
92
+ if (env.MEMTRACE_SKIP_CUDA_EP === "1") {
93
+ return { fetch: false, reason: "MEMTRACE_SKIP_CUDA_EP=1" };
94
+ }
95
+ if (platform !== "win32") {
96
+ return { fetch: false, reason: `platform ${platform} (win32 only)` };
97
+ }
98
+ if (packageName !== CUDA_PACKAGE) {
99
+ return { fetch: false, reason: `platform package ${packageName} (only ${CUDA_PACKAGE})` };
100
+ }
101
+ return { fetch: true, reason: "win32 AVX2 package" };
102
+ }
103
+
104
+ /** True when `spawnSync`-shaped result output looks like an NVIDIA GPU list. */
105
+ function nvidiaSmiSaysYes(result) {
106
+ return Boolean(result && result.status === 0 && /\bGPU \d+/.test(result.stdout || ""));
107
+ }
108
+
109
+ /** True when a Win32_VideoController name dump mentions an NVIDIA adapter. */
110
+ function videoControllerSaysYes(result) {
111
+ return Boolean(result && result.status === 0 && /nvidia/i.test(result.stdout || ""));
112
+ }
113
+
114
+ // ── Host probes ──────────────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * Best-effort NVIDIA detection on win32. `nvidia-smi -L` first (present
118
+ * whenever the NVIDIA driver is installed — exactly the hosts where the
119
+ * CUDA EP can load), then a CIM adapter-name query for driver installs
120
+ * that didn't put nvidia-smi on PATH. Ambiguity/errors → false: the
121
+ * download is 400 MB, so we only fetch on a positive signal, and
122
+ * `memtrace gpu install-cuda --force` exists for detection misses.
123
+ */
124
+ function detectNvidiaGpu({ spawn = spawnSync } = {}) {
125
+ try {
126
+ const smi = spawn("nvidia-smi", ["-L"], { encoding: "utf8", timeout: 5000, windowsHide: true });
127
+ if (nvidiaSmiSaysYes(smi)) return true;
128
+ } catch (_) {
129
+ /* not on PATH — fall through to CIM */
130
+ }
131
+ try {
132
+ const ps = spawn(
133
+ "powershell",
134
+ [
135
+ "-NoProfile",
136
+ "-NonInteractive",
137
+ "-Command",
138
+ "(Get-CimInstance Win32_VideoController).Name",
139
+ ],
140
+ { encoding: "utf8", timeout: 10000, windowsHide: true }
141
+ );
142
+ return videoControllerSaysYes(ps);
143
+ } catch (_) {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ // ── Network ──────────────────────────────────────────────────────────────────
149
+
150
+ function requireFetch(fetchImpl) {
151
+ const f = fetchImpl || globalThis.fetch;
152
+ if (typeof f !== "function") {
153
+ throw new Error("global fetch unavailable (Node >= 18 required)");
154
+ }
155
+ return f;
156
+ }
157
+
158
+ async function fetchText(url, { fetchImpl, timeoutMs = CHECKSUMS_TIMEOUT_MS } = {}) {
159
+ const f = requireFetch(fetchImpl);
160
+ const res = await f(url, {
161
+ redirect: "follow",
162
+ signal: AbortSignal.timeout(timeoutMs),
163
+ headers: { "user-agent": "memtrace-install" },
164
+ });
165
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
166
+ return res.text();
167
+ }
168
+
169
+ /**
170
+ * Stream `url` to `destPath`, returning the sha256 hex of the bytes
171
+ * written. Aborts when the connection stalls for `inactivityTimeoutMs`
172
+ * (no total cap — see header comment).
173
+ */
174
+ async function downloadToFile(url, destPath, { fetchImpl, inactivityTimeoutMs = INACTIVITY_TIMEOUT_MS } = {}) {
175
+ const f = requireFetch(fetchImpl);
176
+ const controller = new AbortController();
177
+ let watchdog = null;
178
+ const armWatchdog = () => {
179
+ clearTimeout(watchdog);
180
+ watchdog = setTimeout(
181
+ () => controller.abort(new Error(`download stalled for ${inactivityTimeoutMs}ms`)),
182
+ inactivityTimeoutMs
183
+ );
184
+ if (watchdog.unref) watchdog.unref();
185
+ };
186
+
187
+ armWatchdog();
188
+ try {
189
+ const res = await f(url, {
190
+ redirect: "follow",
191
+ signal: controller.signal,
192
+ headers: { "user-agent": "memtrace-install" },
193
+ });
194
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
195
+ if (!res.body) throw new Error(`empty response body for ${url}`);
196
+
197
+ const hash = crypto.createHash("sha256");
198
+ const out = fs.createWriteStream(destPath);
199
+ try {
200
+ const reader = res.body.getReader();
201
+ for (;;) {
202
+ const { done, value } = await reader.read();
203
+ if (done) break;
204
+ armWatchdog();
205
+ hash.update(value);
206
+ if (!out.write(value)) {
207
+ await new Promise((resolve, reject) => {
208
+ out.once("drain", resolve);
209
+ out.once("error", reject);
210
+ });
211
+ }
212
+ }
213
+ await new Promise((resolve, reject) => out.end((err) => (err ? reject(err) : resolve())));
214
+ } catch (e) {
215
+ out.destroy();
216
+ throw e;
217
+ }
218
+ return hash.digest("hex");
219
+ } finally {
220
+ clearTimeout(watchdog);
221
+ }
222
+ }
223
+
224
+ // ── Extraction ───────────────────────────────────────────────────────────────
225
+
226
+ /**
227
+ * Expand the single-DLL zip into `destDir` with PowerShell's
228
+ * Expand-Archive (built into every supported Windows; Node has no zip
229
+ * reader and this module takes no deps). Returns the extracted DLL
230
+ * path. Throws with stderr context on failure.
231
+ */
232
+ function extractDll(zipPath, destDir, { spawn = spawnSync } = {}) {
233
+ const result = spawn(
234
+ "powershell",
235
+ [
236
+ "-NoProfile",
237
+ "-NonInteractive",
238
+ "-Command",
239
+ // -LiteralPath: both paths come from mkdtemp and can contain
240
+ // characters PowerShell would otherwise glob.
241
+ `Expand-Archive -LiteralPath '${zipPath}' -DestinationPath '${destDir}' -Force`,
242
+ ],
243
+ { encoding: "utf8", timeout: 120_000, windowsHide: true }
244
+ );
245
+ if (result.status !== 0) {
246
+ const detail = (result.stderr || result.error?.message || "").trim().slice(0, 300);
247
+ throw new Error(`Expand-Archive failed (exit ${result.status})${detail ? `: ${detail}` : ""}`);
248
+ }
249
+ const dllPath = path.join(destDir, CUDA_EP_DLL);
250
+ if (!fs.existsSync(dllPath)) {
251
+ throw new Error(`${CUDA_EP_DLL} not found in archive`);
252
+ }
253
+ return dllPath;
254
+ }
255
+
256
+ // ── Orchestrator ─────────────────────────────────────────────────────────────
257
+
258
+ /**
259
+ * Fetch + verify + place the CUDA EP DLL next to `binaryPath`.
260
+ * Idempotent: an existing DLL short-circuits unless `force`.
261
+ *
262
+ * Never throws for expected conditions — resolves to:
263
+ * {status: "installed", dllPath}
264
+ * {status: "already-installed", dllPath}
265
+ * {status: "skipped", reason} (no GPU / offline / asset absent)
266
+ * {status: "error", reason} (checksum mismatch, extract/place failure)
267
+ */
268
+ async function installCudaEp({
269
+ binaryPath,
270
+ version,
271
+ env = process.env,
272
+ fetchImpl,
273
+ spawn = spawnSync,
274
+ detect, // injectable NVIDIA probe for tests
275
+ force = false,
276
+ log = () => {},
277
+ } = {}) {
278
+ const binDir = path.dirname(binaryPath);
279
+ const dllPath = path.join(binDir, CUDA_EP_DLL);
280
+ if (!force && fs.existsSync(dllPath)) {
281
+ return { status: "already-installed", dllPath };
282
+ }
283
+
284
+ const hasNvidia = detect ? detect() : detectNvidiaGpu({ spawn });
285
+ if (!hasNvidia && !force) {
286
+ return { status: "skipped", reason: "no NVIDIA GPU detected" };
287
+ }
288
+
289
+ const urls = releaseAssetUrls(version, env.MEMTRACE_CUDA_EP_BASE_URL);
290
+
291
+ // checksums.txt first: a ~1 KB fetch that doubles as the offline
292
+ // probe, so we never start a 400 MB transfer we can't verify.
293
+ let checksumsText;
294
+ try {
295
+ checksumsText = await fetchText(urls.checksumsUrl, { fetchImpl });
296
+ } catch (e) {
297
+ return { status: "skipped", reason: `release checksums unreachable (${e.message})` };
298
+ }
299
+ const expected = parseChecksums(checksumsText, CUDA_EP_ASSET);
300
+ if (!expected) {
301
+ return { status: "skipped", reason: `release v${version} has no ${CUDA_EP_ASSET}` };
302
+ }
303
+
304
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "memtrace-cuda-ep-"));
305
+ try {
306
+ const zipPath = path.join(workDir, CUDA_EP_ASSET);
307
+ log(`memtrace: NVIDIA GPU detected — downloading CUDA execution provider (~400 MB, one-time)…`);
308
+ let actual;
309
+ try {
310
+ actual = await downloadToFile(urls.zipUrl, zipPath, { fetchImpl });
311
+ } catch (e) {
312
+ return { status: "skipped", reason: `download failed (${e.message})` };
313
+ }
314
+ if (actual !== expected) {
315
+ return { status: "error", reason: `sha256 mismatch for ${CUDA_EP_ASSET}: expected ${expected}, got ${actual}` };
316
+ }
317
+
318
+ let extracted;
319
+ try {
320
+ extracted = extractDll(zipPath, workDir, { spawn });
321
+ } catch (e) {
322
+ return { status: "error", reason: e.message };
323
+ }
324
+
325
+ // Copy into place via a temp name + rename so a crash mid-copy
326
+ // never leaves a truncated DLL where the engine's probe finds it.
327
+ const staging = path.join(binDir, `.${CUDA_EP_DLL}.download`);
328
+ try {
329
+ fs.copyFileSync(extracted, staging);
330
+ fs.renameSync(staging, dllPath);
331
+ } catch (e) {
332
+ try { fs.rmSync(staging, { force: true }); } catch (_) { /* best-effort */ }
333
+ return { status: "error", reason: `could not place DLL in ${binDir} (${e.message})` };
334
+ }
335
+ return { status: "installed", dllPath };
336
+ } finally {
337
+ try { fs.rmSync(workDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
338
+ }
339
+ }
340
+
341
+ module.exports = {
342
+ CUDA_EP_ASSET,
343
+ CUDA_EP_DLL,
344
+ CUDA_PACKAGE,
345
+ RELEASE_BASE_URL,
346
+ releaseAssetUrls,
347
+ parseChecksums,
348
+ shouldFetchCudaEp,
349
+ nvidiaSmiSaysYes,
350
+ videoControllerSaysYes,
351
+ detectNvidiaGpu,
352
+ downloadToFile,
353
+ extractDll,
354
+ installCudaEp,
355
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
5
5
  "keywords": [
6
6
  "mcp",
@@ -39,12 +39,12 @@
39
39
  "fs-extra": "^11.0.0"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@memtrace/darwin-arm64": "0.8.0",
43
- "@memtrace/linux-x64": "0.8.0",
44
- "@memtrace/linux-arm64": "0.8.0",
45
- "@memtrace/win32-x64": "0.8.0",
46
- "@memtrace/linux-x64-noavx2": "0.8.0",
47
- "@memtrace/win32-x64-noavx2": "0.8.0"
42
+ "@memtrace/darwin-arm64": "0.8.2",
43
+ "@memtrace/linux-x64": "0.8.2",
44
+ "@memtrace/linux-arm64": "0.8.2",
45
+ "@memtrace/win32-x64": "0.8.2",
46
+ "@memtrace/linux-x64-noavx2": "0.8.2",
47
+ "@memtrace/win32-x64-noavx2": "0.8.2"
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=18"