Skip to content
HumanBean17Public

About

Token-frugal JVM source navigation for AI. Find classes & methods in Gradle/Maven jars via on-demand decompilation. MCP + CLI.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

jarpeek

npm CI

Context-frugal navigation into JVM dependency sources, for AI agents.

Why

An agent working on a Gradle or Maven project that needs to know what SomeLibraryClient.builder() actually does has bad options: read a decompiled 3,000-line file whole (context gone), grep the binary jar (noise), or guess from a doc page (hallucination risk). jarpeek gives the agent the same navigation a human has in an IDE — find the class, see its members, read exactly the method body asked for — at a fraction of the token cost.

How it works

  • Lazy, no index. The first query on a fresh project (or after the build files change) runs one bounded Gradle/Maven resolve and writes a manifest of the on-disk jars. That is all the up-front work there is; every later query reads the manifest and opens only the jar entries it needs.
  • Shared across checkouts. The manifest lives in a machine-wide cache, keyed by the build files' contents: a git worktree or clone with the same build files answers its first query from the cache, with no Gradle/Maven run, and nothing is ever written into a checkout.
  • One-file parses. A class lookup parses just the winning artifact's entry for that class — one source entry or class file, never the jar around it.
  • Skeleton outlines. outline renders a Java-shaped skeleton — package, imports, javadoc, members as code lines — with --minimal/--full presets and per-section toggles; read-source serves the whole file, read-member returns only the requested method spans.
  • Provenance on everything. Every answer says whether it is source, decompiled, or signature — so the agent knows what it is reading.
  • Misses are answers. Unknown classes return suggestions and searched artifacts with exit code 0, never a stack trace.
  • Quiet by contract. stderr is capped at three lines per invocation; stdout stays parseable.

jarpeek resolves through the project's own build (Gradle, then Maven, then local machine caches as an explicit last resort), appends the local JDK when it ships sources, and decompiles on demand with a bundled CFR. It speaks MCP (a stdio subprocess, for harnesses that support it) and a plain CLI (for everything else).

The design rationale — why lazy instead of an index, the resolve cascade, the degradation rules — is in the design notes.

Install

npm install -g jarpeek
jarpeek init

Run init once per machine, not per project. It wires the MCP server or CLI hints into your AI harnesses (Claude Code, Codex, Gemini CLI, Qwen Code, GigaCode) at user level, and stops there — no first index, no resolve. From then on every repository, git worktree, monorepo and folder of repos works with no further setup: jarpeek finds the builds of whatever directory the agent runs in (see Workspaces), and the first query auto-resolves. --yes takes the non-interactive defaults (Claude Code + MCP).

Harness MCP (machine-wide) CLI hints (machine-wide)
Claude Code claude mcp add --scope user (printed for you to run when claude is not on PATH) ~/.claude/CLAUDE.md + SessionStart hook in ~/.claude/settings.json
Codex ~/.codex/config.toml ~/.codex/AGENTS.md + hook in ~/.codex/config.toml
Gemini CLI, Qwen Code, GigaCode ~/.gemini/settings.json, ~/.qwen/…, ~/.gigacode/… ~/.<harness>/<HARNESS>.md + hook in its settings.json

The session hook stays silent in folders with no Gradle/Maven build, so machine-wide wiring costs nothing in other projects. init --scope project writes the configs into the current repository instead (.mcp.json, CLAUDE.md, …) for teams that commit them; a project entry takes precedence over the machine-wide one in that repo.

For a quick trial without installing, npx jarpeek@latest init also works — but the wired configs invoke jarpeek by name, so install it globally before relying on them (init warns when it is not on PATH).

Requirements: Node.js >= 20.12.0. A JVM on PATH (or JAVA_HOME) is optional — without it, decompilation degrades to signature-only answers.

Usage

MCP

jarpeek mcp serves a stdio MCP server; init registers it for you (machine-wide; the server serves the client's session folder). The nine tools:

Tool Arguments Answers with
find_class query, limit? Matching classes by FQN, suffix, simple, or fuzzy name
outline fqn, kind?, visibility?, preset?, sections? The class skeleton's declaration rows — the frugal first look
read_member fqn, selectors[] Source slices for the named members
read_source fqn, mode? (full|lines|outline), from?, to? Source text for one class — full by default; prefer outline
read_resource artifact, glob Non-class jar entries (config, services, manifests)
search_symbols query, artifact (required), limit?, kind? Declarations by member name in one artifact
resolve forceUpdate? Forced re-resolve; one summary line (count, duration, warnings), plus the warnings when any. forceUpdate (Maven -U / Gradle --refresh-dependencies) clears cached negative lookups
status — Manifest freshness (present, path, resolvedAt, stale, incomplete, artifactCount) and JVM report
where coordinates The artifact's recorded on-disk paths, each flagged exists or missing

Artifact arguments take full g:a:v coordinates or a unique artifact id (the a segment). Every tool also takes an optional project — see Workspaces.

Workspaces

There is no per-project setup. jarpeek works out which Gradle/Maven builds the session means from its working directory (for MCP: the client's roots, i.e. the session folder):

  • Inside a build — a service folder, a source dir, a worktree of a single-build repo — it uses the enclosing build root (Gradle: nearest settings.gradle[.kts]; Maven: nearest .mvn/, else the outermost parent pom), never climbing past the repository root.
  • Above builds — a monorepo root, a folder of repos — it uses every build below (git ls-files inside a repository, so ignored and fixture projects are skipped; nested repositories included).

With several builds, queries span all of them: each build resolves lazily (two at a time, one stderr line for the batch) and is cached like any other manifest; find_class hits, and the served class and alternatives of outline/read_member/read_source, carry projects — the builds whose dependency set holds that artifact. When two builds pin different versions, pass project (a path inside the build, or its workspace-relative path as status lists it) to read the one you mean. status and resolve report per build. A directory with no build at all behaves as before: it is the project root.

Resolution is lazy

Nothing needs to be primed by hand. The first query on a fresh project — or after the build files change or a recorded jar vanishes — auto-resolves: one bounded Gradle/Maven pass, one stderr notice line (plus a 30s heartbeat while it runs), and the manifest is rewritten. Resolves run the system mvn/gradle from PATH first, the project's root wrapper as fallback — including a retry after a failed system run — so a CI-only wrapper never blocks local use (see --build-tool).

A resolve that fails never fails the query: with a manifest on disk it is served flagged stale with a warning; without one the query answers as a miss, and re-resolution backs off for 60 seconds so a broken build is not re-run per query. jarpeek resolve re-runs the cascade on demand and reports the failure. A Maven reactor where only some modules resolve keeps what resolved but the manifest is flagged incomplete (with the failed modules and the Maven error) — visible in status, and a find_class negative over it carries incomplete: true, so it never reads as a definitive "not in any dependency". When the Maven error names a cached negative lookup ("was cached in the local repository") — the one failure a plain re-resolve cannot clear — the warning says so and names the fix: jarpeek resolve -U re-runs Maven with update checks forced (-U; Gradle resolutions get --refresh-dependencies) and writes a complete manifest in one command; the same option reaches MCP as resolve's forceUpdate input. The full cascade and degradation rules are in the design notes.

CLI

Every tool is also a dash-named subcommand; --json prints the exact MCP result object, --project <dir> picks the workspace directory (default: cwd) — the build enclosing it, or every build below it. Member selectors are #name or #name(Type,...) for overloads.

jarpeek --json find-class StringJoiner --limit 5
jarpeek outline java.util.StringJoiner --kind method
jarpeek outline java.util.StringJoiner --minimal
jarpeek read-member com.example.lib.ApiClient '#execute(Request,int)'
jarpeek read-source com.example.lib.ApiClient --lines 40:80
jarpeek read-resource com.example:demo-lib:1.0.0 'META-INF/**'
jarpeek search-symbols builder --artifact com.example:demo-lib:1.0.0 --kind method
jarpeek resolve
jarpeek resolve -U
jarpeek status
jarpeek where com.example:demo-lib:1.0.0

search-symbols without --artifact is a usage error.

Also: jarpeek init (wire harnesses, once per machine; --scope project for one repo), jarpeek prime (print the agent cheatsheet; --full, --mcp, --export, --hook-json), jarpeek mcp (serve MCP stdio).

Diagnostics — the one bootstrap notice, resolve heartbeats, warnings, degradations — go to stderr (warnings under a three-line cap); stdout stays parseable. Unknown classes exit 0 with suggestions.

Provenance

Every answer carries one of three provenance values, computed for that answer rather than stored anywhere:

  • source — the artifact ships a sources jar (or module source dir). The real published code — and the only provenance whose outlines carry imports and javadoc (class files have neither).
  • decompiled — no sources jar; bytecode decompiled with the bundled CFR on the local JVM. Faithful in structure, but local names, generics, and control flow are the compiler's reconstruction. Treat fine detail accordingly.
  • signature — no sources and no decompilation (JVM unavailable, JDK classes where decompilation is out of scope, or a failed decompile): declarations only. read-member answers "signature only" instead of code.

find_class hits carry the provenance as a promise — what reading that class fully would yield — computed per hit from the artifact's backings and JVM availability.

Degradations are reported, never hidden, and the answer still arrives: each is a warning: ... line on stderr (a degraded[] field in JSON), aggregated under the three-line warning budget. Search results are scoped to the resolved dependency set of the builds in scope, and search_symbols further to the one named artifact.

Configuration

Knob Meaning
--build-tool <auto|system|wrapper> CLI global flag (all subcommands incl. mcp): which mvn/gradle runs resolves (auto = system first, wrapper fallback)
resolve -U / --force-update Per-invocation resolve flag: force dependency update checks (Maven -U, Gradle --refresh-dependencies); the healing path for a cached-negative-lookup partial. No env mirror — it is an action, not configuration, and it does not affect the manifest fingerprint
JARPEEK_BUILD_TOOL Same tri-state via environment; beats config, loses to the flag — the layer to use for harness-spawned MCP servers without server args
--lang <en|ru> CLI global flag: interface language for human-mode output (help, tables' framing, warnings, errors). Flag beats the language config field; default English. --json, the MCP server, and prime output stay English — they are agent/machine contract
JARPEEK_M2_DIR / M2_REPO Where the Maven local repository lives; steers both the Maven resolver's anchor and the cache scan
JARPEEK_GRADLE_CACHE_DIR / GRADLE_USER_HOME Where the Gradle modules-2 cache lives (GRADLE_USER_HOME is the cache's parent — the scan walks <it>/caches/modules-2/files-2.1)
JARPEEK_CACHE_DIR Where the shared manifest cache lives (default: ~/Library/Caches/jarpeek on macOS, %LOCALAPPDATA%\jarpeek on Windows, $XDG_CACHE_HOME/jarpeek or ~/.cache/jarpeek elsewhere)
JARPEEK_HOME Override the home directory used for user-scoped harness configs, the global config below and the default cache dir; while set, init prints the claude mcp add command instead of running it (Claude Code's own config cannot be relocated with it)
JARPEEK_PRIME_MODE Default prime mode (cli or mcp) when config is absent
.jarpeek/PRIME.md Replaces the agent cheatsheet verbatim, every mode
.jarpeek/config.json Per-project defaults, optional: buildTool and language ("en"/"ru") are hand-added; init --scope project records primeMode and its advanced step's m2Dir/gradleCacheDir pins here
~/.config/jarpeek/config.json Machine-wide defaults: primeMode and the m2Dir / gradleCacheDir pins written by init (or by hand), read when the project config names none

Cache roots converge in a fixed order — explicit env var, project config, global config, maven's settings.xml <localRepository> (m2 only), then the default ~/.m2/repository — and env beats config, like buildTool: both are hand-authored machine facts a one-off shell override should win against. init shows the detected roots and persists only an explicit override. Nothing to configure still works: when no candidate matches mvn's classpath output, the resolver derives the repository root from the output itself (quorum-guaranteed) and says so with a maven: m2-anchor-derived:<path> warning; jarpeek status prints the effective roots and the layer each came from.

Flipping the build-tool setting or the m2 root re-resolves: both are part of the manifest fingerprint, so a manifest produced by the other tool — or anchored at another repository — is never served as fresh.

The manifest is the only derived state jarpeek writes to disk, and it lives in the cache directory above, never in the project: entries are keyed by a hash of the build files' contents (plus the build-tool strategy and m2 root), shared by every checkout with the same build files, rebased onto each checkout's own module paths, and pruned after 30 days unused. Concurrent resolves of the same build files — parallel agents in sibling worktrees — run the build once; the others wait for its manifest. A project's .jarpeek/ holds only hand-authored configuration (config.json, PRIME.md); earlier versions' .jarpeek/manifest.json and .jarpeek/gradle-init.gradle are unused and safe to delete (see the design notes).

Limitations

  • No type hierarchy, find-usages, or call-graph navigation — declaration lookup and source reads only.
  • Member-name search is scoped to one artifact (search_symbols requires its artifact argument).
  • JDK classes need $JAVA_HOME/lib/src.zip — there is no jimage fallback. Without it, JDK classes are unavailable.
  • Source-jar FQNs are derived from entry paths: a jar whose entries mismatch its declared packages can misreport a class's location.
  • No generated-source awareness (protobuf, Dagger): generated classes appear only if they land in the resolved artifacts.
  • No remote artifact search (Maven Central by coordinates): on a miss, jarpeek reports what it searched and stops rather than fetching.
  • Maven SNAPSHOT dependencies resolved to timestamped jars (artifact-1.0-20240501.123456-3.jar) are not recognized by the Maven resolver's m2-layout matching and are skipped.
  • Decompilation quality is CFR's; signature is the floor when no JVM is available.
  • Gradle composite builds (includeBuild) and nested independent builds inside a build count as part of the enclosing build: only its own projects' dependencies are resolved.

Development

npm run build        # tsc → dist/
npm test             # vitest run (unit + integration, offline)
npm run typecheck    # tsc --noEmit
node scripts/build-fixtures.mjs   # rebuild test fixture jars (deterministic)

Network-dependent e2e tests self-skip unless JARPEEK_E2E=1 is set. Never edit generated fixture jars by hand — edit test/fixtures/src and rerun the build script.

Releasing

CI runs on pushes to main and on PRs; releases are tag-driven. To cut one:

npm version minor        # bump commit + v* tag (patch | minor | major)
git push --follow-tags   # the release workflow takes over

The workflow verifies the tag matches package.json, reruns every gate, publishes to npm with provenance via trusted publishing — no tokens stored in the repo — and opens a GitHub Release with notes grouped from conventional commits.

One-time setup: on npmjs.com, list this repository and release.yml as a trusted publisher for the jarpeek package.

Credits

Decompilation ships CFR 0.152 by Lee Benfield (MIT); its license is included as vendor/cfr-LICENSE.txt.

License

MIT — see LICENSE.

About

Token-frugal JVM source navigation for AI. Find classes & methods in Gradle/Maven jars via on-demand decompilation. MCP + CLI.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages