Skip to content

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.

  1. Everything in gitno configuration
    1,719,753 tokens · 506 files
  2. + .contextignorebuild output and lockfiles out
    967,099 tokens · 488 files
  3. + .contextincludesource only
    319,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
$ 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 lines

More 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:

.contextignore
# Build output and lockfiles: large, generated, nothing for a model to learn
html/
**/package-lock.json
desktop-app/
docs/*.txt
test-repos/
~/ctxman
$ 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: 18

1,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:

.contextinclude
lib/**
bin/**
/index.js
/index.d.ts
/package.json
/README.md
~/ctxman
$ 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
$ 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
$ 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, fits

ctxman 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:

ContextJSONTOONSaving
File list1,18483829%
Method map (-m), 1,444 methods39,60314,22164%

TOON gains most on uniform lists, such as a method map where every row has a name, a line and a token count:

llm-context.toon
$ 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:

.contextinclude
lib/core/**
llm-context.toon
$ 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
$ 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,775

7. 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
$ 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
$ 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
$ 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
$ 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):

src/config.js
// Left here during a late-night debugging session
export const s3 = { region: 'eu-west-1', accessKeyId: 'AKIAABCDEFGHIJKLMNOP' };
export const github = { token: 'ghp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8' };
payments-service
$ 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
digest.txt
================================================
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:

claude_desktop_config.json
{
  "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:

tools/call
{
  "name": "list_methods",
  "arguments": {
    "path": "~/ctxman",
    "file": "lib/core/ContextBuilder.js"
  }
}
result
[
  {
    "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:

ctxman serve
$ 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 ​

bash
npm ci
node website/scripts/build-showcase.mjs

The 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.

Released under the MIT License.