@munhq/codeindex 0.3.6 → 0.4.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.
@@ -26,6 +26,27 @@ const { resolveBinary, log } = require('./resolve.js');
26
26
  // handshake that never completes, which reads as "the server is broken".
27
27
  if (!args.includes('--mcp')) args.unshift('--mcp');
28
28
 
29
+ // Become the binary rather than supervising it.
30
+ //
31
+ // Once the binary is resolved this process adds nothing: stdio is already
32
+ // inherited, and the status it relays below is the child's own. What it does
33
+ // add is a resident Node heap for the whole life of the MCP session — one per
34
+ // session, sitting next to a server that is a few megabytes — and on a cold
35
+ // cache that heap is the one the download inflated. `execve` replaces this
36
+ // process with the binary, so the heap is gone rather than merely idle, and
37
+ // the process tree loses a level.
38
+ //
39
+ // `args` is the complete argv, argv[0] included. POSIX only, and Node 22.15 /
40
+ // 23.11 and later; anywhere else the supervised child below runs exactly as
41
+ // it always did.
42
+ if (typeof process.execve === 'function') {
43
+ try {
44
+ process.execve(bin, [bin, ...args], process.env);
45
+ } catch (err) {
46
+ log(`could not exec ${bin} (${err.message}); supervising it instead`);
47
+ }
48
+ }
49
+
29
50
  const res = spawnSync(bin, args, { stdio: 'inherit' });
30
51
  if (res.error) {
31
52
  log(`failed to exec ${bin}: ${res.error.message}`);
package/bin/resolve.js CHANGED
@@ -18,6 +18,7 @@ const os = require('os');
18
18
  const path = require('path');
19
19
  const https = require('https');
20
20
  const crypto = require('crypto');
21
+ const { Transform, pipeline } = require('stream');
21
22
 
22
23
  const REPO = 'munhq/codeindex';
23
24
  const { version: VERSION } = require('../package.json');
@@ -83,6 +84,50 @@ function get(url, redirects = 0) {
83
84
  });
84
85
  }
85
86
 
87
+ // Stream an asset to `dest`, hashing the bytes as they go past. Resolves to the
88
+ // hex digest.
89
+ //
90
+ // The binary used to be collected chunk by chunk, concatenated into one 53 MB
91
+ // Buffer, hashed, and only then written — two whole copies of it live in the
92
+ // heap at the peak. This wrapper stays resident for the entire MCP session, and
93
+ // V8 does not hand those pages back, so a session that downloaded carried about
94
+ // 175 MB for as long as it lived, to supervise a server of a few megabytes.
95
+ // Nothing here needs the file in memory: the hash is incremental and the write
96
+ // is a pipe.
97
+ function download(url, dest, redirects = 0) {
98
+ return new Promise((resolve, reject) => {
99
+ if (redirects > 5) return reject(new Error(`too many redirects for ${url}`));
100
+ https
101
+ .get(url, { headers: { 'user-agent': `codeindex-npm/${VERSION}` } }, (res) => {
102
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
103
+ res.resume();
104
+ return resolve(download(res.headers.location, dest, redirects + 1));
105
+ }
106
+ if (res.statusCode !== 200) {
107
+ res.resume();
108
+ return reject(new Error(`GET ${url} -> HTTP ${res.statusCode}`));
109
+ }
110
+ const hash = crypto.createHash('sha256');
111
+ // The hash is a pass-through in the pipeline, not a `data` listener
112
+ // beside it. Adding a listener puts the response into flowing mode,
113
+ // which disables backpressure: the bytes arrive as fast as the socket
114
+ // delivers them and queue inside the write stream, so the whole 53 MB
115
+ // ends up buffered anyway and the fix fixes nothing. `pipeline` keeps
116
+ // the source paused whenever the disk is behind.
117
+ const tap = new Transform({
118
+ transform(chunk, _enc, cb) {
119
+ hash.update(chunk);
120
+ cb(null, chunk);
121
+ },
122
+ });
123
+ pipeline(res, tap, fs.createWriteStream(dest, { mode: 0o755 }), (err) =>
124
+ err ? reject(err) : resolve(hash.digest('hex'))
125
+ );
126
+ })
127
+ .on('error', reject);
128
+ });
129
+ }
130
+
86
131
  // The checksum published beside the binary is not proof on its own — whoever can
87
132
  // replace one can replace the other. It is here to catch a truncated download
88
133
  // and a mismatched tag, which are the failures that actually happen, and to make
@@ -96,16 +141,33 @@ async function verifiedDownload(asset) {
96
141
  if (!line) throw new Error(`SHA256SUMS for v${VERSION} does not list ${asset}`);
97
142
  const want = line.trim().split(/\s+/)[0];
98
143
 
99
- const body = await get(`${base}/${asset}`);
100
- const got = crypto.createHash('sha256').update(body).digest('hex');
101
- if (got !== want) throw new Error(`checksum mismatch for ${asset}: want ${want}, got ${got}`);
102
-
103
144
  const dest = cachedBinary();
104
145
  fs.mkdirSync(path.dirname(dest), { recursive: true });
105
- // Write then rename, so a killed download never leaves a half file that the
106
- // next run treats as installed.
146
+ // Download to a temp name and rename, so a killed download never leaves a
147
+ // half file that the next run treats as installed. The name carries this
148
+ // process's pid because several sessions can start at once on a cold cache,
149
+ // and one shared temp name would let them write over each other.
107
150
  const tmp = `${dest}.tmp-${process.pid}`;
108
- fs.writeFileSync(tmp, body, { mode: 0o755 });
151
+ const discard = () => {
152
+ try {
153
+ fs.unlinkSync(tmp);
154
+ } catch {}
155
+ };
156
+
157
+ let got;
158
+ try {
159
+ got = await download(`${base}/${asset}`, tmp);
160
+ } catch (err) {
161
+ discard();
162
+ throw err;
163
+ }
164
+ if (got !== want) {
165
+ discard();
166
+ throw new Error(`checksum mismatch for ${asset}: want ${want}, got ${got}`);
167
+ }
168
+ // A umask can clear the mode the stream was created with, and a binary that
169
+ // is not executable fails later with a message about the wrong thing.
170
+ fs.chmodSync(tmp, 0o755);
109
171
  fs.renameSync(tmp, dest);
110
172
  log(`installed ${dest}`);
111
173
  return dest;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@munhq/codeindex",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "mcpName": "io.github.munhq/codeindex",
5
5
  "description": "Structural code intelligence for AI coding agents over MCP: find a definition, read one function, see what calls a symbol, trace imports and judge a change's blast radius across 40+ languages \u2014 in tens of tokens instead of the ~1,563 a file read costs.",
6
6
  "keywords": [