@munhq/codeindex 0.3.5 → 0.4.0
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/bin/codeindex-mcp.js +21 -0
- package/bin/resolve.js +69 -7
- package/package.json +1 -1
package/bin/codeindex-mcp.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
+
"version": "0.4.0",
|
|
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": [
|