jskim

Token-saving Java file reader. Use when working with Java files (.java) to reduce token usage. Auto-triggers when exploring, reading, or understanding Java classes, services, controllers, entities, or

SKILL.md

GitHub

jskim — Java Token Saver for Spring Boot

A CLI tool that summarizes Java files compactly, saving 70-80% of input tokens. Optimized for Spring Boot projects: REST controllers, DI wiring, configuration properties, Lombok, records, Spring Modulith packages.

Requirements

Python 3.10+ — install via pip:

pip install jskim

Before first use, verify jskim is installed by running jskim --version. If you get "command not found", tell the user:

jskim is not installed. Install it with: pip install jskim

Do not attempt to run jskim commands until it is confirmed installed. Fall back to your normal file-reading tools if the user declines to install it.

Usage

jskim auto-detects whether you're pointing at a file or directory, and whether you're asking for a summary or method extraction. A flag that does not apply to the detected mode is reported on stderr (Warning: --grep not used in project mode, ignored), never silently dropped.

Single file summary

Summarizes a Java file — drops imports, collapses boilerplate (wiring constructors, getters/setters/equals/hashCode), and shows the class Javadoc's first sentence, method signatures with line ranges, annotations with their arguments, nested types with their members, and traceable method calls.

jskim <file.java>
jskim <file.java> --grep <pattern>       # filter methods by name/signature
jskim <file.java> --annotation <@Ann>    # filter methods by annotation
jskim A.java B.java C.java               # multiple files

Java simple source files without an explicit type wrapper are summarized as implicit class <FileStem>. A package-info.java shows its package-level annotations (Spring Modulith @ApplicationModule, @NamedInterface).

Filters (useful for large files with many methods):

  • --grep billing — show only methods whose signature contains "billing" (case-insensitive)
  • --annotation @Transactional — show only methods with that annotation
  • Filters apply to the method listing only. Header, fields, and inner types are always shown.
  • Filters can be combined: --grep create --annotation @PostMapping

Project map

Generates a compact map of all Java files in a directory — packages (with package-level annotations), classes, annotations, field/method counts, Lombok usage, enum constants; records are listed by name only. Build output (target/, build/, out/) is skipped, so pointing at a repo root is safe.

jskim <src_dir>
jskim <src_dir> --deps                          # package-to-package dependencies
jskim <src_dir> --endpoints                     # REST endpoint map with resolved paths
jskim <src_dir> --beans                         # Spring bean DI graph + @Bean producers + config properties
jskim <src_dir> --callers Class.method          # upstream callers for a specific method
jskim <src_dir> --impact Class.method           # callers + direct callees for a specific method
jskim <src_dir> --impact Class.method --depth 2 # bounded caller/callee hierarchy depth
jskim <src_dir> --package <text>                 # filter by package (substring)
jskim <src_dir> --annotation <@Ann>              # filter by class annotation
jskim <src_dir> --extends <ClassName>            # filter by superclass
jskim <src_dir> --implements <Interface>        # filter by implemented interface

Filters (essential for large projects with hundreds of files):

  • --package logistics.internal — only show classes whose package contains that text
  • --annotation @RestController — only show classes with that annotation
  • --extends BaseService — only show classes extending that superclass
  • --implements EventPublisher — only show classes implementing that interface
  • --deps — show which packages depend on which other packages and through which types (the Spring Modulith boundary view; same-package references are not listed)
  • --endpoints — list all REST endpoints: HTTP method, full path, handler method, line number, and the handler's remaining annotations (@RequiresPermission("trip.create"), @ResponseStatus(...)). Paths and annotation constants (@RequestMapping(BASE_PATH), path = TRIPS + "/{id}", ApiPaths.ROOT, Perms.TRIP_CREATE) are resolved across the whole project.
  • --beans — show Spring bean DI graph (constructor parameters, Lombok constructor + final fields, or @Autowired/@Inject fields), @Bean factory method producers, and @ConfigurationProperties with instance field details
  • --callers BillingService.create — show resolved upstream callers for a class-qualified method target
  • --impact BillingService.create — show callers plus resolved downstream calls from the target method
  • --depth 2 — follow caller/callee edges beyond direct neighbors; default is 1 and usually best
  • Filters can be combined: --package com.example --annotation @Service --deps --endpoints --beans

Call hierarchy rules:

  • Always use a class-qualified target (Class.method or com.example.Class.method). Bare method names like create are intentionally rejected because they are too ambiguous in Java projects.
  • If simple class names collide, rerun with the fully-qualified class name shown in the candidates list.
  • Edges are resolved from same-class calls, static calls on project classes, and field calls where the field type is a project class, such as billingService.create().
  • A call through an interface or superclass also counts as a call to every project implementation: audit.write() on an AuditApi field is a caller of both AuditApi.write and AuditEventService.write, so querying the concrete service works in Modulith codebases where every cross-module call goes through an *Api interface.
  • Calls on local variables, parameters, or overloaded targets are skipped when they cannot be resolved safely. Treat missing edges as "not proven" rather than "not called."

Diff mode

Summarizes only the Java files, fields and methods changed in a git diff. Ideal for PR reviews — instead of reading full files, get structural context for just the changed parts.

jskim --diff HEAD~1                    # changes since last commit
jskim --diff main                      # changes vs main branch
jskim --diff main...feature-branch     # merge-base comparison
jskim src/ --diff HEAD~1               # scoped to directory
git diff main | jskim --diff -         # read diff from stdin

Output markers:

  • [NEW] — file or method that was added
  • [MODIFIED] — method whose body was changed
  • [DELETED] — file or method that was removed (previous signature shown when the base ref is available)
  • [FIELDS] — instance fields or record components added (+) or removed (-); this is how a DTO/record contract change shows up
  • → calls shown for new/modified methods (same format as file summary)
  • Getters, setters, boilerplate, wiring constructors and static constant changes are suppressed; a new constructor dependency shows up as a [FIELDS] entry instead
  • Methods and fields of nested types are compared too, labelled Outer.Inner.name
  • Unchanged methods are counted but not listed
  • (no field or method changes) means only comments, imports, constants or bodies of trivial methods changed

Method extraction

Extracts method source code with context (instance fields, called methods, behaviour annotations, Javadoc). Documentation annotations (@Operation, @ApiResponse, @Schema, ...) are skipped when printing the source, so a 3-line handler under 40 lines of OpenAPI stays 3 lines. Methods of nested types are extractable by name too.

jskim <file.java> --list                         # list all methods
jskim <file.java> <method_name>                   # extract one method
jskim <file.java> <method1> <method2> <method3>   # extract multiple

Multiple methods — pass all names in one call instead of running the script multiple times. This is useful when you need a method and the methods it calls:

  • Deduplicates results automatically
  • Reports any names that weren't found: // not found: methodX
  • Shows "called methods in same class" across all extracted methods

Reading the output

File summary output format

// path/to/BillingController.java
// com.example.billing
// @RestController @RequestMapping("/api/v1/billing")
// class BillingController extends BaseController
// doc: The billing routes: create a bill, read one, and settle it.
//
// fields:
//   BillingService billingService
//   BillingValidator validator (@Autowired)
//   AuditLogger auditLogger
//
// static fields: BASE_PATH = "/api/v1/billing", LOG, MAX_PAGE_SIZE
//
// constructor: L18-L21 (2 params)          <- only stores its parameters; the fields above are the dependency list
// getters: getName, getStatus              <- collapsed, names only
// setters: setName, setStatus              <- collapsed, names only
// boilerplate: toString, hashCode, equals  <- collapsed, names only
// methods:
//     L60-L62 (  3 lines): @PostMapping("/bills") @RequiresPermission(Perms.BILL_CREATE) @ResponseStatus(HttpStatus.CREATED) Bill createBill(@Valid @RequestBody BillDTO dto)
//                → auditLogger.log, billingService.create, notifyStakeholders, validator.validate
//     L64-L80 ( 17 lines): @GetMapping("/bills/{id}") Bill getBill(@PathVariable Long id)
//                → billingService.findById
//
// inner types:
//   L90: public enum Status { DRAFT, PAID }
//   L95: record Claim implements Comparable<Claim>
//     fields: int count, @NotNull Integer last
//   L99: @Configuration static class S3Config
//     methods:
//              L101-L105 (  5 lines): @Bean S3Client client(BillingProperties props)
//
// other classes in file:
//   L120: class BillingHelper [2F, 3M]     <- 2F = 2 instance fields, 3M = 3 methods
//
// total: 130 lines

For Java simple source files:

// SomeScript.java
// (default)
// implicit class SomeScript
//
// methods:
//         L1-L3 (  3 lines): void main()
//
// total: 3 lines

For enums:

// public enum BillStatus
//
// constants: DRAFT, PENDING, APPROVED, REJECTED
//
// fields:
//   String label

For package-info.java:

// src/main/java/com/example/evidence/package-info.java
// com.example.evidence
// @ApplicationModule(displayName = "Evidence")
//
// total: 65 lines
  • L60-L62 = line range from the signature line to the closing brace (use with Read offset/limit). Annotations above the signature are not counted, so ( 3 lines) is the real size of the method even when 40 lines of @ApiResponse sit above it. jskim File.java method prints the annotations and Javadoc too.
  • doc: = first sentence of the type's Javadoc, when there is one. This is where the design intent lives; read it before the methods.
  • fields: = instance fields only, with their non-documentation annotations (@NotNull @Size on a record component are validation rules). static fields: lists constants by name, with the value inline for short non-private string constants (EVENT_READ = "audit.event.read") so permission keys and paths other classes reference are readable without opening the file; private constants stay names only. A record's components appear under fields:.
  • constructor: = constructors that only store their parameters into fields (dependency-injection wiring). Their parameter list is the fields: list, so the signature is not repeated. A constructor that does anything else stays under methods:.
  • inner types: show their own members indented: record components as a one-line fields:, enum constants inline, methods in the usual format. A nested @Configuration with @Bean methods, a nested record Claim(...) and a nested enum Mode are all readable without opening the file.
  • Annotations keep their arguments for everything that changes behaviour: @RequiresPermission(Perms.X), @ResponseStatus(HttpStatus.CREATED), @Transactional(readOnly = true), @Scheduled(cron = "..."). Long argument lists are truncated with ...). Repeated annotations are shown once.
  • Mapping annotations show only the path, resolved from same-class constants: @PostMapping(path = ONE_TRIP + "/start", consumes = ...) renders as @PostMapping("/trips/{tripId}/start"). @RequestMapping(method = GET, ...) renders as @RequestMapping(GET "/path"). A constant defined in another class stays as its source text, e.g. @RequestMapping(ApiPaths.BASE); --endpoints resolves those across the project.
  • Documentation-only annotations are dropped everywhere: OpenAPI/Swagger (@Operation, @ApiResponse, @Parameter, @Schema, @Tag), @SuppressWarnings, @Generated.
  • Parameter annotations keep the marker and lose their arguments: @RequestParam(required = false) Integer size renders as @RequestParam Integer size.
  • → = traceable method calls made by this method (sorted alphabetically). See the next section.
  • getters/setters/boilerplate are collapsed to names only — no line ranges, no calls, not worth reading
  • NF = N instance fields, NM = N methods (used for inner/extra types)
  • Static initializer blocks shown with line ranges: // static initializer (L10-L25, 16 lines)

Interpreting → method calls

The → line lists only calls you can follow from the summary:

  • Same-class calls (no dot): notifyStakeholders → a method in this class. Use jskim File.java notifyStakeholders to read it.
  • Field calls (field.method): billingService.create → look up billingService in fields: to get the type (BillingService), then skim that class.
  • Static calls (Class.method): AuditEvent.now, Ids.newId → a static method on a project class (same package, or imported from the project's root package).
  • super.method → the parent class.

Not shown, because they cannot be followed from the summary: calls on local variables and parameters (dto.getName(), row.status()), calls on JDK/Spring/library classes (Collectors.groupingBy, OffsetDateTime.now, DSL.noCondition, SecurityContextHolder.getContext), calls on or to static-imported members (TRIP.fields(), assertThat(...)), calls on static fields and ALL_CAPS constants (RANDOM.nextInt, ID.eq), collection ops (put, get, stream, collect), logging (log.info), type conversions (toString, valueOf), and chained/fluent calls. Calls are capped at 10 per method; overflow shown as ... +N more. Abstract methods and methods with no traceable calls have no → line.

Project map output format

// Project Map: 42 files, 8500 lines | packages under com.example
//
// billing (8 files, 900 lines) @ApplicationModule(displayName = "Billing")
//   class BillingService @Service [3F | 8M | 120L | lombok:Data]
//   class BillingRepository @Repository [5M | 45L]
//   class BillDTO @Data [7F | 30L | lombok:Data,Builder]
//   enum BillStatus { DRAFT, PENDING, APPROVED, REJECTED } [15L]
//   interface BillingPort [3M | 20L]
//   records: BillView, CreateBillRequest, SettleBillRequest

With --endpoints:

// === REST Endpoints ===
//   GET     /api/v1/billing         BillingController.list()    L45  @RequiresPermission("billing.read")
//   POST    /api/v1/billing         BillingController.create()  L62  @ResponseStatus(HttpStatus.CREATED) @RequiresPermission("billing.create")
//   GET     /api/v1/billing/{id}    BillingController.get()     L70  @RequiresPermission("billing.read")
//   DELETE  /api/v1/billing/{id}    BillingController.delete()  L90  @PreAuthorize("hasRole('ADMIN')")

With --beans:

// === Bean Dependencies ===
//   BillingService @Service ← BillingRepository, BillValidator, KafkaTemplate<String, Event>
//   BillingController @RestController ← BillingService, AuthService
//
// === Bean Producers (@Bean) ===
//   AppConfig @Configuration → ObjectMapper, TaskScheduler, NotificationClient
//
// === Configuration Properties ===
//   billing.* (BillingProperties): BigDecimal taxRate, String currency, int maxRetries

With --deps:

// === Package Dependencies ===
//   billing.internal →
//     audit: AuditApi, AuditEvent
//     billing: BillingPort, BillingPermissions
//     shared.problem: Problem

With --callers BillingService.create --depth 2:

// === Callers: BillingService.create (depth 2) ===
// target: BillingService.create(BillDTO)  src/.../BillingService.java:L45
//
// callers:
//   ← BillingController.createBill(BillDTO)  src/.../BillingController.java:L62
//     ← BillingJob.retryFailedBills()  src/.../BillingJob.java:L30

With --impact BillingService.create:

// === Impact: BillingService.create (depth 1) ===
// target: BillingService.create(BillDTO)  src/.../BillingService.java:L45
//
// callers:
//   ← BillingController.createBill(BillDTO)  src/.../BillingController.java:L62
//
// calls:
//   → BillingRepository.save(Bill)  src/.../BillingRepository.java:L20
//   → BillingService.validate(BillDTO)  src/.../BillingService.java:L80
  • Package names are shown relative to the common root package named in the header (billing.internal under com.example); the root package itself keeps its full name
  • Package header carries the annotations from that package's package-info.java (Spring Modulith module boundaries)
  • records: = the package's records by name (DTOs, requests, views, rows). Skim the file when you need the components; the controller/service signatures already name the ones that matter
  • NF = N instance fields (statics excluded), NM = N methods, NL = N lines in file
  • lombok:Data,Builder = Lombok annotations present on the class
  • inner:Foo,Bar = inner classes/enums inside this class
  • Enum constants shown inline: enum Status { ACTIVE, INACTIVE }
  • Dependencies (--deps) = which packages import, extend or implement types from which other packages, and which types cross the boundary. Same-package references are omitted. Use it to check Modulith boundaries: an internal package appearing as a target of another module is a violation
  • Endpoints (--endpoints) = all @GetMapping/@PostMapping/etc. with full paths, constants resolved project-wide; an unresolvable expression stays visible as source text (/api/Unknown.PATH). The trailing annotations are the route's guards and status: read the security model of the whole API from this one table
  • Beans (--beans) = DI wiring, @Bean factory method producers (nested @Configuration classes included), and @ConfigurationProperties
  • Callers/impact (--callers, --impact) = resolved method call edges only; classes show as simple names, fully qualified only when the simple name is ambiguous in the project; the file path is always shown

Method extraction output format

With --list:

// public class BillingService extends BaseService
//
//     L45-L62 ( 18 lines): @PostMapping("/bills") public Bill createBill(BillDTO dto)
//     L64-L80 ( 17 lines): public void processBill(Long id)

With method names:

// public class BillingService extends BaseService
// fields: BillingRepository billingRepo, String tenantId
//
// @PostMapping("/bills") public Bill createBill(BillDTO dto) (L45-L62)
//
//   45 |     @PostMapping
//   46 |     public Bill createBill(BillDTO dto) {
//        ...full method source with line numbers...
//   62 |     }
//
// --- called methods in same class ---
//   L64-L80: public void processBill(Long id)
  • Shows full method source with line numbers, including the Javadoc and behaviour annotations directly above it; documentation-only annotations (@Operation, @ApiResponse, @Schema, @SuppressWarnings) are skipped, which is why line numbers can jump
  • fields: lists instance fields for context (constants omitted)
  • called methods in same class shows other methods referenced in the extracted method bodies
  • // not found: methodX appears if a requested method name wasn't found
  • Simple source files use the same format with an implicit class <FileStem> header

Workflow

Follow this order to minimize tokens:

  1. Explore -> jskim src/ to understand project structure
  2. Narrow -> jskim src/ --package com.example.billing to focus on relevant package
  3. Spring context -> jskim src/ --endpoints --beans to see REST API + DI wiring
  4. Understand -> jskim File.java to see class structure (fields, methods, line ranges, and method calls)
  5. Trace -> Use → calls to follow execution: match fieldName.method against fields: to find the target class type, then skim that class to continue
  6. Impact -> jskim src/ --callers Class.method or --impact Class.method to see resolved upstream/downstream method edges
  7. Filter -> jskim File.java --grep billing if the class has many methods
  8. Focus -> jskim File.java methodA methodB to read the methods you need
  9. Edit -> Read only the lines that matter from the source file, then edit normally

Tracing call flow across files (step-by-step example)

Goal: Understand what happens when POST /api/v1/billing is called.

Step 1: jskim src/ --endpoints
        → See: POST /api/v1/billing  BillingController.createBill()  L62

Step 2: jskim BillingController.java
        → See: createBill() calls billingService.create, validator.validate
        → See fields: BillingService billingService, BillingValidator validator

Step 3: jskim BillingService.java
        → See: create() calls billingRepo.save, eventPublisher.publish, calculateTax
        → See fields: BillingRepository billingRepo, EventPublisher eventPublisher

Step 4: jskim BillingService.java calculateTax
        → Read the method source to understand the tax logic

Done — you traced Endpoint → Controller → Service → Repository in 4 tool calls,
reading ~60 lines of skim output instead of ~500 lines of raw Java.

Finding callers (reverse lookup)

The → calls show what a method calls (downstream). To find what calls a specific method (upstream), use call hierarchy mode with a class-qualified target:

Goal: Who calls billingService.create()?

Step 1: jskim src/ --callers BillingService.create
        → See resolved direct callers

Step 2: jskim src/ --callers BillingService.create --depth 2
        → See callers of the callers when you need a broader impact view

Use --impact BillingService.create when you need both upstream callers and downstream calls from the target in one compact view.

Fallback for unresolved/ambiguous cases (calls through local variables, parameters, interfaces with several implementations):

  • rg "\.create\(" -g "*.java" — raw text search

When to use each tool

Situation Tool
PR review / what changed? (large diff, 1000+ lines) jskim --diff develop to triage, then git diff for details
PR review / what changed? (small diff, < 1000 lines) git diff develop...HEAD directly — skip jskim
New project, need orientation jskim src/
Find all REST controllers jskim src/ --annotation @RestController
See all API endpoints at a glance jskim src/ --endpoints
See Spring bean DI wiring + producers jskim src/ --beans
Find all classes extending BaseService jskim src/ --extends BaseService
Find all implementations of an interface jskim src/ --implements EventPublisher
Check module boundaries (what imports what across packages) jskim src/ --deps
See which permission guards each route jskim src/ --endpoints
Understand a class structure jskim File.java
Trace call flow downstream Skim the class → follow → field calls → skim the dependency class
Find callers (upstream) jskim src/ --callers Class.method
Assess impact of a change jskim src/ --impact Class.method --depth 1 first; increase depth only if needed
Large class (500+ lines), looking for specific methods jskim File.java --grep keyword
Need to read a method's source code jskim File.java methodName
Need method + related methods together jskim File.java method1 method2 method3

When to use jskim --diff vs git diff

jskim --diff gives structural context — which fields and methods were added, modified, or deleted, with signatures and call graphs. git diff gives the actual code changes. They serve different purposes:

Use jskim --diff when:

  • The diff is large (1000+ lines, 10+ files) and you need to triage what changed before diving in
  • Changed files are large (300+ lines each) — skim tells you which methods were affected without reading entire files
  • You need to understand the shape/scope of changes across many files before reviewing details
  • You want to identify which modified methods call what, to assess blast radius
  • Records/DTOs changed and you want the contract delta ([FIELDS]) without reading every file

Use git diff directly (skip jskim --diff) when:

  • The diff is small (< ~1000 lines total) — you can read the entire diff faster than running jskim and then reading the diff anyway
  • All changed files are small (< 150 lines each) — the skim output is roughly the same size as the raw diff, so it saves nothing
  • You've already read the full git diff — running jskim after is redundant
  • You need to review actual code logic, not just structure — jskim shows signatures and call graphs, not the changed lines themselves. For bug hunting and code review, you still need the real diff

Key insight: jskim --diff is a triage tool, not a replacement for reading the diff. Use it first on large diffs to decide where to focus, then read the actual changes with git diff or your normal diff/file viewer. On small diffs, skip it entirely and go straight to git diff.

When NOT to use jskim

  • Small files (<100 lines) — just read the file directly, skim overhead isn't worth it
  • Small diffs (< ~1000 lines) — git diff is faster and gives you more useful information than jskim --diff
  • You already have line numbers — if search already told you the exact lines, go straight to that slice of the file. Don't waste a tool call on jskim.
  • You already read the full diff — don't run jskim --diff after reading git diff; the structural info is already in context
  • Generated code — JOOQ output, Protobuf stubs, Swagger-generated clients. These are mechanical and don't benefit from summarization.
  • Non-Java files — this tool only handles .java files
  • The user asked to read the full file — respect the request and read the full file directly

Rules

  • Run jskim before reading a Java file directly when you don't already know where to look (no line numbers from search, no prior context). Skip jskim if you already have the line range you need.
  • Use the line ranges from skim output to read only the relevant slice of the file — never read the whole file when you only need one method
  • When exploring a new Java project, start with jskim <src_dir> to understand the structure
  • For large projects (500+ files), use --package to scope project map output (substring match: --package logistics.internal)
  • For caller/impact checks, use class-qualified targets and keep --depth at 1 until you know you need more context
  • For large classes (300+ lines, many methods), use --grep or --annotation to filter output
  • For editing: read the exact lines you need first, then edit normally — skim is for understanding, not for editing
  • When you need multiple related methods, extract them all in one jskim File.java method1 method2 call
  • When tracing call flow, every → entry is followable: unqualified → same class, field.x → the field's type in fields:, Class.x → that class

Fallback — if jskim crashes

If jskim fails (syntax error, unexpected Java construct, Python not found, etc.), do not stop or ask the user to fix it. Fall back to your native tools:

  1. Read the Java file directly with your normal file-reading tool
  2. Produce a similar compact summary yourself — list the package, key annotations, fields (type + name), and method signatures with line ranges
  3. Continue with the workflow as normal

The goal is always: understand the Java file's structure with minimal tokens. jskim is the fast path, but you can always do it yourself if it breaks.

What $ARGUMENTS is for

If the host skill environment invokes this skill with arguments:

  • If argument is a .java file -> run jskim $ARGUMENTS
  • If argument is a directory -> run jskim $ARGUMENTS
  • If argument is <file.java> <method> -> run jskim $ARGUMENTS
  • If no arguments -> explain the available jskim modes