@munhq/codeindex 0.3.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/README.md +67 -0
- package/bin/codeindex-mcp.js +37 -0
- package/bin/postinstall.js +19 -0
- package/bin/resolve.js +135 -0
- package/bin/selftest.js +63 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @munhq/codeindex
|
|
2
|
+
|
|
3
|
+
Structural code intelligence for AI coding agents, as an MCP server. It answers
|
|
4
|
+
"where is X defined", "show me this function", "what calls it" and "what breaks
|
|
5
|
+
if I change this file" in tens of tokens, instead of the ~1,563 a file read
|
|
6
|
+
costs. tree-sitter across 40+ languages, 16 MCP tools, MIT.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
npx -y @munhq/codeindex
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
No account, no API key, no configuration.
|
|
13
|
+
|
|
14
|
+
## Add it to a client
|
|
15
|
+
|
|
16
|
+
Claude Code:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
claude mcp add codeindex -- npx -y @munhq/codeindex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Anything that reads a JSON config (Claude Desktop, Cursor, Windsurf, Zed, Cline):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"codeindex": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "@munhq/codeindex"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Why this is an npm package when the server is not JavaScript
|
|
36
|
+
|
|
37
|
+
codeindex is a statically linked binary — no runtime, no node_modules, about
|
|
38
|
+
53 MB per platform. This package is a 4 KB wrapper: on install it resolves the
|
|
39
|
+
release asset for your platform, verifies it against the `SHA256SUMS` published
|
|
40
|
+
beside it, caches it under `~/.cache/codeindex/bin/codeindex-<version>`, and
|
|
41
|
+
executes it. Six platforms are covered: linux and macOS on x86_64 and arm64,
|
|
42
|
+
Windows on x86_64 and arm64.
|
|
43
|
+
|
|
44
|
+
The cache key carries the version on purpose. An unversioned cache silently keeps
|
|
45
|
+
running an old binary after an upgrade, which is a bug this project has already
|
|
46
|
+
paid for once.
|
|
47
|
+
|
|
48
|
+
`CODEINDEX_BIN` overrides everything, for a local build. `PATH` is deliberately
|
|
49
|
+
*not* searched: this package declares one version to the MCP registry, and
|
|
50
|
+
running whatever `codeindex` happens to be on `PATH` would make that a lie.
|
|
51
|
+
|
|
52
|
+
## What the tools cost
|
|
53
|
+
|
|
54
|
+
| tool | tokens per call |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `Read` (for comparison) | 1,563 |
|
|
57
|
+
| `get_outline` | 113 |
|
|
58
|
+
| `find_symbol` | 102 |
|
|
59
|
+
| `search` | 79 |
|
|
60
|
+
| `find_word` | 41 |
|
|
61
|
+
| `read_symbol` | **35** |
|
|
62
|
+
|
|
63
|
+
## Everything else
|
|
64
|
+
|
|
65
|
+
Source, the other install paths (`curl \| bash`, a Claude Code plugin with the
|
|
66
|
+
skill, a prebuilt binary), the 16 tools and the 16 analyses:
|
|
67
|
+
**https://github.com/munhq/codeindex**
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The MCP server entry point: resolve the binary, then become it.
|
|
3
|
+
//
|
|
4
|
+
// `npx -y @munhq/codeindex` lands here. Every argument is passed straight
|
|
5
|
+
// through, so `--workspace`, `--mcp` and anything the server grows later work
|
|
6
|
+
// without this file knowing about them.
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
const { resolveBinary, log } = require('./resolve.js');
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
let bin;
|
|
14
|
+
try {
|
|
15
|
+
bin = await resolveBinary();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
log(`could not start: ${err.message}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The client speaks JSON-RPC over this process's stdio, so the child inherits
|
|
22
|
+
// all three streams untouched. No pipe, no buffering, no wrapper protocol.
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
// `--mcp` is what the server needs to speak MCP at all. A client that
|
|
25
|
+
// launches the bin with no arguments gets the CLI's help text on stdout and a
|
|
26
|
+
// handshake that never completes, which reads as "the server is broken".
|
|
27
|
+
if (!args.includes('--mcp')) args.unshift('--mcp');
|
|
28
|
+
|
|
29
|
+
const res = spawnSync(bin, args, { stdio: 'inherit' });
|
|
30
|
+
if (res.error) {
|
|
31
|
+
log(`failed to exec ${bin}: ${res.error.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
// Relay the child's fate exactly: a signal death must not look like exit 0.
|
|
35
|
+
if (res.signal) process.kill(process.pid, res.signal);
|
|
36
|
+
process.exit(res.status === null ? 1 : res.status);
|
|
37
|
+
})();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fetch the binary at install time, so the first MCP handshake does not wait for
|
|
3
|
+
// a 53 MB download and time out inside the client.
|
|
4
|
+
//
|
|
5
|
+
// This is allowed to fail. A CI sandbox with no network, or `npm install
|
|
6
|
+
// --ignore-scripts`, must still produce a working install — codeindex-mcp.js
|
|
7
|
+
// downloads on demand when the cache is empty. So the exit code is always 0 and
|
|
8
|
+
// the reason goes to stderr, where it is visible rather than swallowed.
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { resolveBinary, log } = require('./resolve.js');
|
|
12
|
+
|
|
13
|
+
resolveBinary()
|
|
14
|
+
.then(() => process.exit(0))
|
|
15
|
+
.catch((err) => {
|
|
16
|
+
log(`prefetch skipped: ${err.message}`);
|
|
17
|
+
log('the binary will be fetched on first run instead');
|
|
18
|
+
process.exit(0);
|
|
19
|
+
});
|
package/bin/resolve.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Resolve a codeindex binary for this platform, downloading the pinned release
|
|
2
|
+
// asset when the cache does not already hold it.
|
|
3
|
+
//
|
|
4
|
+
// Why an npm package at all, for a program that is not JavaScript: every MCP
|
|
5
|
+
// directory installs servers the way npm installs them. The official registry
|
|
6
|
+
// validates an npm version, Smithery runs `npx`, and mcp.so lists the same
|
|
7
|
+
// command. A compiled server with no npm name is invisible to all three, so this
|
|
8
|
+
// package is the thin shim that makes `npx -y @munhq/codeindex` work — about
|
|
9
|
+
// 4 KB of JavaScript that fetches the 53 MB binary the release already publishes.
|
|
10
|
+
//
|
|
11
|
+
// Write NOTHING to stdout. Stdout is the MCP JSON-RPC channel; one stray line
|
|
12
|
+
// there makes the server look broken with no error that explains why. Every
|
|
13
|
+
// diagnostic goes to stderr, which the client logs.
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const https = require('https');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
|
|
22
|
+
const REPO = 'munhq/codeindex';
|
|
23
|
+
const { version: VERSION } = require('../package.json');
|
|
24
|
+
|
|
25
|
+
const log = (msg) => process.stderr.write(`codeindex: ${msg}\n`);
|
|
26
|
+
|
|
27
|
+
// Map Node's platform/arch onto the asset names the release actually publishes.
|
|
28
|
+
// The release matrix builds Zig target triples — x86_64-linux, aarch64-linux,
|
|
29
|
+
// x86_64-macos, aarch64-macos, x86_64-windows, aarch64-windows — and Node agrees
|
|
30
|
+
// with none of them: process.platform says darwin/win32, process.arch says x64.
|
|
31
|
+
// The shell launcher had this exact bug against uname and every Mac 404'd, so
|
|
32
|
+
// the mapping is spelled out rather than assembled from the raw values.
|
|
33
|
+
// Pure, so plugin/test_platform.sh can drive it for every platform this package
|
|
34
|
+
// claims to support instead of only the one the test runs on. That test already
|
|
35
|
+
// holds install.sh and the shell launcher to the release matrix; this is the
|
|
36
|
+
// third consumer of the same naming and it drifts the same way.
|
|
37
|
+
function assetFor(platform, arch) {
|
|
38
|
+
const a = { x64: 'x86_64', arm64: 'aarch64' }[arch];
|
|
39
|
+
const p = { linux: 'linux', darwin: 'macos', win32: 'windows' }[platform];
|
|
40
|
+
if (!a || !p) return null;
|
|
41
|
+
return `codeindex-${a}-${p}${p === 'windows' ? '.exe' : ''}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assetName() {
|
|
45
|
+
return assetFor(process.platform, process.arch);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cacheDir() {
|
|
49
|
+
const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
50
|
+
return path.join(base, 'codeindex', 'bin');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The cached file carries the version. The shell launcher caches an unversioned
|
|
54
|
+
// `codeindex`, and this repo has already been bitten by that shape: a 0.3.0
|
|
55
|
+
// plugin ran a 0.2.0 binary found first on PATH for days, with both halves
|
|
56
|
+
// reporting their own version and nobody comparing them. A version in the name
|
|
57
|
+
// makes an upgrade a cache miss instead of a silent downgrade.
|
|
58
|
+
function cachedBinary() {
|
|
59
|
+
const exe = process.platform === 'win32' ? '.exe' : '';
|
|
60
|
+
return path.join(cacheDir(), `codeindex-${VERSION}${exe}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function get(url, redirects = 0) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
if (redirects > 5) return reject(new Error(`too many redirects for ${url}`));
|
|
66
|
+
https
|
|
67
|
+
.get(url, { headers: { 'user-agent': `codeindex-npm/${VERSION}` } }, (res) => {
|
|
68
|
+
// GitHub serves release assets as a redirect to object storage.
|
|
69
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
70
|
+
res.resume();
|
|
71
|
+
return resolve(get(res.headers.location, redirects + 1));
|
|
72
|
+
}
|
|
73
|
+
if (res.statusCode !== 200) {
|
|
74
|
+
res.resume();
|
|
75
|
+
return reject(new Error(`GET ${url} -> HTTP ${res.statusCode}`));
|
|
76
|
+
}
|
|
77
|
+
const chunks = [];
|
|
78
|
+
res.on('data', (c) => chunks.push(c));
|
|
79
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
80
|
+
res.on('error', reject);
|
|
81
|
+
})
|
|
82
|
+
.on('error', reject);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The checksum published beside the binary is not proof on its own — whoever can
|
|
87
|
+
// replace one can replace the other. It is here to catch a truncated download
|
|
88
|
+
// and a mismatched tag, which are the failures that actually happen, and to make
|
|
89
|
+
// the pin auditable: both files come from the tag this package version names.
|
|
90
|
+
async function verifiedDownload(asset) {
|
|
91
|
+
const base = `https://github.com/${REPO}/releases/download/v${VERSION}`;
|
|
92
|
+
log(`fetching ${asset} for v${VERSION} (about 53 MB, once per version)`);
|
|
93
|
+
|
|
94
|
+
const sums = (await get(`${base}/SHA256SUMS`)).toString('utf8');
|
|
95
|
+
const line = sums.split('\n').find((l) => l.trim().endsWith(` ${asset}`));
|
|
96
|
+
if (!line) throw new Error(`SHA256SUMS for v${VERSION} does not list ${asset}`);
|
|
97
|
+
const want = line.trim().split(/\s+/)[0];
|
|
98
|
+
|
|
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
|
+
const dest = cachedBinary();
|
|
104
|
+
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.
|
|
107
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
108
|
+
fs.writeFileSync(tmp, body, { mode: 0o755 });
|
|
109
|
+
fs.renameSync(tmp, dest);
|
|
110
|
+
log(`installed ${dest}`);
|
|
111
|
+
return dest;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// An explicit override wins, for a local build or an unusual layout. PATH is
|
|
115
|
+
// deliberately NOT consulted: this package declares one version to the registry,
|
|
116
|
+
// and running whatever `codeindex` happens to be on PATH would make that
|
|
117
|
+
// declaration a lie.
|
|
118
|
+
async function resolveBinary() {
|
|
119
|
+
const override = process.env.CODEINDEX_BIN;
|
|
120
|
+
if (override && fs.existsSync(override)) return override;
|
|
121
|
+
|
|
122
|
+
const cached = cachedBinary();
|
|
123
|
+
if (fs.existsSync(cached)) return cached;
|
|
124
|
+
|
|
125
|
+
const asset = assetName();
|
|
126
|
+
if (!asset) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`no release build for ${process.platform}/${process.arch}. ` +
|
|
129
|
+
`Build from source: https://github.com/${REPO}#build-from-source`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return verifiedDownload(asset);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { resolveBinary, cachedBinary, assetName, assetFor, VERSION, log };
|
package/bin/selftest.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Print the asset this wrapper would fetch on every platform it claims to
|
|
3
|
+
// support, one per line, as `platform<TAB>arch<TAB>asset`.
|
|
4
|
+
//
|
|
5
|
+
// Two consumers: `npm test`, which checks the table is complete and that the
|
|
6
|
+
// package version looks like a release tag, and plugin/test_platform.sh, which
|
|
7
|
+
// compares these names against the release matrix in
|
|
8
|
+
// .github/workflows/release.yml. The shell launcher and install.sh are already
|
|
9
|
+
// held to that matrix; this wrapper is the third place the same mapping is
|
|
10
|
+
// written, and the Mac 404 it caused once is the reason all three are tested.
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { assetFor, VERSION } = require('./resolve.js');
|
|
14
|
+
|
|
15
|
+
const SUPPORTED = [
|
|
16
|
+
['linux', 'x64'],
|
|
17
|
+
['linux', 'arm64'],
|
|
18
|
+
['darwin', 'x64'],
|
|
19
|
+
['darwin', 'arm64'],
|
|
20
|
+
['win32', 'x64'],
|
|
21
|
+
['win32', 'arm64'],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Platforms with no release build must resolve to nothing rather than to a
|
|
25
|
+
// plausible-looking name that 404s at download time.
|
|
26
|
+
const UNSUPPORTED = [
|
|
27
|
+
['freebsd', 'x64'],
|
|
28
|
+
['linux', 'ia32'],
|
|
29
|
+
['sunos', 'x64'],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let fail = 0;
|
|
33
|
+
|
|
34
|
+
for (const [platform, arch] of SUPPORTED) {
|
|
35
|
+
const asset = assetFor(platform, arch);
|
|
36
|
+
if (!asset) {
|
|
37
|
+
process.stderr.write(`FAIL ${platform}/${arch} resolves to nothing\n`);
|
|
38
|
+
fail++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
process.stdout.write(`${platform}\t${arch}\t${asset}\n`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const [platform, arch] of UNSUPPORTED) {
|
|
45
|
+
const asset = assetFor(platform, arch);
|
|
46
|
+
if (asset) {
|
|
47
|
+
process.stderr.write(`FAIL ${platform}/${arch} resolves to ${asset}, but no such build exists\n`);
|
|
48
|
+
fail++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The wrapper downloads from the tag named by its own version, so a version that
|
|
53
|
+
// is not a plain release number can never find an asset.
|
|
54
|
+
if (!/^\d+\.\d+\.\d+$/.test(VERSION)) {
|
|
55
|
+
process.stderr.write(`FAIL version ${VERSION} is not a release version, so v${VERSION} is not a tag\n`);
|
|
56
|
+
fail++;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (fail) {
|
|
60
|
+
process.stderr.write(`\n${fail} problem(s)\n`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
process.stderr.write(`resolver: ${SUPPORTED.length} platforms, version ${VERSION}\n`);
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@munhq/codeindex",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"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.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"code-intelligence",
|
|
10
|
+
"tree-sitter",
|
|
11
|
+
"ai-agents",
|
|
12
|
+
"claude",
|
|
13
|
+
"context-window"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/munhq/codeindex",
|
|
16
|
+
"bugs": "https://github.com/munhq/codeindex/issues",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/munhq/codeindex.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bin": {
|
|
23
|
+
"codeindex-mcp": "bin/codeindex-mcp.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"postinstall": "node bin/postinstall.js",
|
|
27
|
+
"test": "node bin/selftest.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"bin/",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|