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 +75 -44
- package/bin/diffctx.js +3 -3
- package/checksums.json +4 -4
- package/install.js +31 -12
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
[](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
|
|
4
4
|
[](https://pypi.org/project/diffctx/)
|
|
5
5
|
[](https://crates.io/crates/diffctx)
|
|
6
|
+
[](https://www.npmjs.com/package/diffctx)
|
|
6
7
|
[](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
|
|
10
|
-
lines
|
|
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
|
|
14
|
-
>
|
|
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
|
|
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
|
-

|
|
67
|
+

|
|
63
68
|
|
|
64
|
-
*`diffctx . --diff HEAD~1` selects only the fragments
|
|
65
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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).
|
|
87
|
-
|
|
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
|
-
|
|
124
|
-
`
|
|
125
|
-
|
|
126
|
-
`<
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
```
|
|
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
|
|
176
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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.
|
|
3
|
-
"diffctx-1.12.
|
|
4
|
-
"diffctx-1.12.
|
|
5
|
-
"diffctx-1.12.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
"
|
|
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",
|