Showcase: ctxman on its own repository
Every number and output on this page comes from running ctxman on a clone of this repository at commit fda35db (2026-10-12), with exact tiktoken counts. The script that does it, website/scripts/build-showcase.mjs, drives the CLI, the MCP server and the REST API. Nothing on this page is edited by hand.
- Everything in gitno configuration1,719,753 tokens · 506 files
- + .contextignorebuild output and lockfiles out967,099 tokens · 488 files
- + .contextincludesource only319,165 tokens · 140 files ✓ fits Claude Sonnet 5.5's 1M window (32%)
1. Start with everything
No configuration at all, just the files git tracks:
$ ctxman --cli
📊 Total files analyzed: 506
🔢 Total tokens: 1,719,753
💾 Total size: 5.98 MB
📄 Total lines: 192,622
🏆 TOP 5 LARGEST FILES BY TOKEN COUNT:
--------------------------------------------------------------------------------
1. 357,327 tokens (20.8%) - html/assets/index-BUCFJtth.js
💾 1217.9 KB, 📄 40,666 lines
2. 115,914 tokens (6.7%) - package-lock.json
💾 285.0 KB, 📄 8,375 lines
3. 103,364 tokens (6.0%) - desktop-app/package-lock.json
💾 252.7 KB, 📄 7,454 lines
4. 98,301 tokens (5.7%) - docs/coderamp-labs-gitingest-8a5edab282632443.txt
💾 413.1 KB, 📄 12,586 lines
5. 37,380 tokens (2.2%) - html/assets/index-DlhE0rqZ.css
💾 92.1 KB, 📄 3,591 linesMore than a fifth of all tokens is one built bundle of the desktop app (html/assets/*.js), and the next three entries are lockfiles and a vendored text dump. A model learns nothing from any of them.
2. Drop what no model needs
A .contextignore uses .gitignore syntax. Five lines take out the build output and lockfiles:
# Build output and lockfiles: large, generated, nothing for a model to learn
html/
**/package-lock.json
desktop-app/
docs/*.txt
test-repos/$ ctxman --cli
📊 Total files analyzed: 488
🔢 Total tokens: 967,099
💾 Total size: 3.67 MB
📄 Total lines: 116,685
📈 Average tokens per file: 1,982
🚫 Files ignored by .gitignore: 0
📋 Files ignored by context rules: 181,719,753 → 967,099 tokens. That squeezes into a 1M window but leaves little room for the conversation, and more than two thirds of it is documentation, tests and the website.
3. Keep only the source, and check the fit
.contextinclude turns the logic around: only what matches is analysed. --target-model checks the result against the model's context window:
lib/**
bin/**
/index.js
/index.d.ts
/package.json
/README.md$ ctxman --cli --target-model claude-sonnet-5-5
📊 Total files analyzed: 140
🔢 Total tokens: 319,165
💾 Total size: 1.22 MB
📄 Total lines: 41,792
📊 Context Window Analysis:
════════════════════════════════════════════════════
Target Model: Claude Sonnet 5.5
Available Context: 1,000,000 tokens
Your Repository: 319,165 tokens (31.9%)
✅ Context usage is within safe limits
💡 Consider excluding these large files:
- bin/cli.js (16,151 tokens, 5.1%)
- README.md (11,821 tokens, 3.7%)
- index.d.ts (9,002 tokens, 2.8%)
Your entire codebase fits comfortably.140 files and 319,165 tokens: the whole source of ctxman fits in Claude Sonnet 5.5's 1M window with room to spare, 81% less than where we started.
Claude counts with its own tokenizer, so a count made with an OpenAI encoding is an approximation. ctxman count says which kind of count you have and how to get the exact one; for OpenAI models it uses the model's own encoding:
$ ctxman count --model claude-sonnet-5-5 --top 3
Model: Claude Sonnet 5.5 (claude-sonnet-5-5)
Files: 140
Tokens: 319,165 [approximate, tiktoken cl100k_base; Anthropic models use their own tokenizer]
Window: 1,000,000 tokens — 31.9% used, fits
For an exact count: ctxman count --model claude-sonnet-5-5 --api
Largest files:
16,151 bin/cli.js
11,821 README.md
9,002 index.d.ts$ ctxman count --model gpt-5.5 --top 0
Model: GPT-5.5 (gpt-5.5)
Files: 140
Tokens: 321,625 [exact, tiktoken o200k_base]
Window: 1,050,000 tokens — 30.6% used, fitsctxman count --model claude-sonnet-5-5 --api sends the redacted text to Anthropic's free count_tokens endpoint and reports Claude's exact count.
4. Same context, fewer tokens
--context-export writes the context as JSON by default; -o toon uses the official TOON encoder instead. Token counts of the two exports:
| Context | JSON | TOON | Saving |
|---|---|---|---|
| File list | 1,184 | 838 | 29% |
Method map (-m), 1,444 methods | 39,603 | 14,221 | 64% |
TOON gains most on uniform lists, such as a method map where every row has a name, a line and a token count:
$ ctxman --cli --context-export -o toon
project:
root: ctxman
totalFiles: 140
totalTokens: 319165
paths:
"bin/"[14]: cli.js,cm-pack.js,cm-mcp.js,cm-pr.js,cm-map.js,cm-why.js,cm-search.js,cm-count.js,cm-gitingest.js,cm-policy.js,cm-update.js,cm-audit.js,cm-ask.js,mcp-server.js
"/"[4]: README.md,index.d.ts,package.json,index.js
"lib/analyzers/"[9]: token-calculator.js,method-analyzer.js,performance-dashboard.js,dependency-scanner.js,ai-suggester.js,import-tracker.js,unused-exports-detector.js,duplicate-detector.js,go-method-analyzer.js
"lib/pr/"[6]: pr-context.js,pr-renderer.js,related-tests.js,diff-parser.js,pr-git.js,method-mapper.js
"lib/why/"[11]: index.js,render.js,target.js,search.js,references.js,paths.js,git.js,history.js,coupling.js,ownership.js,errors.js
"lib/api/mcp/"[6]: server.js,install.js,resources.js,prompts.js,start.js,sandbox.js
"lib/formatters/"[4]: gitingest-formatter.js,toon-formatter-v1.3.js,format-registry.js,toon-formatter.js
"lib/utils/"[21]: snapshot-manager.js,llm-detector.js,updater.js,context-versioning.js,tokenizer-adapter.js,git-utils.js,format-converter.js,template-manager.js,profile-manager.js,messages.js,multi-repo-manager.js,llm-cost-estimator.js,logger.js,token-utils.js,claude-token-counter.js,file-utils.js,error-handler.js,config-utils.js,secret-redactor.js,clipboard-utils.js,cli-output.js
"lib/map/"[6]: resolver.js,render.js,rank.js,graph.js,imports.js,index.js
"lib/pack/"[7]: compress.js,code-lexer.js,index.js,split.js,select.js,budget.js,tree.js
"lib/policy/"[5]: policy.js,audit.js,ignore-files.js,enforce.js,index.js
"lib/search/"[3]: bm25.js,code-units.js,symbols.js
"lib/ui/"[7]: init-wizard-ui.js,wizard.js,dashboard.js,progress-bar.js,template-selector.js,select-input.js,index.js
…5. A method map of one module
With -m, every method comes with its line number and its own token count. Here for the four core modules only:
lib/core/**$ ctxman --cli -m --context-export -o toon
project:
root: ctxman
totalFiles: 4
totalTokens: 6501
methods:
"lib/core/Analyzer.js"[10]{name,line,tokens}:
constructor,15,54
initStats,27,64
analyze,40,87
analyzeSequential,57,61
analyzeFile,71,253
updateStats,110,218
detectLanguage,142,162
getStats,167,14
reset,171,14
getLanguageDistribution,175,67
"lib/core/ContextBuilder.js"[12]{name,line,tokens}:
constructor,19,66
build,36,161
buildMetadata,68,97
buildFileList,89,125
buildMethodList,118,92
applySmartFiltering,141,201
prioritizeFiles,173,235
calculateCoreScore,211,171
calculateBalancedScore,237,50
optimizeForLLM,248,192
generateCompactPaths,282,58
getSummary,298,150
"lib/core/Reporter.js"[9]{name,line,tokens}:
constructor,21,50
report,38,91
…The complete map of the module is 435 tokens.
6. Only what changed
--changed-since limits any run to the files changed since a branch, tag or commit, for example the base of a pull request:
$ ctxman --cli --changed-since HEAD~10
📝 Found 32 changed files
Since: HEAD~10
Impact: CRITICAL (score: 188)
📊 Total files analyzed: 32
🔢 Total tokens: 125,809
💾 Total size: 0.48 MB
📄 Total lines: 13,7757. A map of the repository in 700 tokens
ctxman map ranks files by PageRank over the import graph, recent git activity and symbol references, and lists each file's functions with their line and token count, until the budget is used:
$ ctxman map --tokens 700
lib/utils/logger.js (score 0.875, 1714 tokens)
11: constructor(options = {}) [293 tok]
52: initializeLogDirectory() [131 tok]
72: getDateString() [51 tok]
80: getTimestamp() [14 tok]
87: shouldLog(level) [20 tok]
94: formatMessage(level, message, meta = {}) [128 tok]
111: writeToFile(message) [109 tok]
129: log(level, message, meta = {}) [76 tok]
146: error(message, meta = {}) [20 tok]
150: warn(message, meta = {}) [20 tok]
154: info(message, meta = {}) [20 tok]
158: debug(message, meta = {}) [20 tok]
162: trace(message, meta = {}) [20 tok]
169: custom(icon, level, message, meta = {}) [49 tok]
179: group(title) [35 tok]
186: groupEnd() [34 tok]
196: time(label) [24 tok]
201: timeEnd(label) [26 tok]
209: clearOldLogs(daysToKeep = 7) [194 tok]
238: getLogFilePath() [44 tok]
247: getRecentLogs(lines = 100) [104 tok]
270: function getLogger(options = {}) [29 tok]
280: function createLogger(options = {}) [14 tok]
lib/parsers/gitignore-parser.js (score 0.590, 1052 tokens)
9: constructor(gitignorePath, contextIgnorePath, contextIncludePath) [64 tok]
18: loadPatterns(gitignorePath, contextIgnorePath, contextIncludePath) [169 tok]
35: parsePatternFile(filePath) [145 tok]
56: convertToRegex(pattern) [253 tok]
80: isIgnored(filePath, relativePath) [182 tok]
105: isDirectory(filePath) [59 tok]
114: testPatterns(patterns, relativePath, reason) [69 tok]
125: testPatternsWithNegation(patterns, relativePath) [70 tok]
lib/utils/file-utils.js (score 0.418, 949 tokens)
26: static isText(filePath) [243 tok]
82: static isCode(filePath) [149 tok]
113: static getType(filePath) [209 tok]
158: static isSkippedDirectory(name) [21 tok]
167: static getExtension(filePath) [34 tok]
lib/analyzers/go-method-analyzer.js (score 0.381, 1056 tokens)
9: extractMethods(content, filePath) [385 tok]
60: getLineNumber(content, index) [25 tok]
64: isInComment(content, index) [175 tok]
87: isKeyword(name) [230 tok]
140: extractMethodContent(content, methodName) [205 tok]
index.js (score 0.248, 574 tokens)
test/format-converter.test.js (score 0.170, 1286 tokens)
24: FormatRegistry.mockImplementation(function () [16 tok]8. Review context for a commit
ctxman pr writes what a reviewer needs about a branch or commit range: the summary, the commit messages, the changed files, the source of every changed method, the diff and the related tests, dropping the least important parts to stay under --budget. Here for the commit that added ctxman count (the diff is left out to keep the page short):
$ ctxman pr --base 805d404~1 --head 805d404 --no-diff --budget 2500
# Pull request context: `805d404` into `805d404~1`
## Summary
- Base: 805d404~1 (cf6d566)
- Head: 805d404 (805d404)
- Merge base: cf6d566
- Commits: 1
- Files changed: 12 (7 modified, 5 added), +744 -28 lines
- Changed methods: 39 (23 added, 16 modified); deleted methods: 0
- Related tests: 27
- Impact (heuristic): critical
- Tokens: ~2,412 total; summary 229, commits 416, files 234, methods 1,533
- Diff: not included (--no-diff)
- Budget: 2,500 tokens; dropped: 27 related test path(s); source of 22 changed method(s) (bin/cli.js:printHelp, bin/cli.js:main, lib/count/index.js:countForModel, bin/cm-count.js:printResult, lib/analyzers/token-calculator.js:printHeader, +17 more)
## Commits (1)
### `805d404` feat(count): count tokens with each model's encoding, exact via api
Claude, 2026-10-09
> Every count used cl100k_base, which is GPT-4's encoding: OpenAI models
> since GPT-4o use o200k_base, and Claude, Gemini and the other vendors
> use their own tokenizers, so the "Exact" label was only true for GPT-4.
>
> - --target-model picks the tiktoken encoding: o200k_base for GPT-4o,
> GPT-4.1, GPT-5, GPT-6 and o-series models, cl100k_base otherwise.
> - Labels say when a count only approximates the model's tokenizer
> (Claude, Gemini and other vendors, GPT-6 whose encoding is
> unpublished, and the estimation fallback, which was labelled "Exact").
> The @anthropic-ai/tokenizer package is the Claude 2 tokenizer and is
> now labelled as an approximation.
> - New `ctxman count [path|-] --model ID [--api] [--json]`: counts a
> project or piped text for a model and checks it against the window.
> With --api it counts Claude models exactly through Anthropic's
> count_tokens endpoint (free, nothing generated; needs the
> @anthropic-ai/sdk package and ANTHROPIC_API_KEY), batching files into
> as few requests as possible and redacting secrets before sending.
> - tiktoken moves to dependencies so a plain `npm install ctxman` counts
> exactly instead of estimating.
>
> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
> Claude-Session: https://claude.ai/code/session_014PDhYwtj4A1a2USJPtyAYG
## Changed files (12)
| Status | File | + | - |
| --- | --- | ---: | ---: |
| modified | `bin/cli.js` | 7 | 0 |
| added | `bin/cm-count.js` | 147 | 0 |
| modified | `lib/analyzers/token-calculator.js` | 18 | 3 |
| added | `lib/count/index.js` | 100 | 0 |
| added | `lib/utils/claude-token-counter.js` | 151 | 0 |
| modified | `lib/utils/token-utils.js` | 37 | 8 |
| modified | `lib/utils/tokenizer-adapter.js` | 60 | 14 |
| modified | `package-lock.json` | 1 | 2 |
| modified | `package.json` | 1 | 1 |
| added | `test/count-command.test.js` | 163 | 0 |
| modified | `test/token-calculator-coverage.test.js` | 1 | 0 |
| added | `test/tokenizer-encoding.test.js` | 58 | 0 |
…9. File contents within a budget
ctxman pack writes the files themselves as one document for a model: the tree first, then each file, then your instructions. With --compress only signatures stay and function bodies become a one-line placeholder; --budget fills the budget with the files ctxman map ranks highest. Here the four core modules in 1,885 tokens:
$ ctxman pack --compress --budget 3000 --style markdown
## Directory structure
```
lib/
core/
Analyzer.js
ContextBuilder.js
Reporter.js
Scanner.js
```
## Files
### lib/core/Analyzer.js
```javascript
/**
* Analyzer - Token & Method Analysis Module
* v3.0.0 - Modular architecture
*/
import fs from 'fs';
import TokenUtils from '../utils/token-utils.js';
import FileUtils from '../utils/file-utils.js';
import MethodAnalyzer from '../analyzers/method-analyzer.js';
import { getLogger } from '../utils/logger.js';
const logger = getLogger('Analyzer');
export class Analyzer {
constructor(options = {}) { /* … 9 lines */ }
initStats() { /* … 10 lines */ }
async analyze(files) { /* … 14 lines */ }
async analyzeSequential(files) { /* … 11 lines */ }
async analyzeFile(fileInfo) { /* … 36 lines */ }
updateStats(analysis) { /* … 29 lines */ }
detectLanguage(extension) { /* … 22 lines */ }
getStats() { /* … 1 line */ }
reset() { /* … 1 line */ }
…10. Why the code looks like this
ctxman why collects what git and the repository record about a function: the commits that changed its lines with their full messages, the references in them, the owners, the files that change with it, notes and tests. Nothing leaves the machine:
$ ctxman why lib/integrations/git/GitClient.js:isValidGitRef --budget 1800 --limit 3
# Why: `isValidGitRef` in `lib/integrations/git/GitClient.js`
Context for explaining why this code looks the way it does, collected by `ctxman why` from the local git history and the repository (no network).
## Target
- File: `lib/integrations/git/GitClient.js`
- Symbol: `isValidGitRef` (function, declared on line 26)
- Lines: 19-28 of 354
- Revision: working tree (HEAD fda35dbdc7dc)
```javascript
/**
* Check that a user-supplied revision (branch, tag, commit, HEAD~2, a..b) is safe to
* hand to git: allowlisted characters only, and no leading '-' that git would parse
* as an option (e.g. --output=<file>)
* @param {string} ref
* @returns {boolean}
*/
export function isValidGitRef(ref) {
return typeof ref === 'string' && !ref.startsWith('-') && /^[a-zA-Z0-9/\-_.@^~]+$/.test(ref);
}
```
## History
1 commit that changed lines 19-28 (`git log -L`), newest first.
### 1. fix(git): run git without a shell and validate user-supplied refs
`6939d13` · 2026-10-06 · Claude
> GitClient.exec() built a command line and ran it through execSync, so
> any git ref was parsed by /bin/sh. Its only guard rejected ';' and '&'.
> Refs reach it from untrusted input:
> - REST GET /api/v1/diff?since=. The server binds to localhost, has no
> auth token by default and sends CORS *, so a web page can issue the
> request.
> - The MCP git_diff tool's `branch` argument, which a model controls.
> - `--changed-since`.
> `ctxman github` also interpolated the URL's owner/repo/branch into a
> `git clone` command line.
>
> - GitClient.exec() now takes an argument array and uses
> execFileSync('git', args): no shell, arguments reach git verbatim.
> File paths go after `--`.
> - New isValidGitRef()/assertGitRef(). They allowlist ref characters
> and reject a leading '-', so a ref cannot become a git option such as
> --output. getChangedFiles, DiffAnalyzer.getFileDiff,
> compareBranches and getRangeStats validate before running git.
> - REST answers 400 for an invalid `since`. MCP git_diff returns
> isError for an invalid ref or a git failure instead of plain text.
> - GitUtils.cloneRepository validates the branch and runs
> `git clone ... -- <url> <dir>` via execFileSync.
>
> The new test/git-ref-injection.test.js uses a real temp repository. It
> feeds hostile refs (shell substitution, pipes, newlines, option
> injection) to GitClient, DiffAnalyzer and the REST endpoint, and
> checks that nothing runs and normal refs still work. Four of its five
> cases fail before this change. Existing unit tests are updated for the
> argv call shape.
>
> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
> Claude-Session: https://claude.ai/code/session_014PDhYwtj4A1a2USJPtyAYG
## References
- URL https://claude.ai/code/session_014PDhYwtj4A1a2USJPtyAYG (6939d13)
- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (6939d13)
…11. Secrets stay out of the digest
A small project with credentials left in a config file (both keys are fake):
// Left here during a late-night debugging session
export const s3 = { region: 'eu-west-1', accessKeyId: 'AKIAABCDEFGHIJKLMNOP' };
export const github = { token: 'ghp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8' };$ ctxman --cli --gitingest
🔒 Redacted secrets: 1 aws-access-key-id, 1 github-token (disable with --no-redact)
📜 Context version created: ctx-v001
Files: 2
Tokens: 109================================================
FILE: src/config.js
================================================
// Left here during a late-night debugging session
export const s3 = { region: 'eu-west-1', accessKeyId: '[REDACTED:aws-access-key-id]' };
export const github = { token: '[REDACTED:github-token]' };Known formats (AWS, GitHub, GitLab, Slack, Stripe, Google, OpenAI and Anthropic keys, PEM private keys) are masked by default; --no-redact keeps them.
12. Ask through MCP
ctxman mcp speaks the Model Context Protocol over stdio, so Claude Desktop, Claude Code, Cursor and other MCP clients can call ctxman directly. ctxman mcp install --client claude-desktop adds it to the client's configuration; this is the entry it writes with --npx:
{
"mcpServers": {
"ctxman": {
"command": "npx",
"args": [
"-y",
"ctxman",
"mcp"
]
}
}
}The server offers 11 tools — analyze_codebase, generate_context, git_diff, search_code, list_methods, repo_map, pr_context, get_symbol, count_tokens, explain_history, pack_context — plus file resources and prompts. A list_methods call, as the client sends it and as ctxman answers:
{
"name": "list_methods",
"arguments": {
"path": "~/ctxman",
"file": "lib/core/ContextBuilder.js"
}
}[
{
"file": "lib/core/ContextBuilder.js",
"methods": [
{
"name": "constructor",
"line": 19,
"tokens": 66
},
{
"name": "build",
"line": 36,
"tokens": 161
},
{
"name": "buildMetadata",
"line": 68,
"tokens": 97
},
{
"name": "buildFileList",
"line": 89,
"tokens": 125
},
{
"name": "buildMethodList",
"line": 118,
"tokens": 92
},
{
"name": "applySmartFiltering",
"line": 141,
"tokens": 201
},
{
"name": "prioritizeFiles",
"line": 173,
"tokens": 235
},
{
"name": "calculateCoreScore",
"line": 211,
"tokens": 171
},
{
"name": "calculateBalancedScore",
"line": 237,
"tokens": 50
},
{
"name": "optimizeForLLM",
"line": 248,
"tokens": 192
},
{
"name": "generateCompactPaths",
"line": 282,
"tokens": 58
},
{
"name": "getSummary",
"line": 298,
"tokens": 150
}
]
}
]13. A hard token budget over REST
ctxman serve starts a local REST API. POST /api/v1/context with targetTokens fills the budget with the highest-ranked files (core code first) and never goes over it:
$ curl -s localhost:3000/api/v1/context -H 'Content-Type: application/json' \
-d '{"path": "~/ctxman", "targetTokens": 30000}'
kept 12 of 140 files, 29,989 of 319,165 tokens
6,104 lib/why/index.js
2,050 lib/core/ContextBuilder.js
1,996 lib/core/Reporter.js
1,376 lib/core/Scanner.js
1,079 lib/core/Analyzer.js
3,315 lib/pack/index.js
…Regenerate this page
npm ci
node website/scripts/build-showcase.mjsThe script clones the repository into a temporary directory, runs every step above and writes website/.vitepress/data/showcase.json, which this page and the home page read.