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 any .java files. Run jskim before reading raw Java source when you need structural context first, then inspect only the lines you need.
git clone https://github.com/garvit-joshi/jskim
git clone --depth=1 https://github.com/garvit-joshi/jskim ~/.claude/skills/garvit-joshi-jskim-jskim
SKILL.mdjskim — 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 with Lombok, REST controllers, DI wiring, and configuration properties.
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.
Single file summary
Summarizes a Java file — collapses imports, fields, boilerplate (getters/setters/equals/hashCode), and shows method signatures with line ranges. Shows annotation parameters for key Spring annotations (
@GetMapping("/path"), @Value("${key}"), @ConfigurationProperties("prefix"), etc.).
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>, and their top-level methods are treated like normal class methods.
Filters (useful for large files with many methods):
— show only methods whose signature contains "billing" (case-insensitive)--grep billing
— show only methods with that annotation--annotation @Transactional- 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, classes, annotations, field/method counts, Lombok usage, enum constants.
jskim <src_dir> jskim <src_dir> --deps # import-based dependencies jskim <src_dir> --endpoints # REST endpoint map jskim <src_dir> --beans # Spring bean DI graph + @Bean producers + config properties jskim <src_dir> --package <prefix> # filter by package 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):
— only show classes in that package (prefix match)--package com.stw.server.tripsheet
— only show classes with that annotation--annotation @RestController
— only show classes extending that superclass--extends BaseService
— only show classes implementing that interface--implements EventPublisher
— show which classes depend on which (uses imports, runs in seconds even on 2000+ files)--deps
— list all REST endpoints: HTTP method, path, handler method, line number--endpoints
— show Spring bean DI graph,--beans
factory method producers, and@Bean
with field details@ConfigurationProperties- Filters can be combined:
--package com.example --annotation @Service --deps --endpoints --beans
Diff mode
Summarizes only the Java files 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:
— file or method that was added[NEW]
— method whose body was changed[MODIFIED]
— file or method that was removed[DELETED]- Deleted methods are shown with their previous signature when the base ref is available, so overload removals stay distinguishable
calls shown for new/modified methods (same format as file summary)→- Getters, setters, and boilerplate changes are suppressed (not interesting)
- Unchanged methods are counted but not listed
Method extraction
Extracts method source code with context (fields, called methods, annotations, Javadoc).
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/File.java // com.example.billing | 12 imports: java.util(3), jakarta.persistence(2), ... // lombok: @Data: getters, setters, toString, equals, hashCode // @RestController @RequestMapping("/api/v1/billing") // public class BillingController extends BaseController // // fields: // BillingRepository billingRepo (@Autowired) // BillingValidator validator // AuditLogger auditLogger // String tenantId // // getters: getName, getStatus <- collapsed, names only // setters: setName, setStatus <- collapsed, names only // boilerplate: toString, hashCode, equals <- collapsed, names only // methods: // L18-L21 ( 4 lines): public BillingController(BillingService svc, BillingValidator v) // L45-L62 ( 18 lines): @PostMapping public Bill createBill(BillDTO dto) // → auditLogger.log, billingService.create, notifyStakeholders, validator.validate // L64-L80 ( 17 lines): @GetMapping("/{id}") public Bill getBill(Long id) // → billingService.findById // L82-L95 ( 14 lines): @PutMapping("/{id}") public Bill updateBill(Long id, BillDTO dto) // → auditLogger.log, billingService.findById, billingService.update, validator.validate // // inner types: // L90: public static enum Status // // other classes in file: // L100: class BillingHelper [2F, 3M] <- 2F = 2 fields, 3M = 3 methods // // total: 120 lines
For Java simple source files:
// SomeScript.java // (default) | 0 imports // 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
= line range in the file (use withL45-L62
offset/limit)Read
= method body length( 18 lines)
= method calls — lists direct method invocations made by this method (sorted alphabetically)→- Noise is auto-filtered: collection ops (
,put
,get
,add
,remove
,stream
), utility checks (collect
,Objects.equals
,StringUtils.isBlank
), logging (MapUtils.isEmpty
,log.info
), type conversions (logger.debug
,toString
), and stream plumbing (valueOf
,map
,filter
) are excludedforEach - Chained/fluent calls (streams, builders) are excluded — only the root call on a simple object is shown
- Calls are capped at 10 per method; overflow shown as
... +N more - Abstract methods and methods with no calls have no
line→
- Noise is auto-filtered: collection ops (
- Spring annotation parameters are preserved:
,@GetMapping("/{id}")@Value("${config.key}") - getters/setters/boilerplate are collapsed to names only — no line ranges, no calls, not worth reading
= N fields,NF
= N methods (used for inner/extra types)NM- Enum constants are listed inline
- Static initializer blocks shown with line ranges:
// static initializer (L10-L25, 16 lines)
Interpreting →
method calls
→The
→ line shows business-logic calls only — boilerplate noise (collection ops, logging, utility checks, stream plumbing, type conversions) is automatically filtered out. What remains is high-signal:
Dependency calls (match a field in
):fields:
→ fieldbillingService.create
exists → injected dependency call. Follow this.BillingService billingService
→ fieldvalidator.validate
exists → dependency call. Follow this.BillingValidator validator
Same-class calls (no dot prefix):
→ unqualified name → private/inherited method in the same class. UsenotifyStakeholders
to read it.jskim File.java notifyStakeholders
Accessor calls on parameters/locals (do NOT match any field):
,dto.getName
→ getter calls on method parameters or local variables. Usually not worth tracing.order.getId
Rule of thumb: If the object name before the dot matches a field name in the
fields: section, it's a dependency call worth following. If it doesn't match any field, it's likely an accessor on a parameter or local — lower priority for tracing.
Project map output format
// Project Map: 42 files, 8500 lines // // com.example.billing (5 files, 600 lines) // class BillingService @Service [3F | 8M | 120L | lombok:Data] // class BillingRepository @Repository [0F | 5M | 45L] // class BillDTO @Data [7F | 0M | 30L | lombok:Data,Builder] // enum BillStatus { DRAFT, PENDING, APPROVED, REJECTED } [0F | 0M | 15L] // interface BillingPort [0F | 3M | 20L]
With
--endpoints:
// === REST Endpoints === // GET /api/v1/billing BillingController.list() L45 // POST /api/v1/billing BillingController.create() L62 // GET /api/v1/billing/{id} BillingController.get() L70 // PUT /api/v1/billing/{id} BillingController.update() L80 // DELETE /api/v1/billing/{id} BillingController.delete() L90
With
--beans:
// === Bean Dependencies === // BillingService @Service <- BillingRepository, BillValidator, KafkaTemplate // BillingController @RestController <- BillingService, AuthService // // === Bean Producers (@Bean) === // AppConfig @Configuration -> ObjectMapper, TaskScheduler, NotificationClient // // === Configuration Properties === // billing.* (BillingProperties): BigDecimal taxRate, String currency, int maxRetries
With
--deps:
// === Dependencies === // BillingService -> BillingRepository, BillDTO, BillingPort
= N fields,NF
= N methods,NM
= N lines in fileNL
= Lombok annotations present on the classlombok:Data,Builder
= inner classes/enums inside this classinner:Foo,Bar- Enum constants shown inline:
enum Status { ACTIVE, INACTIVE } - Dependencies (
) = import-based class references; when simple class names are ambiguous, fully-qualified names are shown--deps - Endpoints (
) = all--endpoints
/@GetMapping
/etc. with full paths@PostMapping - Beans (
) = DI wiring,--beans
factory method producers, and@Bean@ConfigurationProperties
Method extraction output format
With
:--list
// public class BillingService extends BaseService // // L45-L62 ( 18 lines): @PostMapping 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 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
lists class fields for contextfields:
shows other methods referenced in the extracted method bodiescalled methods in same class
appears if a requested method name wasn't found// not found: methodX- Simple source files use the same format with an
headerimplicit class <FileStem>
Workflow
Follow this order to minimize tokens:
- Explore ->
to understand project structurejskim src/ - Narrow ->
to focus on relevant packagejskim src/ --package com.example.billing - Spring context ->
to see REST API + DI wiringjskim src/ --endpoints --beans - Understand ->
to see class structure (fields, methods, line ranges, and method calls)jskim File.java - Trace -> Use
calls to follow execution: match→
againstfieldName.method
to find the target class type, then skim that class to continuefields: - Filter ->
if the class has many methodsjskim File.java --grep billing - Focus ->
to read the methods you needjskim File.java methodA methodB - 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 BillingController.java → See: createBill() calls billingService.create, validator.validate → See fields: BillingService billingService, BillingValidator validator Step 2: jskim BillingService.java → See: create() calls billingRepo.save, eventPublisher.publish, calculateTax → See fields: BillingRepository billingRepo, EventPublisher eventPublisher Step 3: jskim BillingService.java calculateTax → Read the method source to understand the tax logic Done — you traced Controller → Service → Repository in 3 tool calls, reading ~50 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 method (upstream), combine a text search tool with jskim:
Goal: Who calls billingService.create()? Step 1: Search for "\.create(" across *.java files → find calling files Step 2: jskim each calling file → see which methods contain the call and their full context
For finding all usages of a method within the same project:
— scans all files but only shows methods matching "create"jskim src/ --grep create
— finds raw references, then skim the files to understand contextrg "create\\(" -g "*.java"
When to use each tool
| Situation | Tool |
|---|---|
| PR review / what changed? (large diff, 1000+ lines) | to triage, then for details |
| PR review / what changed? (small diff, < 1000 lines) | directly — skip jskim |
| New project, need orientation | |
| Find all REST controllers | |
| See all API endpoints at a glance | |
| See Spring bean DI wiring + producers | |
| Find all classes extending BaseService | |
| Find all implementations of an interface | |
| Understand a class structure | |
| Trace call flow downstream | Skim the class → follow field calls → skim the dependency class |
| Find callers (upstream) | Search for across , then skim calling files |
| Assess impact of a change | Combine downstream () + upstream () to see full blast radius |
| Large class (500+ lines), looking for specific methods | |
| Need to read a method's source code | |
| Need method + related methods together | |
When to use jskim --diff
vs git diff
jskim --diffgit diffjskim --diff gives structural context — which methods were added, modified, or deleted, with signatures and call graphs. git diff gives the actual code changes. They serve different purposes:
Use
when:jskim --diff
- 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
Use
directly (skip git diff
) when:jskim --diff
- 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
— running jskim after is redundantgit diff - 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) —
is faster and gives you more useful information thangit diffjskim --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
after readingjskim --diff
; the structural info is already in contextgit diff - 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
files.java - The user asked to read the full file — respect the request and read the full file directly
Rules
- Run
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.jskim - 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
to understand the structurejskim <src_dir> - For large projects (500+ files), use
to scope project map output--package - For large classes (300+ lines, many methods), use
or--grep
to filter output--annotation - 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
calljskim File.java method1 method2 - When tracing call flow, check the
calls against the→
section to identify dependency calls vs parameter accessor noisefields:
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:
- Read the Java file directly with your normal file-reading tool
- Produce a similar compact summary yourself — list the package, key annotations, fields (type + name), and method signatures with line ranges
- 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
file -> run.javajskim $ARGUMENTS - If argument is a directory -> run
jskim $ARGUMENTS - If argument is
-> run<file.java> <method>jskim $ARGUMENTS - If no arguments -> explain the available jskim modes