tool-map - persistent tool inventory
A cross-platform inventory of CLI tools, scripts, and MCP servers installed on the user's machine. Generates a persistent three-file catalog (lightweight summary, full markdown, machine JSON) so the agent can answer "do I have X?", "where is Y?", "how do I run Z?" without re-scanning the filesystem every session.
Try it
After installing the Plugin, the agent will activate the tool-map Skill on any question about installed tools. On the first session, ask the agent to read the summary, or trigger a refresh:
What CLI tools do I have installed? Where is pnpm?
Refresh my tool inventory - I just installed a new package manager.
Run the tool-map scanner, then tell me which MCP servers are on my PATH.
The first invocation generates ${PLUGIN_DATA}/tools.summary.md, ${PLUGIN_DATA}/tools.md, and ${PLUGIN_DATA}/tools.json (one node process, typically under 2 seconds). Subsequent turns read the summary without re-scanning.
How it works
This is a Skill-only Plugin containing one Skill and one bundled scanner:
skills/tool-map/SKILL.md- tells the agent to consult the cached summary on session start, refresh only when needed, and how to invoke the scanner.scripts/scan.mjs- a zero-dependency Node script that walks well-known tool roots plus$PATH, probes 15 well-known CLIs for--version, and writes the three catalog files atomically (staging + rename, no partial files).scripts/smoke.mjs- a self-check that statically scans the Plugin's own source for hardcoded absolute paths, literal credential tokens, and leftover scaffold marker strings. Exits non-zero on any violation.
Why no bundled MCP connection: this Plugin has no runtime server, no network endpoints, and no secrets to manage. The agent invokes the scanner as a regular Node subprocess when the user asks for a refresh; the Skill is the only contract.
Requirements
- Node.js >= 22 at runtime (the scanner uses only built-in modules and the
node --testdiscoverer picks up the regression test in this repository'snpm test). - The Plugin data directory, exposed as
${PLUGIN_DATA}to the agent. The scanner falls back to~/.local/share/tool-map(XDG_DATA_HOME compliant) when${PLUGIN_DATA}is unset. - A POSIX-like shell or
cmd.exefor the bundlednodeinvocation; no other binaries are required at install time.
Supported platforms
| Platform | Status | Notes |
|---|---|---|
| Windows 10 / 11 (PowerShell 5.1+ or pwsh 7) | Supported (primary) | Drives, %ProgramFiles%, %APPDATA%, %LOCALAPPDATA% resolved from environment. |
| macOS 12+ (bash / zsh) | Supported | ~/.local/bin, /usr/local/bin, /opt/homebrew/bin walked. |
| Linux x86_64 / arm64 | Supported | ~/.local/bin, ~/.local/share/npm/bin, /usr/local/bin walked. |
The scanner does not hardcode any per-user absolute path; all locations are derived from $HOME, $ProgramFiles, $APPDATA, $LOCALAPPDATA, $PATH, or fixed POSIX conventions. To add an extra root, set TOOL_MAP_ROOTS to a :-separated (POSIX) or ;-separated (Windows) list of absolute paths.
Data and network
This Plugin itself:
- Makes no network requests. The scanner is fully offline. It does not contact any registry, index, API, or third-party service.
- Ships no credentials. No API token, no OAuth client, no per-user secret, no shared key. The
~/.ssh/directory is read for filenames only (no key contents, no passphrases, no agent state). - No telemetry. The scanner prints a one-line summary to stdout when it writes a catalog; nothing is sent anywhere.
- No third-party services. No SDK, no analytics endpoint, no error reporter, no remote MCP server. The Plugin is self-contained.
- No data uploaded. The catalog lives entirely in the Plugin data directory. Nothing leaves the host.
The scanner reads (read-only):
- Filesystem metadata (size, mtime, mode) for executables under the configured roots.
- The first line of stdout for
tool --versionfor 15 well-known CLIs (node, npm, pnpm, yarn, mcode, openclaw, clawhub, codex, git, python, python3, gh, docker, pwsh, powershell). Each probe has a 5 s timeout and never throws. ~/.gitconfigfor theuser.nameanduser.emailfields (treated as public identity, displayed in the summary).- The list of filenames under
~/.ssh/that matchid_*(without.pub). File contents are never read.
The scanner writes (only):
${PLUGIN_DATA}/tools.md,${PLUGIN_DATA}/tools.json,${PLUGIN_DATA}/tools.summary.md(or whatever path is passed asargv[2]). Writes are bundle-atomic: every existing target file is first moved to a private backup directory, then the new contents are written into a staging directory, then each staging file is renamed onto its target. If any rename fails, the previous catalog is restored from backup and the staging / backup directories are removed. Seescripts/scan.mjs:atomicWriteBundleand theTOOL_MAP_FAIL_AT_RENAMEregression test for the failure-path behaviour.
Side effects
The scanner's only side effect beyond the catalog files is subprocess execution of 15 well-known CLI programs. This is a deliberate, declared behaviour — the catalog is more useful when the agent can see actual installed versions, not just file existence. To make the policy explicit:
- Whitelisted names only. The exact set of programs that may be spawned is hardcoded as
VERSION_PROBESinscripts/scan.mjsand the same set is exposed asALLOWED_PROBE_NAMES. Any future caller that would probe a name not in the whitelist is rejected insideprobeVersion(fail-closed). Adding a new probe requires editingVERSION_PROBES. - Probes are
execFile, notshell, on every platform. Each probe passes the program as a separate argv (execFileP(cmd[0], cmd.slice(1), ...)), so a same-named wrapper on$PATHcannot be tricked into running a different program by shell metacharacters in the path. - One Windows-only exception:
.cmd/.batshims must go throughcmd.exe. Since the Node.js 21.7.3 fix for CVE-2024-27980,execFilerefuses to spawn batch files withoutshell: true; this Plugin requires Node >= 22, so the CVE fix is in force. The shell decision is per-program:probeVersionwalks$PATH(and$PATHEXTon Windows) to find the actual file the OS will execute, then setsshell: trueonly for programs whose resolved path ends in.cmdor.bat. Native.exebinaries and programs whose resolved path is anything else (includingpowershell.exewith a-Commandscript passed as a separate argv) are spawned directly. POSIX always uses no shell. The full source ofshellForFile,resolveProgram, andshouldUseShellis inscripts/scan.mjs; the regression test exercises both functions. - 5-second timeout, no exceptions. Every probe runs under a hard 5 s
execFiletimeout and any error (timeout, ENOENT, non-zero exit) is swallowed. A wrapper that hangs longer than 5 s is omitted from thecoreversions table; nothing else is affected. - No arguments beyond
--version(or the single read-onlypowershell -NoProfile -Command $PSVersionTable.PSVersion.ToString()for PowerShell). The scanner never passes user input as a CLI argument.
Review your $PATH and any same-named wrappers in the well-known roots before installing this Plugin if you consider arbitrary command execution a concern. The full source of probeVersion and VERSION_PROBES is in scripts/scan.mjs.
Limitations
- The catalog is a snapshot, not live. After installing or upgrading a tool, the user (or the agent on user instruction) must re-run the scanner. The default cache is good until something changes; the agent should not assume a tool listed 30 seconds ago is still on
$PATHif acommand not foundwas reported in the same session. --versionprobes use a 5 s timeout. A tool that hangs longer than that is omitted from thecoreversions table but stays in the file-walk inventory (so the agent still knows the file exists).- The walk has a safety cap of 5000 entries; very large tool collections (e.g. a build farm with thousands of node_modules shims) are truncated. Raise
MAX_RESULTSinscripts/scan.mjsif you need more. - Files larger than 50 MB are skipped (CUDA SDKs, game engines, etc.) to keep the catalog readable.
- On POSIX, an entry is only listed if the file has at least one execute bit set (
mode & 0o111). On Windows the execute bit is ignored (per platform convention). - The scanner does not enumerate npm packages, pip packages, or system packages. It finds executables on disk, not installable artifacts.
Test evidence
Run from the repository root (this directory's parent):
$ npm run check
OK example hello-mcode
OK example hello-mcode-mcp
OK plugin Fectivnfy112357/github-explore
OK plugin hetaoBackend/minimax-code-trajectory
OK plugin HopeYin/dida365
OK plugin HopeYin/ticktick
OK plugin Hylouis233/mcp-server-patterns
OK plugin Hylouis233/search-first
OK plugin Hylouis233/verification-loop
OK plugin antianqi/tool-map
tests 7
pass 7
fail 0
$ node --test test/tool-map.test.mjs
> scan.mjs writes the three catalog files atomically (~700ms)
> scan.mjs JSON has the expected schema (~700ms)
> scan.mjs writes nothing outside the output directory (~700ms)
> scan.mjs leaves no staging files on success (~700ms)
> scan.mjs completes with an empty PATH and still produces a valid catalog (~110ms)
> smoke.mjs exits 0 against the plugin source tree (~35ms)
> atomicWriteBundle rolls back when a mid-bundle rename fails (~10ms)
> atomicWriteBundle is idempotent on the happy path (no residue, all 3 present) (~5ms)
> atomicWriteBundle rolls back when a backup-phase rename fails (early name) (~40ms)
> atomicWriteBundle rolls back when a backup-phase rename fails (later name) (~40ms)
> atomicWriteBundle rolls back brand-new files that were partially installed (~35ms)
> atomicWriteBundle happy path: previously-absent targets are created, no residue (~3ms)
> atomicWriteBundle happy path: mix of existing and absent targets (~3ms)
> ALLOWED_PROBE_NAMES is exactly the 15 declared names (<1ms)
> shellForFile is pure: false on POSIX regardless of file type (<1ms)
> shellForFile classifies Windows paths by extension (<1ms)
> resolveProgram returns null for unknown names (~6ms)
> resolveProgram finds node on the current PATH (~3ms)
> shouldUseShell agrees with shellForFile for every whitelisted probe that is installed (~115ms)
> probeVersion refuses non-whitelisted names (no shell, no spawn) (<1ms)
> POSIX: a .sh file without the execute bit is not reported as a tool (<1ms)
> POSIX: case-distinct tool names on case-sensitive filesystems are kept distinct (<1ms)
> XDG_DATA_HOME is honoured when PLUGIN_DATA is unset (~700ms)
tests 23
pass 23
fail 0
npm run check runs npm run validate (the Plugin shape validator, hardened to the rules proposed in PR #4) and then npm test (which discovers test/tool-map.test.mjs via the node --test runner). The bundled scripts/smoke.mjs exits 0 against the Plugin's own source tree, confirming no hardcoded paths, no literal credentials, and no leftover scaffold markers. The 23-case test suite covers the v0.2.0 review blockers end-to-end: bundle-level atomicity (with a deterministic mid-bundle failure path covering Phase 1 early, Phase 1 later, Phase 3 brand-new partial install, and happy paths), the 15-name whitelist, the per-program shell decision (shellForFile pure, resolveProgram PATH/PATHEXT, shouldUseShell integration), XDG_DATA_HOME precedence, execute-bit filtering, and case-sensitive dedup.
Links
- Issue tracker: https://github.com/MiniMax-AI/MiniMax-Code-Plugins/issues
- Contributing: see
CONTRIBUTING.mdin the repository root. - License: Apache-2.0. See
LICENSEin this directory.