npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

infolang

v1.1.0

Published

Install and manage the InfoLang MCP server across every AI coding client

Readme

infolang

One CLI to install and manage the InfoLang MCP server across every AI coding client you use.

npx infolang install

Replaces @infolang/claude-setup, @infolang/cursor-setup, and @infolang/mcp-setup.

  • Node ≥ 22.14.0
  • ESM-only, single bundled artifact (dist/cli.js)

Commands

| Command | What it does | | --- | --- | | infolang install | Add the MCP server to one or more clients | | infolang uninstall | Remove entries this CLI installed (and only those) | | infolang status | Detect clients, read the manifest, report per-client state | | infolang tools | Live tools/list against the configured endpoint | | infolang doctor | Diagnose every client; find and migrate artifacts from the retired setup packages | | infolang generate | Regenerate the plugin's tool-derived artifacts from a live tools/list | | infolang login | Sign in via browser (PKCE loopback) or an API key | | infolang logout | Clear credentials from every storage tier | | infolang whoami | Verify the stored credential against the API |

install

infolang install [--client <id>...] [--scope user|project] [--api-key]
                 [--name <server-name>] [--dry-run] [--json] [--yes] [--no-plugin]

Run bare, it detects your installed clients, pre-checks them in a multiselect, confirms, then applies with a per-client spinner and a status table. --yes with --client gives you a non-interactive run for scripts and CI.

--dry-run prints the exact diff — including the JSON/TOML that would be written — and touches nothing.

The process exits non-zero if any targeted client failed.

When Claude Code is among the targets, install also drops the bundled plugin (memory skill + session hooks + its own MCP server entry) into ~/.claude/skills/infolang/, where Claude Code loads it as infolang@skills-dir on the next session. --no-plugin skips it. See The Claude Code plugin for why that path and not claude plugin install.

uninstall

infolang uninstall [--client <id>...] [--all] [--dry-run] [--force]

Driven entirely by the install manifest. Before removing an entry it re-hashes the value currently on disk; if you edited it, the entry is skipped with a warning rather than silently discarded. --force overrides.

That tamper check applies only to entries the CLI wrote itself. When an entry was written through a client's own CLI (claude mcp add-json), that client owns the stored representation and may normalize it, so a hash mismatch is expected rather than evidence of an edit — those entries are removed without --force, preferring claude mcp remove and falling back to a direct edit. The manifest records which case an entry is.


Supported clients and their traps

Every one of these is a real failure mode that produces a config the client accepts and then quietly ignores. Each is encoded in the adapter and covered by a unit test.

| Client | Config file | Top-level key | Entry shape | The trap | | --- | --- | --- | --- | --- | | claude-code | ~/.claude.json (user) · .mcp.json (project) | mcpServers | {"type":"http","url":…,"headers":…} | type is mandatory. A remote entry with url and no type is silently skipped. Also: reserved names (workspace, claude-in-chrome, computer-use, Claude Preview, Claude Browser) are refused, and names must match [A-Za-z0-9_-]+. | | claude-desktop | ~/Library/Application Support/Claude/claude_desktop_config.json (mac) · %APPDATA%\Claude\… (win) · ~/.config/Claude/… (linux) | mcpServers | mcp-remote stdio bridge | Never write a bare url entry. Desktop validates mcpServers as stdio-only and strips or breaks on a remote entry. We write an npx -y mcp-remote … bridge and point you at Settings → Connectors, which is the first-class remote path. | | cursor | ~/.cursor/mcp.json (user) · .cursor/mcp.json (project) | mcpServers | {"url":…,"headers":…} | Transport is inferred from url; adding a type field is not the Cursor schema. | | vscode | .vscode/mcp.json (project) · ~/Library/Application Support/Code/User/mcp.json (user) | servers | {"type":"http","url":…} | The key is servers, not mcpServers. Writing mcpServers produces a file VS Code loads without complaint and ignores entirely. | | windsurf | ~/.codeium/windsurf/mcp_config.json | mcpServers | {"serverUrl":…,"headers":…} | Windsurf accepts serverUrl or url, but serverUrl is the documented key honoured by every version. We always write serverUrl. | | gemini-cli | ~/.gemini/settings.json (user) · .gemini/settings.json (project) | mcpServers | {"httpUrl":…,"headers":…} | url means SSE; httpUrl means streamable HTTP. Writing url gets you an SSE handshake that never completes. | | codex | $CODEX_HOME/config.toml (default ~/.codex/config.toml) · .codex/config.toml | [mcp_servers.<name>] | url + bearer_token_env_var | TOML edits are surgical: only InfoLang's own table is spliced. A parse-and-reserialize round trip would silently reformat your model settings, profiles, and comments. A literal token is never written — Codex resolves bearer_token_env_var at run time. |

Preferring the client's own CLI

When claude or code is on PATH, install delegates to claude mcp add-json / code --add-mcp so the entry goes through the client's own schema validation. Spawned with an argv array, never a shell string. Set INFOLANG_NO_CLIENT_CLI=1 to force the direct-file-edit path.


Two token domains

This is the part that most MCP installers get wrong, so it is worth being explicit.

  1. The CLI's own credential. Used for infolang whoami, infolang tools, and verifying a key at login. Stored on your machine, never written into a client config. A browser login mints an il_oauth_ bearer scoped to memories:read memories:write; --api-key stores an il_live_/il_test_ key. Either way it is verified against GET /v2/whoami before being stored.
  2. The client's credential. Client configs receive env-var indirection only — Bearer ${INFOLANG_API_KEY} for JSON clients, bearer_token_env_var for Codex — or the client's own OAuth (e.g. claude mcp login).

A literal il_live_… value never reaches a client config, which means a project-scope .mcp.json, .cursor/mcp.json, .vscode/mcp.json, .gemini/settings.json, or .codex/config.toml is safe to commit. The install path refuses to write a config containing an il_live_ literal.

Export the key in whatever environment starts your editor:

export INFOLANG_API_KEY=il_live_…

If the variable is unset, the client fails loudly at config-parse or first call — which is the intended behaviour. It is much better than a silent 401 an hour later.

Credential storage precedence

| Tier | Notes | | --- | --- | | INFOLANG_API_KEY env var | Read, never persisted. Your session, your business. | | OS keychain | Via @napi-rs/keyring, service infolang. (keytar is unmaintained and deliberately unused.) | | ~/.config/infolang/credentials.json | Fallback only. Mode 0600 inside a 0700 directory, with a visible warning at write time. |

infolang logout clears every tier and tells you if INFOLANG_API_KEY is still exported in the current shell.


Authentication

Browser (default)

Standard OAuth 2.1 authorization code + PKCE against the gateway's own authorization server. Nothing about the endpoints is compiled in — they come from discovery:

  1. GET {INFOLANG_URL}/.well-known/oauth-authorization-server (RFC 8414) → endpoints.
  2. Bind the loopback listener first, so the exact redirect URI http://127.0.0.1:<port>/callback is known.
  3. POST {registration_endpoint} (RFC 7591 dynamic client registration) with that exact URI, as a public client (token_endpoint_auth_method: "none").
  4. Browser → {authorization_endpoint} with client_id, redirect_uri, state, S256 code_challenge, and scope=memories:read memories:write.
  5. The gateway routes the user through the console's consent screen and back to the loopback with code + state.
  6. POST {token_endpoint} with grant_type=authorization_code and the code_verifier → an il_oauth_ bearer.

Why registration happens on every login: the gateway validates redirect_uri by exact string match against what was registered. Registering fresh, after the port is bound, means the ephemeral port is baked into the registration — so there is no port to pre-agree on and none to get wrong. --port remains for corporate setups that need a pinned, firewalled port.

Hardening:

  • S256 only. plain has no code path, and the browser flow refuses a server that does not advertise S256. (The check runs when that flow starts, not at discovery — a device-only deployment is legitimate and must not be rejected before we look at what we need.)
  • state verified with a constant-time comparison.
  • Listener bound to the 127.0.0.1 IP literal — never localhost (which can resolve to ::1 or worse) and never 0.0.0.0.
  • Ephemeral port by default, with a fixed fallback range 51735–51739.
  • One-shot listener: it stops accepting the moment a valid callback lands. A stray tab replaying the wrong state gets a 400 and does not tear the flow down.
  • The callback URL — the one carrying the authorization code — is never printed or logged.
  • Every discovered endpoint must share the deployment's origin, so a misconfigured or tampered metadata document cannot redirect the authorization code elsewhere.

API key

infolang login --api-key            # masked prompt
echo "$KEY" | infolang login --api-key   # piped

Keys must start with il_live_ or il_test_, and are verified against the API before being stored. --key <value> still exists for backwards compatibility but prints a deprecation warning — an argv value lands in your shell history.

Device code (headless machines)

infolang login --device    # or just `infolang login` on a headless box

Chosen automatically when the session looks headless (no DISPLAY/WAYLAND_DISPLAY on Linux, SSH_CONNECTION set, CI, or a container marker), and forceable anywhere with --device — useful in a remote tmux, on a machine whose default browser is broken, or when you would simply rather approve on your phone.

The CLI prints a URL and an 8-character code, then polls until you approve:

  Open  https://app.infolang.ai/activate
  Code  BCDF-GHJK

  ! Only continue if you just ran `infolang login` yourself.
    Never enter a code someone sent you.

It tries to open a browser as a convenience, but never depends on one — the printed instructions are the primary path, because the whole premise is a machine that may not be able to open anything.

This replaces the old advice to paste an API key on headless machines, which was the exact trade the OAuth lane exists to avoid: an il_live_ key never expires and carries your whole grant set, where a device login yields a scoped 90-day token.

Read the warning. Device-code phishing needs no vulnerability: an attacker runs this command on their machine, gets a genuine code, and talks you into entering it. Every URL you see is real, and the token lands on their box carrying your authority. The console shows you the same code so you can check it matches what your terminal printed — if it does not, or if you did not start this yourself, deny it.

RFC 8628 details, for anyone auditing:

  • The device_authorization_endpoint is taken from the discovery document and never guessed. A server that does not advertise it gets a clear "this deployment has no device flow" rather than a 404 halfway through a login.
  • Registration asks for the device grant and sends no redirect_uri — this flow has no redirection, and a loopback address registered but never listened on would be a real destination a real authorization code could go to.
  • Polling honours the server's interval and adds 5 seconds on every slow_down (§3.5). All five poll outcomes arrive as HTTP 400, so "non-2xx means failure" would abort on the first poll of every successful login.
  • The device_code is a bearer secret: it is never printed, never logged, and never put in an error message. Only the user_code and the URL are displayed.

Write discipline

Everything below is enforced by unit tests.

  • plan/apply split. plan() is pure: it reads the world, computes the resulting bytes, and writes nothing. apply() is the only code path that touches disk.
  • jsonc-parser edit API. Comments, key order, and unrelated formatting survive. Existing indentation (2-space, 4-space, tabs) and line endings (LF/CRLF) are detected and matched.
  • Atomic writes. Same-directory temp file → fsync → rename. A crash mid-write cannot leave a truncated config.
  • Timestamped backups. <file>.infolang-backup-<ISO8601> before the first modification of any file in a run, with the path printed. A no-op run takes no backup.
  • Verify after write. The file is re-read, re-parsed, and the entry confirmed present and matching. A failed verification is a failed install.
  • An unparseable existing config is a hard error. It is reported per-client and the file is left byte-for-byte alone. Treating a broken config as empty and overwriting it is install-mcp's bug and is explicitly banned here.
  • Idempotent. Running install twice produces byte-identical files with unchanged mtimes.

Install manifest

~/.config/infolang/installs.json (respects XDG_CONFIG_HOME):

{
  "schemaVersion": 1,
  "installs": [
    {
      "client": "cursor",
      "scope": "user",
      "path": "/Users/you/.cursor/mcp.json",
      "configKey": "mcpServers",
      "serverName": "infolang",
      "writtenValueHash": "<sha256 of the normalized JSON value>",
      "backupPath": null,
      "cliVersion": "1.0.0",
      "installedAt": "2026-07-27T12:00:00.000Z",
      "installMethod": "npx",
      "delegated": false
    }
  ]
}

Live introspection

infolang tools performs a real MCP initialize + tools/list against the configured endpoint (streamable HTTP JSON-RPC; both JSON and SSE responses are handled). There is no hardcoded list of tool names anywhere in src/ — a test asserts it. Anything the CLI says about server capabilities comes from the live response, so a server-side change shows up without a CLI release.

infolang tools              # table
infolang tools --json       # full schemas
infolang tools --markdown   # generated rules/instructions block

The markdown generator lives in src/generate/ and is the seam the plugin's SKILL.md tool-reference block and hooks.json are rendered from.


The Claude Code plugin

plugin/ ships inside the npm package and is a complete Claude Code plugin:

plugin/
├── .claude-plugin/plugin.json   # ONLY plugin.json may live here
├── skills/memory/SKILL.md       # when to recall, when to remember, what never to store
├── hooks/hooks.json             # SessionStart (recall + drain) / PreCompact / Stop
├── hooks/memory-hook.mjs        # zero-dependency shim for the two write hooks
└── .mcp.json                    # the bundled MCP server

.mcp.json uses Claude Code's environment expansion, so no secret is ever committed:

{
  "mcpServers": {
    "infolang": {
      "type": "http",
      "url": "${INFOLANG_URL:-https://api.infolang.ai}/v1/mcp/",
      "headers": { "Authorization": "Bearer ${INFOLANG_API_KEY}" }
    }
  }
}

The naming trap

Tools from a plugin-bundled server carry both the plugin name and the server key:

mcp__plugin_infolang_infolang__<tool>      ← callable name, matchers, allowed-tools
plugin:infolang:infolang                   ← an mcp_tool hook's `server` field

A matcher or permission rule written against the bare mcp__infolang__* never fires. The generated tool-reference block spells out the scoped name for every tool, and a test asserts nothing outside that block names a tool at all.

Hooks

| Event | Matcher | Handler | Why | | --- | --- | --- | --- | | SessionStart | startup\|resume | mcp_tool | Recall for the current workspace, injected as opening context. SessionStart accepts only command and mcp_tool, and an mcp_tool result's text is treated like command stdout — so no process is spawned. | | SessionStart | startup\|resume | command, sync | Drain coordination events that landed while the session was away. Carries a local read cursor, which a stateless tool call cannot, and must be synchronous: an async hook's stdout is discarded rather than fed back. | | PreCompact | auto\|manual | command, async | Flush before compaction discards the conversation. An mcp_tool hook cannot carry the session's facts — its input is the hook envelope — so the shim reads transcript_path from stdin. | | Stop | (none) | command, async | Persist the session outcome. Stop takes no matcher; one written here would never fire. |

The two write hooks have a 5s timeout and are async: true; the drain has a 3s timeout over a 2s internal budget, because it is the one hook the user waits on. memory-hook.mjs exits 0 on every failure path — no credential, no network, a 500, a malformed transcript. A hook must never nag or block a turn.

UserPromptSubmit and PostToolUse are deliberately absent: they fire on every prompt and every edit, and the latency shows. Opt-in future, not a default.

The write hooks record session markers — where the session ran and what the user asked for, with credential-shaped strings redacted — not transcripts. A process with no model cannot judge what is durable; capturing decisions is the skill's job. Stop is rate limited to one write per 15 minutes per session. Opt out with INFOLANG_HOOKS=off (everything) or INFOLANG_HOOK_CAPTURE=off (writes only).

The events drain

--mode drain reads GET /coord/v1/events on the configured origin with the same bearer the MCP server uses, and summarizes what changed into additionalContext. REST rather than a tool call for three reasons: a tools/call costs three sequential round trips before the first byte and does not fit a 2s budget; naming a tool here would hardcode a server-owned name into a shipped client, which is the defect this package exists to not repeat; and a 404 on /coord/v1 is an unambiguous "this deployment has no coordination layer".

Positions live in ~/.config/infolang/events-cursor.json (mode 0600), keyed by endpoint

  • credential fingerprint + project directory — the credential is hashed, never stored. The directory is in the key so two checkouts get their own "while you were gone"; the credential is in the key because a cursor is a coordinate in one workspace's log.

The first run reports nothing. With no stored position, starting from the beginning of the log would open the session by reciting every coordination event the workspace ever recorded. So a first run pages to the tail, records where the tail was, and stays silent; only the second run onward has an interval that honestly qualifies as "while you were gone". A log too large to traverse in one budget keeps advancing silently across sessions until it catches up, which is why the stored position carries an initialized flag separate from the cursor itself.

The digest is deduplicated, ordered so lapses and supersessions outrank routine traffic, and capped at 20 lines plus a remainder count. The position is persisted before the report is printed, and nothing is printed if that write failed: at-most-once, because a lost digest is a non-event whereas an unadvanceable cursor would re-announce the same events forever. Every failure is silent — no credential, no coordination layer, a refused credential, a 5xx, an unreachable host, a timeout, or a cursor the server rejects (which re-initializes at the tail rather than failing forever). Opt out with INFOLANG_HOOK_EVENTS=off.

Why not claude plugin install

claude plugin install <plugin> resolves a name against configured marketplaces; there is no local-path form (--help offers only --config and --scope). The supported local route is a skills directory: any folder under one carrying .claude-plugin/plugin.json loads as <name>@skills-dir next session, with no marketplace and no install step. So infolang install --client claude-code copies the tree to ~/.claude/skills/infolang/ and records it in the manifest, which is what lets infolang uninstall remove it again.

Generation and drift

The tool-reference block in SKILL.md and the tool names in hooks.json are generated, never hand-written:

infolang generate all                     # regenerate from a live tools/list
infolang generate skill --check           # report drift, exit non-zero, write nothing
infolang generate all --from tools.json   # render from `infolang tools --json` output

The block is a function of the tool surface and nothing else — no endpoint, no server version — so "drifted" means "the tools changed", and a self-hosted deployment is not permanently flagged.

Hook tool names are resolved by role, not looked up in a table: the generator scores every advertised tool against generic verb hints and refuses to emit anything when no tool clearly plays a role, or when two tie. A server-side rename becomes a loud regeneration rather than a hook that silently calls nothing.

infolang doctor compares the committed artifacts against the live surface and reports drift; --fix regenerates them.


Claude Desktop: the .mcpb bundle

npm run build:mcpb    # → dist/infolang.mcpb

The bundle is the preferred route for Claude Desktop: it prompts for the API key and stores it in the OS keychain, so no environment variable and no config-file secret are involved. Install it by double-clicking, or via Settings → Extensions → Advanced settings.

manifest.json declares a user_config.api_key field marked sensitive, injected into the server's environment as ${user_config.api_key} — never into args, because process arguments are world-readable. The bundled server/launch.mjs spawns mcp-remote as the stdio bridge and passes the header by env reference (Authorization:${AUTH_HEADER}), which is also mcp-remote's own workaround for Windows/Cursor mangling spaces inside args.

scripts/build-mcpb.mjs writes the zip directly with node:zlib rather than shelling out to @anthropic-ai/mcpb, so the release build is hermetic and byte-deterministic. The output still unpacks and validates with the official tooling.


doctor

infolang doctor           # dry run by construction — reads, never writes
infolang doctor --fix     # migrate what can be migrated
infolang doctor --json

Per client it reports whether the config parses, whether the entry is present / modified / missing, whether the endpoint is reachable, whether the credential verifies, and whether the generated artifacts have drifted from the live tool surface.

It also finds what the three retired packages left behind, recognising the shapes from their own sources:

| Artifact | Where | Recognised by | | --- | --- | --- | | Server entry with a literal key | ~/.claude.json, ~/.cursor/mcp.json, .mcp.json | il_live_ / il_test_ / il_oauth_ in the entry | | Server entry using the retired stdio gateway | same | mcp-gateway / mcp-proxy / setup-core in command/args | | claude-setup hooks | ~/.claude/settings.json, .claude/settings.json | a hook command naming claude-hook.mjs | | claude-setup managed block | ~/.claude/CLAUDE.md, ./CLAUDE.md | <!-- infolang:begin --> … <!-- infolang:end --> | | cursor-setup hooks | ~/.cursor/hooks.json, .cursor/hooks.json | [infolang-cursor-setup] in a prompt | | cursor-setup rules | ~/.cursor/rules/, .cursor/rules/ | infolang-memory.mdc, infolang-output-brevity.mdc |

--fix replaces a legacy server entry by running the ordinary adapter install (same backup, same atomic write, same verification, same manifest bookkeeping), strips only the hook entries that carry a legacy marker — leaving anyone else's hooks untouched — swaps the legacy CLAUDE.md block for a generated one while keeping the surrounding prose, and regenerates infolang-memory.mdc. Everything is backed up first, and every fix is idempotent: a second --fix finds nothing and writes nothing.


Configuration

| Variable | Effect | | --- | --- | | INFOLANG_URL | Base URL of the deployment. Also --url. Default https://api.infolang.ai. | | INFOLANG_API_KEY | Credential for the CLI's own API calls, and the variable client configs reference. | | INFOLANG_BROWSER / BROWSER | Command used to open the login URL, instead of the platform default. | | INFOLANG_NO_CLIENT_CLI | 1 disables claude mcp add-json / code --add-mcp delegation. | | INFOLANG_DISABLE_KEYCHAIN | 1 skips the OS keychain and uses the fallback file. | | INFOLANG_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER / CI | Silence the update notifier. | | CODEX_HOME | Honoured for the Codex user-scope config path. | | INFOLANG_DEBUG | 1 prints stack traces on unexpected errors. | | INFOLANG_PLUGIN_DIR | Overrides where the packaged Claude Code plugin is read from. | | INFOLANG_HOOKS | off disables every plugin hook. | | INFOLANG_HOOK_CAPTURE | off disables the PreCompact/Stop writes; SessionStart recall and drain still run. | | INFOLANG_HOOK_EVENTS | off disables the SessionStart coordination events drain only. | | INFOLANG_HOOK_MIN_INTERVAL_MS | Rate limit for the Stop hook's writes. Default 15 minutes. |

Update checks

Passive only, and the CLI never mutates itself. Under npx there is zero update machinery (the next npx infolang already fetches the newest version). Under a global install it does one non-blocking registry read at most every 24 hours and prints a single line at exit with the correct upgrade command.


Development

npm install
npm run build       # tsdown → dist/cli.js
npm run typecheck
npm test            # unit + E2E, then `claude plugin validate --strict`
npm run test:unit
npm run test:e2e    # builds first
npm run build:mcpb  # → dist/infolang.mcpb
npm run validate:plugin

validate:plugin runs claude plugin validate plugin/ --strict when the claude binary is on PATH. When it is not, it skips loudly — printing that the manifest was not validated — so a green run with no validation is never mistaken for a validated one.

The E2E suite spawns the built binary with a per-test temp HOME/XDG_CONFIG_HOME/ APPDATA/USERPROFILE/CODEX_HOME and a PATH containing only node, so it can never read or write your real client configs — and never picks up your real claude/code/codex binaries. No test makes an outbound network request: introspection and login run against a local fixture HTTP server.