Domain-specific context reduction for gemini-cli.
Prunes the files sent to the Gemini API from an entire monorepo (~1.8 M tokens) down to a focused, import-graph-centred window (~28 K tokens) before any API call. Zero additional LLM inference; all analysis is local and SQLite-cached.
Large TypeScript monorepos (Rocket.Chat, NestJS apps, Next.js projects) easily exceed Gemini's free-tier context window. Naively loading every file the user @-mentions forces gemini-cli to either fail or send an enormous, unfocused prompt.
This package inserts a BeforeModel hook that rewrites the request in-process:
| Without context-reducer | With context-reducer |
|---|---|
| ~8 000 files, ~1.8 M tokens | ~15–25 files, ~28 K tokens |
| 2–4 s just to tokenise | < 1 s (SQLite cache populated) |
Given one or more seed files (the files the user @-mentioned):
- Outward traversal — walk the import graph from the seed outward (files it imports), up to
maxOutwardDepthhops (default: 1). - Inward traversal — walk the reverse import graph from the seed inward (files that import it), with no depth limit.
- Render mode assignment:
- Seed →
full(sent verbatim) - Inward, depth 1, budget ample →
full - Inward, depth ≥ 2 or outward →
stub(onlyexport declaresignatures) - Budget overflow → downgrade
full → stub → omit
- Seed →
- Stub generation — files in
stubmode get conciseexport declaresignatures so the model knows what is available without paying for the full body. - System annotation — a one-line annotation is prepended to inform the model that context reduction is active and how to request omitted files.
This package is part of the gemini-cli monorepo. After cloning:
npm install # from repo root – installs all workspaces
cd packages/context-reducer
npm run buildbetter-sqlite3 is a native Node module. It is pre-built for common platforms via the node-gyp-build mechanism. If you see build errors, install the platform build tools:
- macOS:
xcode-select --install - Linux:
sudo apt-get install build-essential python3 - Windows:
npm install --global windows-build-tools(run as Administrator)
# Enable context reduction (defaults: 28k token budget, depth 1)
gemini --reduce --prompt "explain the auth login flow" @src/api/user.controller.ts
# Dry-run: print scope log to stderr, do NOT call the model
gemini --reduce --reduce-dry-run --reduce-verbose \
--prompt "add input validation" @src/api/user.controller.ts
# Explicit budget and depth
gemini --reduce --reduce-budget 40000 --reduce-depth 2 \
--prompt "refactor auth" @src/auth/auth.service.ts
# Force a specific domain (overrides auto-detection)
gemini --reduce --reduce-domain server-core \
--prompt "update startup sequence" @src/main.ts| Flag | Type | Default | Description |
|---|---|---|---|
--reduce |
boolean | false |
Enable context reduction |
--reduce-domain |
string | auto | Force domain (overrides auto-detect) |
--reduce-budget |
number | 28000 |
Token budget (sum across full + stub files) |
--reduce-depth |
number | 1 |
Max outward BFS depth (imports-of-seed) |
--reduce-verbose |
boolean | false |
Print scope log to stderr |
--reduce-dry-run |
boolean | false |
Print scope log, skip model call |
Drop a .gemini-context YAML file at the repo root to define domains — named groups that tell the reducer which files belong together and which to exclude.
version: 1
budget:
tokens: 28000
maxOutwardDepth: 1
maxTotalFiles: 80
domains:
- name: auth
description: "Authentication and JWT utilities"
entryPatterns:
- "src/auth/**/*.ts"
excludePatterns:
- "**/*.spec.ts"
alwaysInclude:
- "src/auth/auth.module.ts"
- name: api
description: "REST API controllers"
entryPatterns:
- "src/api/**/*.ts"
excludePatterns:
- "**/*.spec.ts"
alwaysInclude: []
autoScope:
- pattern: "src/auth/**"
domain: auth
- pattern: "src/api/**"
domain: apiThe autoScope rules determine which domain activates automatically based on the seed file paths. If no rule matches, the full repo is crawled without a domain filter.
Ready-made templates for popular project types live in templates/:
| Template | Description |
|---|---|
rocketchat.gemini-context |
Rocket.Chat monorepo |
nextjs.gemini-context |
Next.js app (App Router + Pages Router) |
nestjs.gemini-context |
NestJS monorepo |
Copy and rename to .gemini-context at your repo root.
import { ContextReducerEngine } from '@google/gemini-cli-context-reducer';
const engine = ContextReducerEngine.create('/path/to/repo');
const result = await engine.reduce({
repoRoot: '/path/to/repo',
entryFiles: ['/path/to/repo/src/api/user.controller.ts'],
tokenBudget: 28_000,
maxOutwardDepth: 1,
maxTotalFiles: 80,
});
console.log(result.scopeLog);
// Full files (1):
// [full] src/api/user.controller.ts (~4200 tok)
// Stub files (2):
// [stub] src/auth/auth.service.ts (~320 tok)
// [stub] src/models/user.model.ts (~180 tok)
// Omitted files: 0
engine.close();interface ReductionResult {
included: FileNode[]; // renderMode === 'full'
stubbed: FileNode[]; // renderMode === 'stub'
omitted: FileNode[]; // renderMode === 'omit'
totalTokensBefore: number; // estimated tokens without reduction
totalTokensAfter: number; // estimated tokens after reduction
scopeLog: string; // human-readable scope summary
systemAnnotation: string; // one-line annotation for the model system prompt
}ContextReducerEngine
│
├─ ImportExtractor ──► ASTCache (SQLite, better-sqlite3)
├─ SignatureExtractor ─► ASTCache
│
├─ ReducerPipeline
│ ├─ PatternReducer (priority 100) – exclude node_modules, dist, *.d.ts …
│ ├─ ManifestReducer (priority 75) – .gemini-context domain filtering
│ └─ ImportGraphReducer (priority 50) – BFS Ripple Scope
│
└─ StubGenerator – produces `export declare` stubs for stub-mode files
GeminiCliHook
└─ Registered as a ProgrammaticBeforeModelHandler in HookSystem
→ fires before any external shell hooks
→ rewrites GenerateContentParameters in-process
→ fail-open: errors fall back to unmodified request
cd packages/context-reducer
npm testFour test suites using Vitest:
| Suite | Coverage |
|---|---|
tests/ast-cache.test.ts |
Cache miss/hit, hash mismatch, round-trip, persistence |
tests/stub-generator.test.ts |
Function, class, type, const stubs; header/footer |
tests/ripple-scope.test.ts |
BFS traversal, render modes, depth limits, disconnected files |
tests/engine.test.ts |
End-to-end reduce(), budget enforcement, empty inputs |
# Against the mock-monorepo fixture (always available)
npm run bench
# Against a real large repo (e.g. Rocket.Chat)
BENCH_REPO=/path/to/Rocket.Chat npm run benchExpected warm-cache results on a real monorepo:
| Scenario | Median latency |
|---|---|
| Cold (first call, no SQLite cache) | ~1–2 s |
| Warm (subsequent calls) | ~50–200 ms |
| Depth-0 (seed only) | ~10–30 ms |
- Never calls the Gemini API — 100% local static analysis.
- Fail-open — any unhandled error returns the original (unreduced) request.
- Seed files are never omitted — entry files always appear at minimum as stubs.
alwaysIncludefiles are never omitted — domain-declared pinned files survive budget overflow.- Synchronous SQLite —
better-sqlite3keeps the fast-path latency predictable. - NodeNext module resolution — all internal imports use
.jsextensions.
Apache 2.0 — see LICENSE.