sentinel-ir-mcp 0.5.0 → 0.5.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.
package/README.md CHANGED
@@ -28,6 +28,32 @@ of safety. Other languages are not supported yet. What is detected in Python
28
28
  and Rust (and what is not) is listed in the
29
29
  [sentinel-ir README](https://gitlab.com/jakubrojicek11/sentinel-ir#python).
30
30
 
31
+ **0.5.1** makes sure an installed server gets fixes:
32
+
33
+ - It needs `sentinel-ir` 0.5.6 or newer. 0.5.3–0.5.6 fix a comment between
34
+ two tokens hiding dangerous code, a broken Python or Rust file showing
35
+ `low`, a parser that could hang for minutes on one file, and the line of
36
+ Rust `use std::process::Command` signals.
37
+ - `setup` writes `sentinel-ir-mcp@stable-0.5`, an npm tag that points at the
38
+ newest 0.5.x. `npx` keeps starting the copy it downloaded first for a
39
+ version or range such as `@0.5` and never picks up a fix; a tag is looked
40
+ up again on every start. Measured with npm 10.8.3 and 11.21.2 against a
41
+ local registry: after the tag moved, the next start ran the new version;
42
+ with the registry unreachable (connection refused), the cached copy still
43
+ started.
44
+ - `doctor` warns about an entry that pins a version or range (`@0.5`,
45
+ `@0.5.0`, ...) and about every copy in npm's `_npx` cache whose
46
+ `sentinel-ir` is older than 0.5.6, with the folder to delete. It fails when
47
+ the server itself runs an older `sentinel-ir`.
48
+
49
+ If you set up 0.5.0 or older, run `setup` again **with the new tag** (`npx -y
50
+ sentinel-ir-mcp@0.5 ...` would start your cached 0.5.0):
51
+
52
+ ```bash
53
+ npx -y sentinel-ir-mcp@stable-0.5 setup --client cursor
54
+ npx -y sentinel-ir-mcp@stable-0.5 doctor --client cursor
55
+ ```
56
+
31
57
  **0.5.0** reads Rust (`.rs`) with `sentinel-ir` 0.5.2: `unsafe` and FFI,
32
58
  `std::process::Command`, file deletes and writes, HTTP clients and routes
33
59
  (`axum`, `actix-web`, ...). A `build.rs` or procedural macro that touches
@@ -38,7 +64,8 @@ because it runs on your machine during `cargo build`.
38
64
  Cargo's `CACHEDIR.TAG` file, listed in `skipped.cargoTarget`. A plain folder
39
65
  named `target` (e.g. in a JS project) is still scanned.
40
66
  - `setup` writes `sentinel-ir-mcp@0.5`; `doctor` asks entries pinned to 0.4
41
- or older to run `setup` again.
67
+ or older to run `setup` again (since 0.5.1 `setup` writes
68
+ `sentinel-ir-mcp@stable-0.5`, see above).
42
69
 
43
70
  **0.4.6** is listed in the official
44
71
  [MCP Registry](https://registry.modelcontextprotocol.io) as
@@ -54,7 +81,7 @@ can check later which version of a file the agent was shown (see
54
81
  - **One question in `setup`:** *"Keep a log of what the agent asks for (file
55
82
  names and results, never your code)? [y/N]"*. Or `setup --audit-log`;
56
83
  `--no-audit-log` turns it off. Running `setup` again keeps the setting.
57
- - **One command to read it:** `npx -y sentinel-ir-mcp@0.5 log` prints the last
84
+ - **One command to read it:** `npx -y sentinel-ir-mcp@stable-0.5 log` prints the last
58
85
  50 entries as plain lines, `--all` everything, `--json` for scripts:
59
86
 
60
87
  ```text
@@ -183,9 +210,9 @@ nothing to compile (the parser is WebAssembly).
183
210
  Run it in your project folder:
184
211
 
185
212
  ```bash
186
- npx -y sentinel-ir-mcp@0.5 setup --client cursor # or windsurf, claude-desktop, vscode, claude-code
187
- npx -y sentinel-ir-mcp@0.5 doctor --client cursor # after restarting the client
188
- npx -y sentinel-ir-mcp@0.5 guide # optional: text for AGENTS.md / client rules
213
+ npx -y sentinel-ir-mcp@stable-0.5 setup --client cursor # or windsurf, claude-desktop, vscode, claude-code
214
+ npx -y sentinel-ir-mcp@stable-0.5 doctor --client cursor # after restarting the client
215
+ npx -y sentinel-ir-mcp@stable-0.5 guide # optional: text for AGENTS.md / client rules
189
216
  ```
190
217
 
191
218
  `setup` (without `--client` it asks which client):
@@ -199,8 +226,8 @@ npx -y sentinel-ir-mcp@0.5 guide # optional: text for AGENTS
199
226
  touched;
200
227
  - gives the agent one folder (`--project`, default: the current folder) and
201
228
  refuses your home folder or a whole disk;
202
- - writes `sentinel-ir-mcp@0.5`, so clients get 0.5.x fixes but no unexpected
203
- new features;
229
+ - writes `sentinel-ir-mcp@stable-0.5`, an npm tag for the newest 0.5.x, so
230
+ clients get 0.5.x fixes on their next start but no unexpected new features;
204
231
  - asks which files or folders the agent must not read (`--exclude <name>`,
205
232
  repeatable, skips the question; see [Excluding files](#excluding-files));
206
233
  - for VS Code and Claude Code it writes nothing and prints their own install
@@ -214,7 +241,7 @@ npx -y sentinel-ir-mcp@0.5 guide # optional: text for AGENTS
214
241
  | `vscode`, `claude-code` | nothing; prints the command to run |
215
242
 
216
243
  On Windows the entry is `"command": "cmd", "args": ["/c", "npx", "-y",
217
- "sentinel-ir-mcp@0.5", "<folder>"]`: there `npx` is `npx.cmd`, and starting it
244
+ "sentinel-ir-mcp@stable-0.5", "<folder>"]`: there `npx` is `npx.cmd`, and starting it
218
245
  without a shell failed with `ENOENT` on a real Windows runner. For VS Code and
219
246
  Claude Code, `setup` prints a PowerShell and a Command Prompt version of the
220
247
  command. What was measured on which system: [COMPATIBILITY.md](COMPATIBILITY.md).
@@ -238,21 +265,26 @@ Open Cursor Settings → Customize → MCPs and turn on the switch next to
238
265
  *Connected* and 3 tools. A server installed with the link below (user-level
239
266
  `~/.cursor/mcp.json`) connected without this step.
240
267
 
241
- ### Getting a newer 0.2.x
242
-
243
- `npx` keeps the package it downloaded for `sentinel-ir-mcp@0.2` and keeps
244
- starting that copy, even when a newer 0.2.x is on npm (measured with npm
245
- 10.8.2, 10.8.3 and 10.9.9: a cached 0.2.2 kept running after 0.2.3 was
246
- released; `--prefer-online` and `npm cache clean --force` did not change it).
247
- To get the newest 0.2.x:
248
-
249
- 1. Check what runs: `npx -y sentinel-ir-mcp@0.2 doctor` prints
250
- `sentinel-ir-mcp <version>`.
251
- 2. Close the client app and delete the `_npx` folder in npm's cache
268
+ ### Getting a newer version
269
+
270
+ `npx` keeps the package it downloaded for a version or range
271
+ (`sentinel-ir-mcp@0.5`, `@0.2`, ...) and keeps starting that copy, together
272
+ with the `sentinel-ir` it got then, even when newer versions are on npm
273
+ (measured with npm 10.8.2, 10.8.3, 10.9.9 and 11.21.2; on npm 10
274
+ `--prefer-online` and `npm cache clean --force` did not change it). The tag
275
+ `setup` writes since 0.5.1 (`@stable-0.5`) is looked up again on every start
276
+ instead. If a client still starts an old copy:
277
+
278
+ 1. Run `npx -y sentinel-ir-mcp@stable-0.5 doctor`. It prints the versions it
279
+ runs and lists every cached copy with a `sentinel-ir` older than 0.5.6,
280
+ with its folder.
281
+ 2. Close the client app and delete those folders (npm 11: `npm cache npx rm
282
+ <key>`). Deleting the whole `_npx` folder in npm's cache also works
252
283
  (`npm config get cache` shows where it is; usually `~/.npm/_npx`, on
253
- Windows `%LocalAppData%\npm-cache\_npx`). This removes all packages cached
254
- by `npx`, not only this one; they are downloaded again when needed.
255
- 3. Start the client again. The next start downloads the newest 0.2.x.
284
+ Windows `%LocalAppData%\npm-cache\_npx`); it removes all packages cached
285
+ by `npx`, which are downloaded again when needed.
286
+ 3. Run `npx -y sentinel-ir-mcp@stable-0.5 setup` for your client and start the
287
+ client again.
256
288
 
257
289
  A folder that is literally named `setup`, `doctor` or `guide` must now be
258
290
  passed as `./setup` (or an absolute path); the bare word is the command.
@@ -274,8 +306,8 @@ VS Code 1.143 and Cursor 3.26 (see [COMPATIBILITY.md](COMPATIBILITY.md)). A link
274
306
  project path, so it uses `${workspaceFolder}`; if your client does not expand
275
307
  it, use `setup` instead.
276
308
 
277
- - Cursor: [Install in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=sentinel-ir&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInNlbnRpbmVsLWlyLW1jcEAwLjMiLCIke3dvcmtzcGFjZUZvbGRlcn0iXX0=)
278
- - VS Code: [Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22sentinel-ir%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22sentinel-ir-mcp%400.3%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D)
309
+ - Cursor: [Install in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=sentinel-ir&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInNlbnRpbmVsLWlyLW1jcEBzdGFibGUtMC41IiwiJHt3b3Jrc3BhY2VGb2xkZXJ9Il19)
310
+ - VS Code: [Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22sentinel-ir%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22sentinel-ir-mcp%40stable-0.5%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D)
279
311
 
280
312
  Some sites (including GitLab and npm) may not open `cursor://` or `vscode:`
281
313
  links; copy the link into the browser address bar or use `setup`.
@@ -294,7 +326,7 @@ Settings → Developer → Edit Config opens `claude_desktop_config.json`
294
326
  "mcpServers": {
295
327
  "sentinel-ir": {
296
328
  "command": "npx",
297
- "args": ["-y", "sentinel-ir-mcp@0.5", "/Users/you/projects/my-app"]
329
+ "args": ["-y", "sentinel-ir-mcp@stable-0.5", "/Users/you/projects/my-app"]
298
330
  }
299
331
  }
300
332
  }
@@ -313,7 +345,7 @@ project entry has to be switched on once in Cursor Settings → Customize → MC
313
345
  "mcpServers": {
314
346
  "sentinel-ir": {
315
347
  "command": "npx",
316
- "args": ["-y", "sentinel-ir-mcp@0.5", "${workspaceFolder}"]
348
+ "args": ["-y", "sentinel-ir-mcp@stable-0.5", "${workspaceFolder}"]
317
349
  }
318
350
  }
319
351
  }
@@ -329,8 +361,8 @@ with your project's absolute path.
329
361
  #### Other clients
330
362
 
331
363
  Any MCP client that starts stdio servers works with
332
- `command: "npx"`, `args: ["-y", "sentinel-ir-mcp@0.5", "<folder>"]`
333
- (on Windows: `command: "cmd"`, `args: ["/c", "npx", "-y", "sentinel-ir-mcp@0.5", "<folder>"]`).
364
+ `command: "npx"`, `args: ["-y", "sentinel-ir-mcp@stable-0.5", "<folder>"]`
365
+ (on Windows: `command: "cmd"`, `args: ["/c", "npx", "-y", "sentinel-ir-mcp@stable-0.5", "<folder>"]`).
334
366
  Without folder arguments the server uses the client's
335
367
  [MCP roots](https://modelcontextprotocol.io/specification/latest/client/roots)
336
368
  if the client offers them; if it offers none, only `analyze_code` works.
@@ -338,7 +370,7 @@ if the client offers them; if it offers none, only `analyze_code` works.
338
370
  ### Excluding files
339
371
 
340
372
  ```bash
341
- npx -y sentinel-ir-mcp@0.5 setup --client cursor --exclude secrets --exclude "*.min.js"
373
+ npx -y sentinel-ir-mcp@stable-0.5 setup --client cursor --exclude secrets --exclude "*.min.js"
342
374
  ```
343
375
 
344
376
  | you write | excludes |
@@ -363,8 +395,8 @@ other tools of its client is told about them, but this server cannot stop it.
363
395
  ### Audit log
364
396
 
365
397
  ```bash
366
- npx -y sentinel-ir-mcp@0.5 setup --client cursor --audit-log # or answer "y" in setup
367
- npx -y sentinel-ir-mcp@0.5 log # what the agent asked for
398
+ npx -y sentinel-ir-mcp@stable-0.5 setup --client cursor --audit-log # or answer "y" in setup
399
+ npx -y sentinel-ir-mcp@stable-0.5 log # what the agent asked for
368
400
  ```
369
401
 
370
402
  | system | folder |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinel-ir-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "mcpName": "io.github.jakcmencz/sentinel-ir-mcp",
5
5
  "description": "MCP server for sentinel-ir: lets AI agents (Claude Desktop, Cursor, Windsurf, ...) read a compact structural IR of JavaScript, TypeScript, Python and Rust files instead of the raw source. Read-only, sandboxed to allowed folders.",
6
6
  "main": "src/server.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/sdk": "^1.32.1",
23
- "sentinel-ir": "^0.5.2",
23
+ "sentinel-ir": "^0.5.6",
24
24
  "zod": "^4.1.0"
25
25
  },
26
26
  "keywords": [
package/src/clients.js CHANGED
@@ -3,8 +3,13 @@
3
3
  const os = require("os");
4
4
  const path = require("path");
5
5
 
6
+ const { version } = require("../package.json");
7
+
6
8
  const SERVER_NAME = "sentinel-ir";
7
- const PACKAGE_SPEC = "sentinel-ir-mcp@0.5";
9
+ // An npm dist-tag, not a range: npx keeps starting a cached copy that still
10
+ // satisfies a range such as @0.5, but resolves a tag again on every start.
11
+ const STABLE_TAG = `stable-${version.split(".").slice(0, 2).join(".")}`;
12
+ const PACKAGE_SPEC = `sentinel-ir-mcp@${STABLE_TAG}`;
8
13
  const WORKSPACE_VAR = "${workspaceFolder}";
9
14
 
10
15
  function homeOf(env) {
@@ -169,6 +174,6 @@ function installLinks() {
169
174
  }
170
175
 
171
176
  module.exports = {
172
- SERVER_NAME, PACKAGE_SPEC, WORKSPACE_VAR, CLIENTS, CLIENT_NAMES, FILE_CLIENTS,
177
+ SERVER_NAME, STABLE_TAG, PACKAGE_SPEC, WORKSPACE_VAR, CLIENTS, CLIENT_NAMES, FILE_CLIENTS,
173
178
  homeOf, npxEntry, serverEntry, excludeArgs, entryExcludes, serverFlags, entryAuditLog, isWindowsNpxWrapper, shellQuote, powershellQuote, installCommand, installLinks
174
179
  };
package/src/doctor.js CHANGED
@@ -4,8 +4,8 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
6
6
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
7
- const { version } = require("../package.json");
8
- const { SERVER_NAME, PACKAGE_SPEC, WORKSPACE_VAR, CLIENTS, FILE_CLIENTS, isWindowsNpxWrapper, entryExcludes, entryAuditLog, serverFlags } = require("./clients");
7
+ const { version, dependencies } = require("../package.json");
8
+ const { SERVER_NAME, PACKAGE_SPEC, homeOf, WORKSPACE_VAR, CLIENTS, FILE_CLIENTS, isWindowsNpxWrapper, entryExcludes, entryAuditLog, serverFlags } = require("./clients");
9
9
  const { AuditLogError, defaultAuditDir, normalizeAuditDir, assertOutsideProjects, logFiles, dayName, createAuditLog } = require("./auditLog");
10
10
  const { readConfig } = require("./setup");
11
11
  const { createExcluder, ExcludeError } = require("./exclude");
@@ -16,6 +16,8 @@ const BIN = path.join(__dirname, "..", "bin", "sentinel-ir-mcp.js");
16
16
  const EXPECTED_TOOLS = ["analyze_code", "analyze_directory", "analyze_file"];
17
17
  const VALUE_OPTIONS = new Set(["--max-file-bytes", "--max-files", "--time-budget-ms", "--exclude"]);
18
18
 
19
+ const IR_MIN = dependencies["sentinel-ir"].replace(/^[\^~>=]+/, "");
20
+
19
21
  function irVersion() {
20
22
  try {
21
23
  return require("sentinel-ir/package.json").version;
@@ -24,6 +26,87 @@ function irVersion() {
24
26
  }
25
27
  }
26
28
 
29
+ // true when version a is older than b ("unknown" or junk counts as older).
30
+ function olderThan(a, b) {
31
+ const pa = /^(\d+)\.(\d+)\.(\d+)/.exec(String(a));
32
+ if (!pa) return true;
33
+ const pb = b.split(".").map(Number);
34
+ for (let i = 0; i < 3; i++) if (Number(pa[i + 1]) !== pb[i]) return Number(pa[i + 1]) < pb[i];
35
+ return false;
36
+ }
37
+
38
+ function readVersion(file) {
39
+ try {
40
+ const v = JSON.parse(fs.readFileSync(file, "utf8")).version;
41
+ return typeof v === "string" ? v : "unknown";
42
+ } catch {
43
+ return "unknown";
44
+ }
45
+ }
46
+
47
+ // The _npx/<key> folder this copy was started from, or null.
48
+ function npxCopyDir() {
49
+ const m = new RegExp(`^(.*\\${path.sep}_npx\\${path.sep}[^\\${path.sep}]+)\\${path.sep}`).exec(__dirname);
50
+ return m ? m[1] : null;
51
+ }
52
+
53
+ // npm's cache folder: the one this copy was started from by npx, else npm's default.
54
+ function npmCacheDir(env, platform) {
55
+ const marker = `${path.sep}_npx${path.sep}`;
56
+ const i = __dirname.lastIndexOf(marker);
57
+ if (i > 0) return __dirname.slice(0, i);
58
+ if (env.npm_config_cache) return env.npm_config_cache;
59
+ if (platform === "win32") return path.join(env.LOCALAPPDATA || path.join(homeOf(env), "AppData", "Local"), "npm-cache");
60
+ return path.join(homeOf(env), ".npm");
61
+ }
62
+
63
+ // Copies of sentinel-ir-mcp that npx cached and whose sentinel-ir is older
64
+ // than this release needs. npx keeps starting such a copy for its spec.
65
+ function staleNpxCopies(cacheDir) {
66
+ const root = path.join(cacheDir, "_npx");
67
+ let keys;
68
+ try { keys = fs.readdirSync(root); } catch { return []; }
69
+ const stale = [];
70
+ for (const key of keys.sort()) {
71
+ const dir = path.join(root, key);
72
+ const mod = path.join(dir, "node_modules");
73
+ if (!fs.existsSync(path.join(mod, "sentinel-ir-mcp", "package.json"))) continue;
74
+ // npm nests sentinel-ir under the server when another package in this
75
+ // npx folder needs a different sentinel-ir at the top level.
76
+ const nested = path.join(mod, "sentinel-ir-mcp", "node_modules", "sentinel-ir", "package.json");
77
+ const ir = readVersion(fs.existsSync(nested) ? nested : path.join(mod, "sentinel-ir", "package.json"));
78
+ if (!olderThan(ir, IR_MIN)) continue;
79
+ let spec = "sentinel-ir-mcp";
80
+ try {
81
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
82
+ const named = pkg._npx && Array.isArray(pkg._npx.packages) && pkg._npx.packages.find((p) => /^sentinel-ir-mcp(@|$)/.test(p));
83
+ if (named) spec = named;
84
+ else if (pkg.dependencies && typeof pkg.dependencies["sentinel-ir-mcp"] === "string") spec = `sentinel-ir-mcp@${pkg.dependencies["sentinel-ir-mcp"]}`;
85
+ } catch { /* keep the bare name */ }
86
+ stale.push({ key, dir, spec, mcp: readVersion(path.join(mod, "sentinel-ir-mcp", "package.json")), ir });
87
+ }
88
+ return stale;
89
+ }
90
+
91
+ function checkNpxCache(env, platform, add) {
92
+ const cacheDir = npmCacheDir(env, platform);
93
+ const stale = staleNpxCopies(cacheDir);
94
+ if (stale.length === 0) {
95
+ add("ok", "npx.cache", `npx cache (${path.join(cacheDir, "_npx")}): no copy with sentinel-ir older than ${IR_MIN}`);
96
+ return;
97
+ }
98
+ for (const c of stale) {
99
+ add("warn", "npx.cache", `npx keeps starting a cached ${c.spec} (sentinel-ir-mcp ${c.mcp}, sentinel-ir ${c.ir}) from ${c.dir}; sentinel-ir older than ${IR_MIN} misses security fixes`,
100
+ `Close the client, delete that folder (npm 11: npm cache npx rm ${c.key}), then run: npx -y ${PACKAGE_SPEC} setup`);
101
+ }
102
+ }
103
+
104
+ // The version part of the configured sentinel-ir-mcp argument, "" when none.
105
+ function entrySpec(args) {
106
+ const a = args.find((x) => typeof x === "string" && /^sentinel-ir-mcp(@|$)/.test(x));
107
+ return a ? a.slice("sentinel-ir-mcp@".length) : "";
108
+ }
109
+
27
110
  // Folders the configured entry passes to the server (arguments after the package).
28
111
  function entryFolders(args) {
29
112
  const i = args.findIndex((a) => typeof a === "string" && /^sentinel-ir-mcp(@|$)/.test(a));
@@ -84,8 +167,10 @@ function checkConfig(client, file, explicit, project, add, platform, env) {
84
167
  const runs = `${label}: "${SERVER_NAME}" runs ${entry.command} ${args.join(" ")}`;
85
168
  if (entry.command === "npx" && platform === "win32") {
86
169
  add("warn", `${client}.entry`, runs, `On Windows a client may not find "npx" without a shell. Run: npx -y ${PACKAGE_SPEC} setup --client ${client}`);
87
- } else if (args.some(a => /^sentinel-ir-mcp@0\.[0-4](\.|$)/.test(a))) {
170
+ } else if (/^[\^~=v]*0\.[0-4](\.|$)/.test(entrySpec(args))) {
88
171
  add("warn", `${client}.entry`, runs, `This entry stays on an older version without Rust support. Run: npx -y ${PACKAGE_SPEC} setup --client ${client}`);
172
+ } else if (/^[\^~=v<>]*\d/.test(entrySpec(args))) {
173
+ add("warn", `${client}.entry`, runs, `npx keeps starting the copy it downloaded first for a version or range and misses later fixes. Run: npx -y ${PACKAGE_SPEC} setup --client ${client}`);
89
174
  } else if (entry.command === "npx" || isWindowsNpxWrapper(entry)) {
90
175
  add("ok", `${client}.entry`, runs);
91
176
  } else {
@@ -189,7 +274,12 @@ async function runDoctor(opts, io) {
189
274
  const nodeVersion = io.nodeVersion || process.versions.node;
190
275
  if (Number(nodeVersion.split(".")[0]) >= 20) add("ok", "node", `Node.js v${nodeVersion}`);
191
276
  else add("fail", "node", `Node.js v${nodeVersion} is too old`, "Install Node.js 20 or newer.");
192
- add("ok", "package", `sentinel-ir-mcp ${version}, sentinel-ir ${irVersion()}`);
277
+ const ir = irVersion();
278
+ if (olderThan(ir, IR_MIN)) {
279
+ add("fail", "package", `sentinel-ir-mcp ${version} runs sentinel-ir ${ir}, needs ${IR_MIN} or newer`,
280
+ npxCopyDir() ? `Close the client, delete ${npxCopyDir()}, then run: npx -y ${PACKAGE_SPEC} setup` : `Reinstall it so that npm installs sentinel-ir ${dependencies["sentinel-ir"]}.`);
281
+ } else add("ok", "package", `sentinel-ir-mcp ${version}, sentinel-ir ${ir}`);
282
+ checkNpxCache(io.env, io.platform, add);
193
283
 
194
284
  const project = path.resolve(io.cwd, opts.project || ".");
195
285
  if (opts.client && !Object.prototype.hasOwnProperty.call(CLIENTS, opts.client)) {
@@ -238,4 +328,4 @@ async function runDoctor(opts, io) {
238
328
  return failed ? 1 : 0;
239
329
  }
240
330
 
241
- module.exports = { runDoctor, entryFolders };
331
+ module.exports = { runDoctor, entryFolders, olderThan, staleNpxCopies, IR_MIN };