diffctx 1.12.1 → 1.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,15 +3,16 @@
3
3
  [![CI](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml/badge.svg)](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
4
4
  [![PyPI](https://img.shields.io/pypi/v/diffctx)](https://pypi.org/project/diffctx/)
5
5
  [![crates.io](https://img.shields.io/crates/v/diffctx)](https://crates.io/crates/diffctx)
6
+ [![npm](https://img.shields.io/npm/v/diffctx)](https://www.npmjs.com/package/diffctx)
6
7
  [![License](https://img.shields.io/pypi/l/diffctx)](https://pypi.org/project/diffctx/)
7
8
 
8
9
  **diffctx selects the minimum code an LLM needs to review a git diff.**
9
- Instead of pasting whole files, it walks the dependency graph from the changed
10
- lines outward and stops as soon as additional context stops paying for itself.
10
+ Instead of pasting whole files, it walks the dependency graph outward from the
11
+ changed lines and stops once more context stops paying for itself.
11
12
 
12
13
  > Coming from [`treemapper`](https://pypi.org/project/treemapper/)? That name is
13
- > deprecated — it was a thin wrapper around diffctx. Every command, flag, and
14
- > API call works unchanged: `treemapper` → `diffctx`, `treemapper-mcp` → `diffctx-mcp`.
14
+ > deprecated. Every command, flag, and API call works unchanged:
15
+ > `treemapper` → `diffctx`, `treemapper-mcp` → `diffctx-mcp`.
15
16
 
16
17
  ## Why not just use `tree` or repomix?
17
18
 
@@ -35,21 +36,25 @@ pip install diffctx # or: into an active environment
35
36
  pipx install 'diffctx[mcp]' # + MCP server for AI assistants
36
37
  ```
37
38
 
38
- The `[tree-sitter]` extra adds AST-level parsing for more accurate context
39
- selection across 30+ languages.
40
-
41
39
  Without Python:
42
40
 
43
41
  ```bash
44
42
  cargo install diffctx # native CLI from crates.io
43
+ npx diffctx . --diff HEAD~1 # npm wrapper over the native binary
45
44
  docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1
46
45
  ```
47
46
 
47
+ The image runs as a non-root user (uid 10001) and writes to stdout — the
48
+ native binary has no `-o` flag, so redirect to capture: `... --diff HEAD~1 >
49
+ context.yaml`.
50
+
48
51
  Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
49
52
  are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
50
53
  The native binary covers diff mode with YAML/JSON output; tree mode, Markdown
51
54
  output, the `graph` subcommand and the MCP server live in the Python package.
52
- `cargo add diffctx` embeds the selection pipeline in a Rust project
55
+ `cargo add diffctx` embeds the pipeline in a Rust project — the library is
56
+ imported as `_diffctx` (`use _diffctx::pipeline::build_diff_context`), since
57
+ the crate doubles as the Python extension module
53
58
  ([docs.rs](https://docs.rs/diffctx)).
54
59
 
55
60
  ## Quick start
@@ -59,19 +64,18 @@ diffctx . --diff HEAD~1 # smart context for last commit → paste into Cla
59
64
  diffctx . -f md -c # full codebase export → clipboard in Markdown
60
65
  ```
61
66
 
62
- ![diffctx demo](https://raw.githubusercontent.com/nikolay-e/diffctx/main/docs/demo.gif)
67
+ ![diffctx demo](https://raw.githubusercontent.com/nikolay-e/diffctx/main/docs/demo/demo.gif)
63
68
 
64
- *`diffctx . --diff HEAD~1` selects only the fragments — functions, imports,
65
- type definitions — that an LLM actually needs to review the last commit,
66
- instead of dumping every changed file in full.*
69
+ *`diffctx . --diff HEAD~1` selects only the fragments an LLM needs to review the
70
+ last commit, instead of dumping every changed file in full.*
67
71
 
68
72
  ## Diff context mode
69
73
 
70
- Finds the minimal set of code fragments needed to understand a change —
71
- imports, callers, type definitions, config dependencies — across 50+ file
72
- types. It builds a code graph (imports, co-changes, type refs), propagates
73
- relevance from the changed lines outward, and stops when relevance drops below
74
- `--tau` or the `--budget` token cap is reached.
74
+ Finds the minimal set of fragments needed to understand a change — imports,
75
+ callers, type definitions, config dependencies — across 50+ file types. It
76
+ builds a code graph (imports, co-changes, type refs), propagates relevance
77
+ outward from the changed lines, and stops when relevance drops below `--tau` or
78
+ the `--budget` token cap is hit.
75
79
 
76
80
  | Flag | Default | Description |
77
81
  |-------------|---------|--------------------------------------------------------------------------|
@@ -83,8 +87,10 @@ relevance from the changed lines outward, and stops when relevance drops below
83
87
  | `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
84
88
 
85
89
  Calibration of `--alpha`, `--tau`, and the edge-weight priors:
86
- [`docs/parameter-strategy.md`](docs/parameter-strategy.md). Theory:
87
- [Context-Selection for Git Diff (Zenodo, 2026)](https://doi.org/10.5281/zenodo.18824580).
90
+ [`docs/engineering/parameter-strategy.md`](docs/engineering/parameter-strategy.md).
91
+ Theory:
92
+ [diffctx: Budgeted Typed-Graph Retrieval for Diff-Aware Code Context
93
+ Selection (Zenodo, 2026)](https://doi.org/10.5281/zenodo.18824579).
88
94
 
89
95
  ### `graph` subcommand
90
96
 
@@ -119,11 +125,11 @@ diffctx . --diff HEAD~1 -c # diff context to clipboard
119
125
  <!-- END USAGE -->
120
126
 
121
127
  Every run reports token count and size on stderr — `12,847 tokens
122
- (o200k_base), 52.3 KB` (tiktoken, the GPT-4o tokenizer; `~`-prefixed
123
- approximation above 1 MB). `-c/--copy` sends output to the clipboard via
124
- `pbcopy` (macOS), `clip` (Windows), or `wl-copy`/`xclip`/`xsel` (Linux).
125
- Unreadable files are replaced by placeholders such as `<binary file: N bytes>`,
126
- `<file too large: N bytes>`, or `<unreadable content: not utf-8>`.
128
+ (o200k_base), 52.3 KB` (tiktoken, the GPT-4o tokenizer; `~`-prefixed above
129
+ 1 MB). `-c/--copy` copies output via `pbcopy` (macOS), `clip` (Windows), or
130
+ `wl-copy`/`xclip`/`xsel` (Linux). Unreadable files become placeholders like
131
+ `<binary file: N bytes>`, `<file too large: N bytes>`, or
132
+ `<unreadable content: not utf-8>`.
127
133
 
128
134
  ## Python API
129
135
 
@@ -159,32 +165,42 @@ print(to_yaml(tree))
159
165
 
160
166
  diffctx includes an [MCP](https://modelcontextprotocol.io) server that lets AI
161
167
  assistants (Claude Code, Cursor, Windsurf, etc.) call diff context analysis
162
- automatically during code review. Install with `pip install 'diffctx[mcp]'`
163
- and add to your MCP client config (e.g. `~/.claude/mcp.json` for Claude Code):
164
-
165
- ```json
166
- {
167
- "mcpServers": {
168
- "diffctx": {
169
- "command": "diffctx-mcp"
170
- }
171
- }
172
- }
168
+ automatically during code review. Install with `pip install 'diffctx[mcp]'`,
169
+ then register it — for Claude Code:
170
+
171
+ ```bash
172
+ claude mcp add diffctx -- diffctx-mcp
173
173
  ```
174
174
 
175
- The server exposes a `get_diff_context` tool that assistants call when
176
- reviewing PRs, explaining changes, or investigating broken tests. Configs for
175
+ The server exposes three tools — `get_diff_context`, `get_tree_map`, and
176
+ `get_file_context` — that assistants call when reviewing PRs, explaining
177
+ changes, or investigating broken tests. Tool reference and configs for
177
178
  Cursor, Continue, Windsurf, and Zed:
178
179
  [`src/diffctx/mcp/README.md`](src/diffctx/mcp/README.md).
179
180
 
180
181
  ## Ignore patterns
181
182
 
182
183
  Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
183
- every directory level, with gitignore semantics (negation `!important.log`,
184
- anchored `/root_only.txt`). `.diffctx/whitelist` acts as an include-only
185
- filter, and the output file is always auto-ignored. `--no-default-ignores`
186
- disables the built-in patterns; `--no-ignores` disables all ignore rules
187
- (tree mode only).
184
+ every directory level, with full gitignore semantics (negation `!important.log`,
185
+ anchored `/root_only.txt`). `.diffctx/whitelist` acts as an include-only filter,
186
+ and the output file is always auto-ignored. `--no-default-ignores` disables the
187
+ built-in patterns; `--no-ignores` disables all ignore rules (tree mode only).
188
+
189
+ ## Token cache
190
+
191
+ Diff mode caches per-blob tokenization under
192
+ `~/Library/Caches/diffctx/token-cache` (macOS),
193
+ `$XDG_CACHE_HOME/diffctx/token-cache` (Linux) or
194
+ `%LOCALAPPDATA%\diffctx\token-cache` (Windows). It is a pure speedup: deleting
195
+ it only costs one cold run.
196
+
197
+ | Variable | Effect |
198
+ |----------|--------|
199
+ | `DIFFCTX_TOKEN_CACHE_DIR` | Relocate the cache |
200
+ | `DIFFCTX_TOKEN_CACHE_MAX_BYTES` | Size cap, default `536870912` (512 MB); `0` disables eviction |
201
+
202
+ Eviction is amortized: each run trims one of the cache's 256 shards back under
203
+ its share of the cap, oldest entries first.
188
204
 
189
205
  ## Exit codes
190
206
 
@@ -194,17 +210,32 @@ disables the built-in patterns; `--no-ignores` disables all ignore rules
194
210
  | `1` | Runtime error (bad path, permission denied, etc.) |
195
211
  | `2` | Usage error (invalid flags/arguments) |
196
212
  | `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
197
- | `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename-only diffs list `deleted_files`/`renamed_files` and exit `0` |
213
+ | `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename/lockfile-only diffs list `deleted_files`/`renamed_files`/`lockfile_changes` and exit `0` |
214
+ | `124`| `--diff` exceeded the `--timeout` wall-clock deadline |
198
215
  | `130`| Interrupted (Ctrl-C) |
199
216
  | `141`| Broken pipe (e.g. piping into `head`) |
200
217
 
218
+ ## Repository layout
219
+
220
+ Rust product code lives in `crates/diffctx-native/`; the Python CLI and MCP
221
+ package live in `src/diffctx/`. Evaluation code, immutable datasets, and paper
222
+ artifacts are intentionally separated under `eval/`, `datasets/`, and `paper/`.
223
+ See [repository ownership boundaries](docs/architecture/repository-layout.md)
224
+ for the lifecycle and entry point of each area.
225
+
201
226
  ## License
202
227
 
203
228
  Apache 2.0
204
229
 
230
+ <!-- mcp-name: io.github.nikolay-e/diffctx -->
231
+ <!-- Ownership marker read from the PyPI description by the MCP registry. -->
232
+ <!-- Must survive edits verbatim: one space after the colon, case-sensitive. -->
233
+
205
234
  ---
206
235
 
236
+ - [Documentation site](https://nikolay-e.github.io/diffctx/) — the pipeline
237
+ end to end: diff → fragments → graph → relevance → selection
207
238
  - [Changelog](CHANGELOG.md)
208
239
  - [Security policy](SECURITY.md) — threat model and vulnerability reporting
209
- - [Parameter strategy](docs/parameter-strategy.md) — how `--alpha`,
240
+ - [Parameter strategy](docs/engineering/parameter-strategy.md) — how `--alpha`,
210
241
  `--tau`, and edge weights are calibrated
package/bin/diffctx.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { spawnSync } = require('child_process');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
7
 
8
8
  const binaryName = process.platform === 'win32' ? 'diffctx.exe' : 'diffctx';
9
9
  const binaryPath = path.join(__dirname, binaryName);
package/checksums.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "diffctx-1.12.1-aarch64-apple-darwin.tar.gz": "93eec66793bbf7f7d62f4288e990852da7d7a6ab79d3b6b29bea03ae2ac7e434",
3
- "diffctx-1.12.1-aarch64-unknown-linux-gnu.tar.gz": "55610bfe7856f96a73e87451eddc1aa1a546b84ddc2778cf67738e2333f16f02",
4
- "diffctx-1.12.1-x86_64-pc-windows-msvc.zip": "731177d8af8807e7bc1ba7f021a1723c48da3dfc75bb544415b55b268d5f03b4",
5
- "diffctx-1.12.1-x86_64-unknown-linux-gnu.tar.gz": "f37da41a6381497c15df881f35b8caf324d8ac5fa331118d2736537a246a71b1"
2
+ "diffctx-1.12.2-aarch64-apple-darwin.tar.gz": "fa575452f5120305c7a6c2ce2abd9bc58b93a775fbe430c740f54c5646379295",
3
+ "diffctx-1.12.2-aarch64-unknown-linux-gnu.tar.gz": "c36b6a56241f96a410f291b5069001adf075fa0fbce152818d0ae8eab2af00cc",
4
+ "diffctx-1.12.2-x86_64-pc-windows-msvc.zip": "f2d063f64d9b4b23c28efa9b87e7d18aa57947e75c5fbe37eba99824e03eebfc",
5
+ "diffctx-1.12.2-x86_64-unknown-linux-gnu.tar.gz": "3faa5da74a60e3289a4f86110be397a80b4733ca3d0655e999c0bd53a679197b"
6
6
  }
package/install.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const fs = require('fs');
5
- const os = require('os');
6
- const path = require('path');
7
- const crypto = require('crypto');
8
- const https = require('https');
9
- const { execFileSync } = require('child_process');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const crypto = require('node:crypto');
8
+ const https = require('node:https');
9
+ const { execFileSync } = require('node:child_process');
10
10
 
11
11
  const { version } = require('./package.json');
12
12
  const checksums = require('./checksums.json');
@@ -65,16 +65,33 @@ function verify(archivePath, expectedSha256) {
65
65
  }
66
66
  }
67
67
 
68
+ function systemBinary(candidates, name) {
69
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
70
+ if (!found) {
71
+ throw new Error(`cannot locate ${name}; expected one of ${candidates.join(', ')}`);
72
+ }
73
+ return found;
74
+ }
75
+
68
76
  function extract(archivePath, archiveKind, destinationDir) {
69
77
  if (archiveKind === 'zip') {
78
+ const systemRoot = process.env.SystemRoot || String.raw`C:\Windows`;
79
+ const powershell = systemBinary(
80
+ [
81
+ path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
82
+ path.join(systemRoot, 'SysWOW64', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
83
+ ],
84
+ 'powershell.exe'
85
+ );
70
86
  execFileSync(
71
- 'powershell',
87
+ powershell,
72
88
  ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${destinationDir}' -Force`],
73
89
  { stdio: 'inherit' }
74
90
  );
75
91
  return;
76
92
  }
77
- execFileSync('tar', ['-xzf', archivePath, '-C', destinationDir], { stdio: 'inherit' });
93
+ const tar = systemBinary(['/usr/bin/tar', '/bin/tar'], 'tar');
94
+ execFileSync(tar, ['-xzf', archivePath, '-C', destinationDir], { stdio: 'inherit' });
78
95
  }
79
96
 
80
97
  async function main() {
@@ -96,10 +113,12 @@ async function main() {
96
113
  extract(archivePath, archive, workDir);
97
114
 
98
115
  const binaryName = process.platform === 'win32' ? 'diffctx.exe' : 'diffctx';
99
- fs.copyFileSync(path.join(workDir, binaryName), path.join(binDir, binaryName));
100
- if (process.platform !== 'win32') {
101
- fs.chmodSync(path.join(binDir, binaryName), 0o755);
102
- }
116
+ const extracted = path.join(workDir, binaryName);
117
+ const installed = path.join(binDir, binaryName);
118
+ fs.copyFileSync(extracted, installed);
119
+ // Carry over the archive's mode instead of a hardcoded literal, so the
120
+ // binary stays executable without widening permissions beyond the release.
121
+ fs.chmodSync(installed, fs.statSync(extracted).mode & 0o777);
103
122
  fs.rmSync(workDir, { recursive: true, force: true });
104
123
  }
105
124
 
package/package.json CHANGED
@@ -1,13 +1,21 @@
1
1
  {
2
2
  "name": "diffctx",
3
- "version": "1.12.1",
3
+ "mcpName": "io.github.nikolay-e/diffctx",
4
+ "version": "1.12.2",
4
5
  "description": "Selects the minimum code an LLM needs to review a git diff",
5
6
  "keywords": [
6
7
  "git",
7
8
  "diff",
8
9
  "llm",
9
10
  "code-review",
10
- "context"
11
+ "context",
12
+ "mcp",
13
+ "code-context",
14
+ "context-engineering",
15
+ "ai-agents",
16
+ "diff-context",
17
+ "llm-context",
18
+ "codebase"
11
19
  ],
12
20
  "homepage": "https://github.com/nikolay-e/diffctx",
13
21
  "bugs": "https://github.com/nikolay-e/diffctx/issues",