Changelog

Changelog

Every jdx release, newest first. Install or upgrade with one command.

Already on jdx? jdx upgrade moves a release install to the latest version. Changes merged since v1.6.1 are on main; the docs on this site track main.

v1.6.1#

jdx v1.6.1 — usages --kind throw says what it means#

Patch release: no behaviour change, no output shape change, no exit code change. jdx usages --kind throw has always read throws declarations (declaration-level — "who declares this type"), never ATHROW throw sites ("who actually throws it"). Attributing a throw site needs data-flow analysis the reference extractor deliberately skips. Until now that took source-diving to learn; now the help text, the MCP tool description, and the docs say it everywhere (closes #7).

Install#

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash

Windows users who prefer an Explorer-first bundle: download jdx-1.6.1-windows.zip from the assets below and run sh install-release.sh --tarball jdx-1.6.1-windows.zip. Release installs self-update with jdx upgrade.

Releases are unsigned: expect a first-run warning — Unblock-File on Windows (PowerShell), xattr -d com.apple.quarantine <install-dir>/jdx on macOS.

What changed#

throw = throws declarations, not ATHROW sites — stated explicitly in:

  • jdx usages --help and the --kind flag help (UsagesCommand)
  • the MCP usages tool kind parameter description
  • docs/COMMANDS.md (command table), docs/PROPOSAL.md (§7.3), docs/guide/navigating-code.md (kind table + Limitations bullet)
  • KDoc on JdxService (UsageKindFilter, T-075 comment, collectMetadataUsages), ReferenceExtractor (ATHROW intentionally not extracted), RpcDispatch (usages kind contract)

The follow-up commit also fixes the guide naming a nonexistent jdx flow: throw-site attribution is deferred to future work without naming a command that does not exist yet.

Known limitations#

  • usages --kind throw remains declaration-level: methods whose throws clause declares the type. Throw-site (ATHROW) attribution needs data-flow analysis and is deferred to future work.
  • Same standing notes as v1.6.0: two setup backends still missing (Windsurf #38, Aider #41); version-probe caveats #84 / #85; MULTI_RELEASE_VARIANT provenance #29.

Testing#

  • Existing UsagesServiceTest.kind throw shows throws declarations already pins the semantics; UsagesServiceTest (34 passed), UsagesCommandTest (6 passed), lint clean (per PR #93).
  • Release gate: full ./gradlew check -Pci plus per-OS distribution builds and launcher smoke tests, all green.

PRs in this release#

PR What
#93 Clarify throw as throws declarations (closes #7)

Full Changelog: https://github.com/MohammadMD1383/jdx/compare/v1.6.0...v1.6.1

v1.6.0#

jdx v1.6.0 — docs website live, Antigravity setup, sturdier body#

Minor release: the documentation website is live (generated from the docs and the real CLI, with gates that stop it drifting), jdx setup grew its eighth agent backend (Antigravity, verified against a real install), and jdx body no longer gives up on one bad Vineflower token. No query command changes behaviour, no output shape changes, and no command exit code changes — if you script against jdx, nothing here needs re-learning.

Install#

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash

Windows users who prefer an Explorer-first bundle: download jdx-1.6.0-windows.zip from the assets below and run sh install-release.sh --tarball jdx-1.6.0-windows.zip. Release installs self-update with jdx upgrade.

Releases are unsigned: expect a first-run warning — Unblock-File on Windows (PowerShell), xattr -d com.apple.quarantine <install-dir>/jdx on macOS.

Docs website is live#

The GitHub Pages site (mohammadmd1383.github.io/jdx) is generated from the repository's Markdown files and the freshly built jdx binary by a new build-time-only :site Gradle module — never shipped in the product distribution.

What keeps it honest, on every PR:

  • Exec blocks: console exec code blocks in the docs are executed against the built binary and their real output is embedded — if an exit code or an output line changes, the build fails instead of publishing stale docs.
  • Doc lint: every jdx <cmd> --flag mention in the docs is validated against the real CLI's --help, so a renamed flag breaks the build, not a user's copy-paste.
  • Link checker: all internal links and anchors must resolve.
  • Autogenerated reference: the command reference pages are rendered from actual help output, and every HTML page gets a Markdown twin for language-model consumption.

Riding along in the same PR: layered user guides and FAQ, the green accent colour, release-note heading nesting without skipped levels, and Lighthouse as a warn-only gate that never blocks deploy.

jdx setup --agent antigravity#

The eighth backend, verified against the real agy 1.3.0 binary (installed via the official installer — not just the docs):

--agent Project scope System scope Version probe
antigravity .agents/mcp_config.json, walk-up ~/.gemini/config/mcp_config.json agy --version

The entry is the minimal shape agy mcp list recognises:

{ "mcpServers": { "jdx": { "command": "jdx", "args": ["mcp"] } } }
jdx setup --agent antigravity --scope project
antigravity project setup installed (mcpServers entry)
  /home/you/project/.agents/mcp_config.json
next: restart antigravity (config loads once at startup)

Real-binary findings: system scope writes the user-level file only (all of agy mcp add/list/enable/disable/remove operate there); agy mcp never writes the project file, so jdx hand-edits it in the same mcpServers shape. Bidirectional interop was proven with the built jdx both ways — jdx-written files pass agy mcp list, agy-written bytes pass jdx --check (exit 0, byte-identical no-op) — and jdx --remove leaves agy mcp list empty. Same merge discipline as the other seven backends: unrelated servers survive, re-runs are byte-exact no-ops, --check reports without writing (exit 1 when absent).

Fix: jdx body survives one bad Vineflower token (#87)#

body / source --around locate members by parsing decompiled text with JavaParser. A single rejected token — Vineflower emitting import foo.1; / this.1 for synthetic $1 siblings (Found ".1" expected "." ";") — failed the whole file, so a member that members lists and javap proves was reported not found, while source served the same file fine unparsed.

The fix adds a lenient seam (parseJavaUnitLenient / loadJavaUnitLenient / findJavaBodiesLenient): the offending Found "..." token is blanked to spaces — line breaks and lengths preserved, so ranges still address the original lines and slices stay verbatim — and parsing retried, up to 10x. Paired -sources.jar input stays strict (still an honest ParseError / exit 5). If even lenient parsing fails, the error now says the member exists in bytecode and names the file view that still works (jdx source 'TYPE' --engine vineflower) alongside the --engine javap hatch. Exit codes are unchanged, so the default-ladder retry still works.

Test infrastructure: categories, budgets, progress#

Also in this release: tests are categorised, per-test time budgets are enforced (test stays a fast unit loop), and the runner logs live progress — so a slow or hung suite points at the culprit instead of a silent minute.

Known limitations#

  • Two agents from the umbrella are still missing: Windsurf (#38) and Aider (#41), tracked under #32. Antigravity (#34) closed with this release.
  • Version detection is a best-effort probe: the raw probe output is not collapsed to one line, so a chatty binary spills a multi-line, absolute-path-bearing WARNING across the detected: line in text mode (#84; the --json envelope escapes it correctly and is fine); and the agent-config root comes from the JVM's user.home, which on Linux comes from getpwuid() and therefore ignores $HOME (#85).
  • Attribution still covers reader warnings; the MULTI_RELEASE_VARIANT provenance line does not say which variant was selected (#29).

Testing#

  • Website: the pages workflow builds and validates the site on the PR; lint, :site:test and :site:buildSite green.
  • Antigravity: 20 new SetupServiceTest cases (incl. a real-agy-bytes golden pin and generative fixed-point families), 9 new SetupCommandTest cases, 3 new byte-pinned goldens, 3 new doctor tests — all with fake homes; 545 unit tests, full integration suite, lint, :site:test, :site:buildSite green.
  • Lenient slicing: 8 new JavaBodiesLenientTest tests (incl. a property test: injected bad imports still slice via lenient over generated members), plus BodyServiceTest (repairable forced-vineflower succeeds exit 0 with vineflower provenance; unrecoverable names the file view + javap in text and JSON) and SourceServiceTest (--around with one bad token succeeds); :sources:integrationTest 140 pass, :index:check 651 pass, :cli:check green.

PRs in this release#

PR What
#89 GitHub Pages site generated from the docs and the real CLI
#90 jdx body survives a bad Vineflower token
#91 Antigravity setup support, agy-verified

Full Changelog: https://github.com/MohammadMD1383/jdx/compare/v1.5.0...v1.6.0

v1.5.0#

jdx v1.5.0 — jdx setup: every agent you actually use#

Minor release: jdx setup grew from one agent backend to seven, jdx doctor gained an AppCDS row, and three defects were fixed — two of which could make jdx report the wrong thing, and one of which had a quality gate silently not running at all. No query command changes behaviour, no output shape changes, and no command exit code changes — if you script against jdx, nothing here needs re-learning. jdx setup --agent opencode behaves exactly as it did in v1.4.0, byte for byte.

In v1.4.0 you could paste an opencode.json snippet by hand and jdx would write it correctly. The obvious follow-up question was the one everybody actually has: which config file does my agent read? This release answers it for the seven agents the project verified against real installs, writes the entry in that agent's own shape, and reports which line it found — rather than guessing a shape and hoping.

--agent Project scope System scope Version probe
opencode (v1.4.0) opencode.json/opencode.jsonc, walk-up ~/.config/opencode opencode --version → v1/v2 line
claude-code (alias claude) .mcp.json, walk-up ~/.claude.json claude --version
cursor .cursor/mcp.json, walk-up ~/.cursor/mcp.json cursor-agent --version (legacy agent shim fallback)
kilo .kilo/kilo.json[c], else kilo.json[c], walk-up (fresh default .kilo/kilo.json) ~/.config/kilo/kilo.json[c] none (Kilo has no classified line)
cline ~/.cline/data/settings/cline_mcp_settings.json same single global file none
codex .codex/config.toml, walk-up $CODEX_HOME/config.toml, else ~/.codex/config.toml codex --version
copilot .mcp.json, walk-up, .github/mcp.json fallback $COPILOT_HOME/mcp-config.json, else ~/.copilot/mcp-config.json copilot --version

Install#

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash

Windows users who prefer an Explorer-first bundle: download jdx-1.5.0-windows.zip from the assets below and run sh install-release.sh --tarball jdx-1.5.0-windows.zip. Release installs self-update with jdx upgrade.

Releases are unsigned: expect a first-run warning — Unblock-File on Windows (PowerShell), xattr -d com.apple.quarantine <install-dir>/jdx on macOS.

jdx setup --agent claude-code#

The shape is the one claude mcp add jdx -- jdx mcp would write, so an agent that already knows the entry recognises it:

jdx setup --agent claude-code --scope project
claude-code project setup installed (mcpServers entry)
  /home/you/project/.mcp.json
  detected: claude-code (2.1.283 (Claude Code))
next: restart claude-code (config loads once at startup)
{ "mcpServers": { "jdx": { "command": "jdx", "args": ["mcp"] } } }

Project scope walks up for an existing .mcp.json and writes one in the checkout root if there is none; system scope targets ~/.claude.json.

jdx setup --agent cursor#

Same mcpServers.jdx shape — Cursor reads it from .cursor/mcp.json (project) or ~/.cursor/mcp.json (system), verified against cursor-agent 2026.09.26-dd393fe via agent mcp list.

jdx setup --agent kilo#

Kilo Code is an OpenCode fork sharing the mcp map shape, so it gets the single mcp.jdx entry (not OpenCode's v1+v2 pair — there is no second line to cover). Fresh kilo.json[c] files carry no $schema. Project scope walks up, preferring the .kilo/ variant at each level — within one directory the order is .kilo/kilo.jsonc, .kilo/kilo.json, kilo.jsonc, kilo.json — and a checkout with no Kilo config gets a fresh .kilo/kilo.json.

Kilo Code and Cline are the two backends that report no detected: line: neither has a config-line version to classify, and jdx does not invent one.

jdx setup --agent cline#

Cline's entry is nested one level deeper — it is a transport object:

{ "mcpServers": { "jdx": { "transport": { "type": "stdio", "command": "jdx", "args": ["mcp"] } } } }

Verified against the real install (cline CLI 3.0.65 + extension 4.1.21: resolveMcpSettingsPath; cline/cline#2418 open): Cline keeps no project-level MCP file, so --scope project and --scope system name the same global ~/.cline/data/settings/cline_mcp_settings.json. That is the verified layout, not a fallback, so the output says so out loud rather than letting a project-scoped run look checkout-local:

jdx setup --agent cline --scope project
cline project setup installed (global file — Cline keeps no project-level MCP config)
  /home/you/.cline/data/settings/cline_mcp_settings.json
next: restart Cline (config loads once at startup)

The probe also accepts the legacy flat command/args shape, so --check and doctor report a Cline entry written by an older jdx.

jdx setup --agent codex#

Codex CLI config is TOML, so the entry is a [mcp_servers.jdx] table and the merge is line-based rather than a JSON tree rewrite — every other key in config.toml survives as written:

[mcp_servers.jdx]
command = "jdx"
args = ["mcp"]

Verified against codex-cli 0.157.1. Project scope targets .codex/config.toml (Codex only reads it in a trusted checkout); system scope honours $CODEX_HOME.

jdx setup --agent copilot#

The GitHub Copilot CLI entry is mcpServers.jdx with "type": "local", in .mcp.json (project — the nearest one walking up, .github/mcp.json as fallback, trusted directories) or $COPILOT_HOME/mcp-config.json, else ~/.copilot/mcp-config.json. Verified against GitHub Copilot CLI 1.0.88. A hand-written "type": "stdio" entry probes as installed; jdx writes local.

Every backend still merges, never clobbers#

Unchanged from v1.4.0 and now true for all seven: unrelated servers, models, providers and every unrelated top-level key survive; only the jdx entry is added, replaced, or removed. Re-runs are byte-exact no-ops, so comments and formatting survive. --check reports without writing (exit 1 when absent), --remove uninstalls cleanly, and exit codes are unchanged: 0 installed/removed/present, 1 checked-absent, 3 bad usage, 5 unreadable config.

One backend-specific fix worth calling out: isJdxCommand accepts Windows shims (jdx.exe, jdx.cmd, jdx.bat) and backslash paths on every host, so --check and the doctor row stay truthful on Windows instead of reporting "not installed" for an entry jdx itself wrote.

jdx doctor grew an appcds row#

The AppCDS archive (libs/jdx.jsa, trained per-OS by :app:installDist) was invisible to doctor — the one thing users asked about when cold start felt slow. It is now a stat-only row (size + mtime against the fat jar, no JVM probe, no network):

State Reported
present ok — path and size
absent warn — cold start without AppCDS; rebuild with ./gradlew :app:installDist
empty (0 bytes) warn — corrupt or truncated; rebuild
older than the fat jar warn — stale; rebuild
unresolvable (dev layout, no jar) warn — ship with :app:installDist

Absent or stale is a warn, never a fail: the archive only affects speed, and -Xshare:auto degrades to a plain run. Only the generic probe wrapper turning a thrown exception into fail can make this row fail.

install.sh now also prints a one-line note when it installs from a build with no archive, pointing at this row rather than failing.

The setup row was widened in the same change: it now reports wiring for all seven backends, on both scopes, with each backend's version classification.

Fix: a stale fat jar can no longer report the wrong version (#68)#

A versioned -PjdxVersion build on top of a default one left both jdx-0.1.0-SNAPSHOT-all.jar and jdx-1.3.0-all.jar in app/build/libs. The launcher globbed and took the first, so jdx version could report the wrong version — and jdx upgrade, which decides what to do from that version, was misled with it. Fixed on both sides:

  • the app build evicts any superseded jdx-*-all.jar after writing its own, so libs holds exactly one;
  • the launcher treats more than one fat jar as an ambiguous install and fails loudly, naming every jar it found, instead of picking one:
jdx version
jdx: multiple jdx fat jars in '/opt/jdx/libs': '/opt/jdx/libs/jdx-1.4.0-all.jar' '/opt/jdx/libs/jdx-1.5.0-all.jar' — rebuild to leave exactly one: ./gradlew :app:clean :app:installDist

Fix: a stale AppCDS archive is now silent (#54, #22)#

The archive is keyed to the exact JDK build, so on a different vendor or update the JVM rejects it and logged a [warning][cds] version-mismatch notice. Output and exit codes were always correct — but the notice lands on stdout, where it corrupts piped and --json output, and on stderr it reads like a crash for a command that then works fine. Every launcher now passes -Xlog:cds=off (after -Xshare:auto, before -XX:SharedArchiveFile), so the degradation is silent: exit 0, correct output, empty stderr, just without the archive speedup. Pinned end-to-end by a launcher test that feeds a garbage archive and still requires a clean jdx version.

CI: the core mutation gate can run again (#66)#

The core ≥ 80 % mutation gate has never once run in CI. PIT aborts in its pre-scan — before generating a single mutant, with no score produced at all — when the fixture corpus is missing, and on a clean runner it always was. Two independent causes: a PIT minion is a fresh JVM that inherits neither the test/tier2Test system properties nor a dependsOn on :testfixtures:build, so it found no corpus. This is worse than no gate: a gate that cannot run looks exactly like a passing one.

Fixed by wiring both corpus properties (jdx.fixturesDir, jdx.diffFixturesDir, named once so they cannot drift from the fixture constants) and all four fixture jars into every PIT module, and — the part that stops it recurring — by verifyPitestWiring, a cheap assertion in every check that fails if a PIT module loses either property or a jar dependency. A wiring regression now reads as one clear red line in seconds instead of four confusing PIT aborts twenty minutes in.

The same lesson is now written down where it will be read next: a new rule in AGENTS.md §7 and CONTRIBUTING.md (any precondition a gate needs gets its own assertion in check), plus docs/TESTING.md §10.1 and a new "ask, then resolve" convention — a missing input is a property of the machine, so helpers expose an availability probe and the suite assumeTrues on it, making absence a visible SKIP rather than a red that points at product code.

CI: the /opencode agent can read CI logs (#36)#

The job's permissions: block gained actions: read (a whitelist, not additive), and its GITHUB_TOKEN is now exposed to the agent as GH_TOKEN — read-only, this repo only, minted per job and dead when the job ends. So an agent debugging a red build reads gh run view <id> --log instead of guessing, and never needs a PAT.

Known limitations#

  • Three agents from the umbrella are still missing: Antigravity (#34), Windsurf (#38) and Aider (#41), tracked under #32. The seam is one enum case plus a merge function; each lands the way these six did, verified against a real install rather than a guess.

  • Cline has one global file, so its two scopes are the same file. A project-scoped Cline run still edits ~/.cline/.... This is Cline's verified layout, and the output says so — but it is not checkout-local, and pretending otherwise would be the wrong kind of convenience.

  • Codex CLI only reads .codex/config.toml in a trusted checkout. jdx writes it either way; Codex decides whether to load it.

  • jdx setup is still not served by the daemon and has no MCP tool or HTTP route: it writes to your config, not to a workspace. It is a local maintenance command like jdx upgrade and jdx cache.

  • Version detection is a best-effort probe and a binary that answers with something unparseable reports the raw output (claude-code version unknown (<raw>), codex not found on PATH) so a human can judge — jdx never guesses a line. Two caveats found by dogfooding the released binary while writing these notes, both filed and both pre-existing rather than regressions: the raw probe output is not collapsed to one line, so a chatty binary spills a multi-line, absolute-path-bearing WARNING across the detected: line in text mode (#84; the --json envelope escapes it correctly and is fine); and the agent-config root comes from the JVM's user.home, which on Linux comes from getpwuid() and therefore ignores $HOME (#85) — so overriding HOME does not redirect setup/doctor, and there is currently no flag to point them at a different home.

  • Attribution still covers reader warnings; the MULTI_RELEASE_VARIANT provenance line does not say which variant was selected (#29).

  • Follow-ups found while doing this, both open: the fixture-corpus wiring is still copy-pasted across seven module build files (#82), and :index:pitest reports bogus surviving mutants under the parallel javap fan-out (#81).

Testing#

185 new tests and 18 new goldens, none of which can touch your real home:

  • 98 service tests + 49 adapter tests for the six new backends, covering the merge matrix per backend (fresh install, merge-preserves-unrelated, remove-keeps-others, byte-exact idempotent re-run, corrupt config → 5, project walk-up, $CODEX_HOME/$COPILOT_HOME overrides, type: stdio probed true where local is written, Cline's legacy flat shape, Kilo's .kilo/ preference, hostile command words) and each backend's --json payload.
  • 18 byte-pinned goldens — fresh-install, merge-preserves and remove-keeps-others for all six new backends. Rewrite with -Pgolden.update=true, and read the diff before committing it.
  • 23 doctor tests covering the new appcds row (present, absent, empty, stale, unresolvable, unreadable) and the widened setup row, plus 2 Windows probe tests.
  • 7 launcher/install round-trips in :app running the real scripts: the multiple-fat-jar exit 6, the garbage-archive silent-degradation path, the install.sh no-archive note, and the Windows .exe probe.
  • 6 fixture-ownership tests in testfixtures/core making the resolver-ownership pin path- and source-set-correct, so the corpus the mutation gate now depends on is the same corpus the tests read.

userHome, projectDir, $CODEX_HOME and $COPILOT_HOME are all temp dirs or constructor args, and every version probe is a ProcessRunner fake, so the suite never runs a real agent and never reads your real config.

Docs#

docs/COMMANDS.md (the jdx setup and jdx doctor rows rewritten) · docs/PROPOSAL.md §15 (launcher tuning) + §17.2 (the release-bundle caveat) + Appendix B (the widened setup line) · README.md (one setup line per agent) · docs/TESTING.md §10.1 (the gate-wiring rule) · AGENTS.md §7/§11, CONTRIBUTING.md and the cli, app, testfixtures module notes.

PRs in this release#

PR What
#70 Kilo Code setup support
#71 Claude Code setup support
#72 Stale fat-jar version bug fixed
#73 AppCDS doctor row + install.sh note
#74 Stale CDS noise silenced + pinned + docs
#77 Codex CLI setup support
#78 Cline setup support
#79 Cursor setup support
#80 GitHub Copilot CLI setup support
#83 The core mutation gate can get a green pre-scan

Full Changelog: https://github.com/MohammadMD1383/jdx/compare/v1.4.0...v1.5.0

v1.4.0#

jdx v1.4.0 — jdx setup: jdx in your agent, in one command#

Minor release: one new command, one exit-code fix that closes a hole in the contract, one diff provenance fix. No query command changes behaviour and no output shape changes — if you script against jdx, the only thing to re-learn is that a mistyped command now fails instead of exiting 0.

Two things an agent could not do before: wire jdx into its own editor's config (paste a JSON snippet from the README in the right shape for the right OpenCode line, and get mcp.servers vs mcp right), and trust that a usage error is an error — jdx nosuchcommand exited 0, so a typo looked like a successful run, in every release up to v1.3.0.

Install#

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash

Windows users who prefer an Explorer-first bundle: download jdx-1.4.0-windows.zip from the assets below and run sh install-release.sh --tarball jdx-1.4.0-windows.zip. Release installs self-update with jdx upgrade.

Releases are unsigned: expect a first-run warning — Unblock-File on Windows (PowerShell), xattr -d com.apple.quarantine <install-dir>/jdx on macOS.

jdx setup --agent opencode#

One command wires jdx into OpenCode, in the checkout or for the whole machine:

jdx setup --agent opencode --scope project
opencode project setup installed (v1+v2 entries)
  /home/you/project/opencode.json
  detected: opencode v2 (opencode v2.0.18)
next: restart opencode (config loads once at startup)

Both OpenCode lines get wired, always, because they disagree about the shape of an MCP entry and only one of them is what a given install actually reads:

  • v1 (opencode 1.x): servers sit directly under the top-level mcp object keyed by name, each { "type": "local", "command": ["jdx","mcp"], "enabled": true }.
  • v2 (0.0.0-next-* / 0.0.0-beta-* / 0.0.0-dev-* / 2.x): servers sit under mcp.servers, and enabled is replaced by its inverse disabled.

So setup writes both entries and reports the line it detected rather than guessing which one your install reads:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "jdx": { "type": "local", "command": ["jdx", "mcp"], "enabled": true },
    "servers": {
      "jdx": { "type": "local", "command": ["jdx", "mcp"], "disabled": false }
    }
  }
}

It merges; it never clobbers. Every other server in mcp and in mcp.servers, every model/provider/theme key, and every unrelated top-level key survives — only mcp.jdx and mcp.servers.jdx are added, replaced, or removed. The file is read leniently (JSONC comments and trailing commas, which is what a real opencode.jsonc carries), and a second run is a byte-exact no-op, so your comments and formatting survive a re-run:

jdx setup --agent opencode --scope project
opencode project setup already installed (no changes)
  /home/you/project/opencode.json
  detected: opencode v2 (opencode v2.0.18)

A v1 server entry literally named servers cannot share the key with the v2 namespace, so the foreign entry wins untouched and only the v1 jdx entry is written. Merge discipline beats completeness.

Flags and exit codes#

Flag Meaning
--agent <name> Only opencode so far; parsed case- and separator-insensitively (OpenCode, open-code). Other backends follow the same seam — see the limitations.
--scope project|system project (default): the nearest opencode.json[c] walking up from the checkout, else a fresh opencode.json there. system: the existing ~/.config/opencode/opencode.jsonc (what a real install carries), else a fresh opencode.json in ~/.config/opencode.
--check Report without writing anything. Exit 1 when absent, 0 when present — a CI step can assert wiring without changing it.
--remove Uninstall: deletes both jdx entries, drops an emptied mcp.servers and then an emptied mcp, keeps every other server. Idempotent.
--json The same information in the standard envelope.

Exits: 0 installed / removed / checked-present, 1 checked-absent, 3 bad usage, 5 an unreadable or corrupt config (the file is named in the message and is never rewritten).

jdx setup --agent opencode --scope project --check
opencode project setup not installed
  /home/you/project/opencode.json
  detected: opencode v2 (opencode v2.0.18)
next: jdx setup --agent opencode --scope project
echo $?
1

jdx setup --agent cursor
usage error: unsupported --agent 'cursor' (only --agent opencode so far)
echo $?
3

Restart the agent afterwards — it reads its config once at startup.

jdx doctor grew a setup row#

jdx doctor now answers "is jdx wired into my agent, and which line is reading it?" without you having to ask:

jdx doctor
jdx doctor
  jdk: ok (21 (/usr/lib/jvm/java-21-openjdk))
  …
  setup: ok (opencode v2 (opencode v2.0.18): installed (/home/you/project/opencode.json); no system config (/home/you/.config/opencode/opencode.json))

The row is ok when either scope is wired and warn when neither is — setup is optional, so it never fails — and it names the file it looked at plus the exact command to fix it. The version probe is the same pure seam setup uses, so doctor and setup can never disagree about which line is installed.

Fix: a usage error is now an error (exit 3)#

This closed the largest hole in the exit-code contract. Clikt reports a usage error by handing a status code to its own Context.exitProcess seam, and clikt-core's default seam is a no-op (the real one lives in the mordant flavor jdx excludes), so every one of these printed a perfectly correct message and exited 0:

jdx members ; echo $?
Usage: jdx members [<options>] <ref>

Error: missing argument <ref>
0                                  # v1.3.0 and earlier — v1.4.0 prints 3

jdx nosuchcommand ; echo $?
Usage: jdx [<options>] <command> [<args>]...

Error: no such subcommand nosuchcommand
3

jdx members java.lang.String --nope ; echo $?
Usage: jdx members [<options>] <ref>

Error: no such option --nope. (Possible options: --no-daemon, --no-jdk, --no-color, --repo)
3

They now exit 3 — the code AGENTS.md §6 has reserved for usage errors since v1.0.0 — while --help and --version still exit 0 and every service-level code is untouched (3 for a bad flag value, 5 for an unreadable artifact, …). The mapping is 1 → 3, installed once on the root Context so every subcommand inherits it; nothing about the message format changed.

If a script of yours ran jdx … and checked only for 0, a typo in it now stops the pipeline instead of passing silently. That is the point.

Fix: a diff warning now names the artifact it came from (#64)#

Found by dogfooding jdx diff on the gson 2.11.0 → 2.14.0 upgrade. gson's nest-marker classes have binary names that start with an empty $ segment (com/google/gson/internal/$Gson$Types.class), which the reader cannot model, so it degrades. With two inputs the warning could not say which side produced it — and "was this class dropped from the old jar, or is it new in the new one" is exactly the question a diff exists to answer:

jdx diff ~/.gradle/caches/…/gson-2.11.0.jar ~/.gradle/caches/…/gson-2.14.0.jar
api diff gson-2.11.0.jar -> gson-2.14.0.jar (public surface)
…
warning MULTI_RELEASE_VARIANT: in gson-2.11.0.jar: multi-release gson-2.11.0.jar serves version-specific variants for this JDK (21)
warning UNNAMEABLE_CLASS: in gson-2.11.0.jar: unnameable class name (empty name segment in [, Gson, Types]): com/google/gson/internal/$Gson$Types.class

That single word — in gson-2.11.0.jar: — immediately disproved the assumption the report used to hide behind: those classes are in the old jar, not the new one. Both sides' warnings are now attributed before they are unioned, and the consequences are deliberate in both directions:

  • the same unreadable class in two differently-named jars is two facts, so it now reports once per side instead of collapsing into one anonymous line;
  • a self-diff (jdx diff a.jar a.jar) still prints one line, because the two attributed messages are then identical.

The reader's own warning text is unchanged — it is shared by every command and pinned in other goldens. Only diff's union copies it, so no other command's output moved a byte.

Known limitations#

  • Only OpenCode is wired. jdx setup --agent cursor|copilot|cline|windsurf| kilo-code|aider|codex exits 3 naming what is supported. The eight follow-ups are filed and tracked (#36–#42); each is one enum entry plus a merge rule behind the same seam.
  • The v1 MCP entry is deprecated upstream and slated for removal by OpenCode. jdx keeps writing both entries until v1 support is dropped from jdx — a deliberate, temporary compatibility cost, not an oversight.
  • jdx setup is not served by the daemon and has no MCP tool or HTTP route: it writes to your config, not to a workspace, and there is nothing for a remote adapter to answer. It is a local maintenance command like jdx upgrade and jdx cache.
  • Version detection is a best-effort probe of opencode on PATH (falling back to the legacy opencode2 shim only when no opencode exists). A binary that answers with something unparseable reports opencode version unknown (<raw output>) so a human can judge — jdx never guesses a line.
  • Attribution now covers reader warnings; the MULTI_RELEASE_VARIANT provenance line still does not say which variant was selected, only which side produced the warning.
  • Unchanged since v1.2.x and still shipped: the per-OS libs/jdx.jsa logs shared archive file version … does not match noise on a JDK build other than the CI runner's (#54) — output and exit codes are correct, you only lose the shared-archive speedup; and the core mutation gate still cannot run in CI (#66).

Testing#

60 new tests and 3 new goldens, none of which can touch your real home:

  • 30 service tests + 12 adapter tests for setup in cli, covering the merge matrix (fresh install, v1-only completed, v2-only completed, unrelated servers preserved in both maps, a v1 server named servers left alone, remove-keeps-others, byte-exact idempotent re-run, JSONC read, corrupt config → 5, project walk-up, system jsonc preference, hostile command words) and the --json payload.
  • 7 property-based families over the two pure functions that can be wrong quietly: version classification and describeVersion (generated minors and patches, so 1.x vs 0.0.0-next|beta|dev|alpha|rc vs 2.x vs garbage is never a guess), the binary probe's preference of opencode over the legacy shim (with generated PATH noise), and the JSONC stripper — 200 generated strings that must keep comment-like text inside strings intact, plus generated comment bodies that must vanish deterministically.
  • 3 byte-pinned goldens: fresh install, merge-preserves, remove-keeps-others. Rewrite with -Pgolden.update=true — and read the diff before committing it.
  • 6 doctor tests for the setup row (unwired, v1, v2, the opencode2 shim, an unparseable version, wired project) and 5 in JdxCliTest.
  • 4 launcher round-trips in :app that run the real launcher script and fat jar: a missing argument, an unknown command and an unknown option each exit 3, plus a service-level usage error that still exits 3 — which pins the fix at the process boundary, not just at the Clikt seam.
  • 2 index tests for the diff attribution: a corrupt class in both jars is reported once per side with each side named, and a self-diff still reports once.

The probe is a ProcessRunner fake and every userHome/projectDir is a temp dir, so the suite never runs opencode and never reads your real config.

Docs#

docs/COMMANDS.md (a jdx setup row + the rewritten jdx doctor row) · docs/PROPOSAL.md Appendix B (per-command highlights) · README.md (the one-line OpenCode setup hook) · and the cli module notes for SetupService and the doctor row.

Full Changelog: https://github.com/MohammadMD1383/jdx/compare/v1.3.0...v1.4.0

v1.3.0#

jdx v1.3.0 — jdx diff: public-API diff with binary-compat warnings#

Minor release: one new command, and nothing that already shipped changes behaviour. jdx diff answers the question a dependency upgrade poses — what changed, and can I still compile and link against it? — with every difference named by a rule code an agent can filter on, most breaking first.

A real upgrade, on the surface a consumer compiles against — this one is clean, so the CI gate passes and the answer is that you can take the upgrade:

jdx diff gson-2.11.0.jar gson-2.14.0.jar --fail-on breaking
api diff gson-2.11.0.jar -> gson-2.14.0.jar (public surface)
info  TYPE_ADDED  com.google.gson.internal.GsonTypes
info  TYPE_ADDED  com.google.gson.internal.bind.JsonElementTypeAdapter
info  MEMBER_ADDED  com.google.gson.FieldNamingStrategy#alternateNames(java.lang.reflect.Field)
…
0 breaking · 0 suspicious · 17 informational · 2 types added · 0 types removed
fail-on breaking: no breaking changes (exit 0)

The same two jars on the all surface, where the internal churn a consumer never sees is also reported — 118 breaking findings, which is the point of the flag for refactors and mod/mixin work:

jdx diff gson-2.11.0.jar gson-2.14.0.jar --visibility all --severity breaking
api diff gson-2.11.0.jar -> gson-2.14.0.jar (all surface)
breaking  TYPE_REMOVED  com.google.gson.Gson$1
breaking  TYPE_REMOVED  com.google.gson.Gson$2
…
118 breaking · 11 suspicious · 101 informational · 32 types added · 29 types removed

Both sides may be a jar, a class directory, a glob, or a Maven coordinate (--fetch to download; ~/.gradle/caches and ~/.m2 are used without it). No workspace is needed and no index is built — a diff names its own two artifacts. Warm, two real jars compare in 230–250 ms.

Install#

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash

Windows users who prefer an Explorer-first bundle: download jdx-1.3.0-windows.zip from the assets below and run sh install-release.sh --tarball jdx-1.3.0-windows.zip. Release installs self-update with jdx upgrade.

Releases are unsigned: expect a first-run warning — Unblock-File on Windows (PowerShell), xattr -d com.apple.quarantine <install-dir>/jdx on macOS.

What jdx diff reports#

47 rules at three severities (16 breaking, 11 suspicious, 20 informational). The rule id is the CompatRule enum name, so a caller filters on a code and never on prose — MEMBER_REMOVED, RETURN_TYPE_CHANGED, STATIC_TO_INSTANCE, ENUM_CONSTANT_REMOVED, KOTLIN_SUSPEND_CHANGED, and so on. Full set in docs/PROPOSAL.md §7.7.

A member's identity is its name plus the erased parameter list, so a covariant return is one RETURN_TYPE_CHANGED rather than a removal plus an addition, and a field's type is part of its identity. Two pairing passes keep the report free of remove/add avalanches: shape (name + arity) first, then Kotlin name and Kotlin arity for JVM renames and suspend changes. An ambiguous bucket is never paired — with two removals of one shape, guessing which addition replaced which is worse than reporting both.

The inheritance check refuses to guess. A member gone from its type but declared by one of the type's new supertypes is MEMBER_MOVED_TO_SUPERTYPE, not a removal — the caller's invokevirtual still resolves through the chain. When the walk reaches a supertype the artifact does not hold, absence proves nothing, so the removal stays MEMBER_REMOVED and the detail names the supertype it could not read. A diff never downgrades a breaking change on a maybe. The universal roots (java.lang.Object and friends) are excluded from that walk, because counting java.lang.Object as "missing from the artifact" put an "inheritance not checked" caveat on every removal in every jar.

Structure comes from bytecode alone — no sources jar, no decompilation, no classpath — because a diff has no "flesh" to recover.

Exit codes — one documented exception#

A comparison that ran exits 0 whatever it found. The answer is in the report, not the process status, because "no differences" and "differences that do not matter to you" are the same outcome and only you can tell them apart. --fail-on breaking (or any) is the CI gate and the only way a successful diff exits non-zero:

jdx diff old.jar new.jar --fail-on breaking   # exit 1 when a breaking change is present

The verdict is also in the envelope as result.gate, so a consumer that cannot read exit codes still gets the answer. This is the single documented exception to the exit-code table in AGENTS.md §6 / PROPOSAL.md §16; everything else is unchanged (3 for a bad spec or flag, 5 for an unreadable artifact).

Flags#

Flag Meaning
--visibility public|all public (default) is the surface a consumer compiles against; all compares every declared member, for refactors and mod/mixin work.
--severity all|suspicious|breaking Cumulative: suspicious = breaking + suspicious. The per-severity tally always describes the whole comparison, never only what was printed.
--fail-on none|breaking|any Turn the report into a process status.
--limit <int> Maximum findings shown (default 200); the tail this cuts is by construction the least severe.
--include-synthetic Compare bridge/synthetic members too, hidden by default.
--fetch, --repo Maven coordinate resolution, as on every other command.

All four interfaces#

CLI, MCP (jdx_diff), the daemon wire, and HTTP (GET /v1/diff?query=<old>&new=<new>) — the same JdxService.diff call, verified byte-identical between the CLI and HTTP adapters.

jdx diff is deliberately not forwarded to the daemon, and there is deliberately no --no-daemon flag: a daemon warm on one workspace's classpath holds nothing a two-artifact comparison can reuse, and a --fail-on verdict has no way back over the v1 wire. A flag that does nothing is a lie, so it is absent rather than inert — jdx --no-daemon diff a b parses, jdx diff --no-daemon a b does not. The daemon still serves diff.

Fixes that shipped with it#

Found by the new test families, not by inspection — each is a case where the report was confidently wrong:

  • A spurious caveat on every removal. MEMBER_REMOVED carried "supertype java.lang.Object is not in new.jar; inheritance not checked" on every removal in every jar, because every class extends java.lang.Object. The universal roots are now skipped in the inheritance walk, and a private removal skips the walk entirely (JLS §8.2).
  • SUPERTYPE_REMOVED java.lang.Object. A class that becomes an interface, or that now names a real superclass, drops java.lang.Object from its header — reported as a lost supertype, on a change already reported as TYPE_KIND_CHANGED or SUPERTYPE_ADDED.
  • FIELD_TYPE_CHANGED printed its ref twice. A field's canonical ref carries no type, so the detail read now: pkg.Type#field on both sides of the arrow. It now reads int -> long, which is the whole news.
  • ANNOTATION_VALUES_CHANGED kotlin.Metadata on every Kotlin edit. The compiler's own bookkeeping changed value on every declaration touched, so every Kotlin diff carried a line that is always true, burying the real findings. The internal annotations are now excluded; a JetBrains @Nullable is still compared, because that one is real declared API.
  • Every degradation warning was printed twice on a self-diff.
  • macOS and Windows could not build the new test fixtures. The Kotlin plugin contributes a default src/diffV1/kotlin for source set diffV1; an added src/diffv1/kotlin is a different path on Linux and the same directory on a case-insensitive filesystem, so every fixture reached the compiler twice (CLASSIFIER_REDECLARATION). Fixed with setSrcDirs and source-set-cased directory names.
  • Windows could not run the glob tests. * is not a legal character in a Windows path, so four tests that built a glob argument with Path.resolve("*.jar") threw before their first assertion. A spec is matched, never opened — a glob is a perfectly good argument.

Known limitations#

  • A public member that is no longer declared but was an override of a universal root's method (toString, equals, …) still links through that root, and is reported as MEMBER_REMOVED. Resolving it needs the roots' member names, which is a table, i.e. a guess — so the case stays conservative and documented on the rule.
  • A degradation warning does not yet say which of the two artifacts produced it (#64); a multi-release jar trips MULTI_RELEASE_VARIANT for the side that carries it.
  • VARARGS_CHANGED is only reachable from hand-modified bytecode: javac gives f(String) and f(String...) different descriptors, so that change is reported as PARAMETER_TYPE_CHANGED. Pinned by unit tests and listed as unreachable-from-javac in the coverage test, so the gap is deliberate rather than invisible.
  • Unrelated to diff, and pre-existing in every release so far: a Clikt-level usage error — a missing argument, an unknown command, an unknown flag — prints the right message but exits 0 (#65). Service-level exits are correct.
  • The core mutation gate cannot currently run in CI (#66): the PIT pre-scan needs the fixture corpus, which three modules do not hand to the minion JVM, so PIT aborts before mutating anything on a clean checkout. Pre-existing since at least v1.0.0.

Testing#

206 tests and 22 goldens for this feature alone: 73 per-rule examples and 12 property-based families in core (self-identity, total order, determinism, the truncation law, text ⊆ JSON, hostile names), 19 snapshot tests, 21 renderer tests, 44 service/dispatch/rule-coverage/golden tests in index, and 37 adapter tests in cli.

Plus a tier-3 javap differential over six real version pairs in the local corpus, which pins two things nothing else can: the snapshot's member sets equal javap -p -s exactly, and the differ is neither blind to a javap-visible change nor noisy on an unchanged class.

core's mutation gate is 84.0% module-wide against a ≥ 80% threshold, with the new core/diff package at 82.2% — the six remaining gaps in that package were real unexercised branches (the enum-constant access flags, the ambiguous-bucket guard's mirror case, field Signature/ConstantValue/annotations, a non-stripAppliesTo Kotlin view, and each comparator tiebreak), not incidental equivalent mutants.

New corpus: testfixtures/src/diffV1 and src/diffV2 — two versions of the same package, real javac and kotlinc output, byte-deterministic across a clean rebuild. It self-describes through a rule-coverage test rather than per-type annotations: the property worth pinning belongs to the pair — that every rule fires against real compiler output. A rule with no fixture fails the build, and a fixture with no declared expectation fails it too, so neither can grow silently. The Kotlin rules are proven against real @Metadata there, which matters because KOTLIN_SUSPEND_CHANGED depends on a reader flag (stripTrailingContinuation) that nothing else would catch if it stopped being set.

Docs#

docs/PROPOSAL.md §7.7 (the full rule set and the exit-code reasoning), Appendix B, §16, §21 · docs/COMMANDS.md · docs/TESTING.md §11.2 · README.md · and the core, index, cli, server and testfixtures module notes.

Full Changelog: https://github.com/MohammadMD1383/jdx/compare/v1.2.1...v1.3.0

v1.2.1#

jdx v1.2.1 — Windows correctness + full CI on all three OSes#

Patch release: no new commands, no output-shape changes on Linux. Windows users get working --jars globs, Kotlin KDoc from CRLF checkouts, and JDK sources; everyone gets the CI suite proving it — full check (tests + lint

  • coverage gates) green on Linux, macOS and Windows on every push.

Install#

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash

Windows users who prefer an Explorer-first bundle: download jdx-1.2.1-windows.zip from the assets below and run sh install-release.sh --tarball jdx-1.2.1-windows.zip. Release installs self-update with jdx upgrade.

Releases are unsigned: expect a first-run warning — Unblock-File on Windows (PowerShell), xattr -d com.apple.quarantine <install-dir>/jdx on macOS.

What's changed since v1.2.0#

  • JDK sources resolve (body/source/doc over java.*): JDK 9+ src.zip nests files under the module directory (java.base/java/util/ArrayList.java), which the source mapping missed — every JDK query errored instead of serving sources on JDKs that ship it.
  • Windows --jars globs work: *?[{ are illegal path characters on Windows, so expansion threw InvalidPathException instead of matching; glob roots now split with string math, mismatches exit 5, hits sort deterministically on every filesystem.
  • Kotlin KDoc from CRLF checkouts: the PSI doc association misses every KDoc under CRLF text while bodies still slice — CRLF sources (every Windows text=auto checkout) silently lost all Kotlin KDoc. Sources now read LF-pinned, so parse offsets and served text agree on all platforms.
  • Windows output spellings: source-scan mention paths join with / (one spelling for agents), directory listings sort by name string (Windows orders case-insensitively), fault JSON names JSON-escaped paths.
  • jdx doctor Kotlin row probes the resolved cache root, honouring JDX_CACHE_DIR instead of the ambient home.
  • Full CI/CD suite (#55–#61): Linux/macOS/Windows check legs with JUnit + coverage uploads, coverage gates riding check, nightly soak/bench and weekly mutation tiers, workflow hardening (concurrency, minimal permissions, timeouts) and a release check-gate. CI mode (-Pci) keeps slow-runner timing report-only instead of red.

Full Changelog: https://github.com/MohammadMD1383/jdx/compare/v1.2.0...v1.2.1

v1.2.0#

jdx v1.2.0 — full multiplatform support#

Every OS gets a native bundle in this release: jdx-<version>-<os>.tar.gz and jdx-<version>-<os>.zip (<os> = linux, macos, windows), each with a .sha256 checksum and a jdx.jsa AppCDS archive trained on that OS (the archive only engages when your JDK build matches the CI JDK — otherwise it logs [cds] noise and runs normally; see #54). The bare jdx-<version>.tar.gz/.zip aliases are the Linux assets, kept so the installer and jdx upgrade default URLs keep working.

Install#

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash

Windows users who prefer an Explorer-first bundle: download jdx-1.2.0-windows.zip from the assets below and run sh install-release.sh --tarball jdx-1.2.0-windows.zip. Release installs self-update with jdx upgrade.

Releases are unsigned: expect a first-run warning — Unblock-File on Windows (PowerShell), xattr -d com.apple.quarantine <install-dir>/jdx on macOS.

What's changed since v1.1.0#

  • Windows launcher (jdx.bat + jdx.ps1): JDK discovery via %JAVA_HOME%, where java.exe, registry and %ProgramFiles% probes; same flags and exit-6 contract as the POSIX launcher (#46).
  • Per-OS directories: cache/config/daemon-socket/install roots follow the platform table (Linux XDG, macOS ~/Library, Windows LOCALAPPDATA/APPDATA); old dot-dir locations migrate automatically; a missing XDG_RUNTIME_DIR falls back instead of exiting 3 (#44, #45).
  • Daemon starts on Windows/macOS with portable detach, sun_path length guard, start mutual exclusion, and pid-validated stop (#49, #50).
  • POSIX hardening for macOS: no GNU-only flags in the launcher or installers; JDK via /usr/libexec/java_home (#47).
  • Tool + filesystem correctness on Windows: .exe probes, drive/UNC-safe globs, case-insensitive .jar, USERPROFILE fallback (#51).
  • Per-OS release matrix with zip bundles, per-leg checksums, and per-OS jdx.jsa (#48).
  • README rework (hook/demo/install/quickstart, command catalogue moved to docs/COMMANDS.md) plus reproducible benchmark proof.

Full Changelog: https://github.com/MohammadMD1383/jdx/compare/v1.1.0...v1.2.0

v1.1.0#

What's new: jdx upgrade self-update#

Release installs can now self-update from GitHub Releases — no reinstall script needed:

jdx upgrade --check        # report only, never touches the install
jdx upgrade                # update to the latest release
jdx upgrade --version v1.1.0 --json   # pinned version, machine-readable

The updater resolves the target tag, verifies the SHA-256 checksum, and swaps the release tree with a backup-restore guard. Source builds (no .jdx-release marker) are refused with exit 3 — they were never release installs.

One-time note for v1.0.0 installs#

v1.0.0 predates the .jdx-release marker, so jdx upgrade on a v1.0.0 install reports "not a release install". Reinstall once with the script below; from v1.1.0 on, self-update works:

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash

Full Changelog: https://github.com/MohammadMD1383/jdx/compare/v1.0.0...v1.1.0

v1.0.0#

jdx v1.0.0 — initial release#

jdx is an IDE for AI agents, as a command line tool. It answers the questions agents working on JVM codebases ask dozens of times per task — what members a class has, what a method does, who calls it, what implements an interface — directly from .jar, -sources.jar, class/source directories, Maven coordinates, and the JDK itself. Every command returns the smallest correct answer with text + --json output and stable exit codes.

Read: declaration, members, structure#

  • jdx show, members, outline, signature, doc, body, source
  • Structure from bytecode (ASM, never class-loaded), flesh from sources; no sources jar → Vineflower decompile (labelled ⚠ reconstructed) → javap fallback. Kotlin signatures from @Metadata; Kotlin bodies/KDoc via the jdx kotlin install sidecar.

Search and browse#

  • jdx search (incl. camel-hump + fuzzy), resolve, ls, tree

Graph: usages, hierarchy, calls, samples#

  • jdx usages (call/read/write/ref/new/throw/annotation), hierarchy, implementors, callers, calls, samples — live over bytecode roots, plus --src textual scan of project source dirs

Environment, maintenance, serving#

  • jdx version, doctor, ws (named workspaces + Gradle/Maven auto-discovery), cache, kotlin install, help --agent, bench
  • jdx daemon (warm JVM), mcp (20 typed tools), serve (HTTP/JSON), batch (NDJSON) — all byte-identical to --json

Install#

curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash
jdx members java.util.HashMap --limit 5

Requires JDK 21+. Verify with jdx version / jdx doctor.

Full Changelog: https://github.com/MohammadMD1383/jdx/commits/v1.0.0