sift-cli 1.0.0__tar.gz

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.
Files changed (160) hide show
  1. sift_cli-1.0.0/.gitattributes +21 -0
  2. sift_cli-1.0.0/.github/workflows/ci.yml +40 -0
  3. sift_cli-1.0.0/.github/workflows/release.yml +77 -0
  4. sift_cli-1.0.0/.gitignore +10 -0
  5. sift_cli-1.0.0/LICENSE +21 -0
  6. sift_cli-1.0.0/PKG-INFO +326 -0
  7. sift_cli-1.0.0/README.md +306 -0
  8. sift_cli-1.0.0/notlar/00-PLAN.md +155 -0
  9. sift_cli-1.0.0/notlar/01-YAKALAMA.md +97 -0
  10. sift_cli-1.0.0/notlar/02-MODEL-KOPRUSU.md +115 -0
  11. sift_cli-1.0.0/notlar/03-DAMITMA.md +116 -0
  12. sift_cli-1.0.0/notlar/04-GUVENLIK-AGI.md +135 -0
  13. sift_cli-1.0.0/notlar/05-DIL-BAGIMSIZLIGI.md +260 -0
  14. sift_cli-1.0.0/notlar/06-DOSYA-TASLAGI.md +348 -0
  15. sift_cli-1.0.0/notlar/07-MCP-SUNUCUSU.md +194 -0
  16. sift_cli-1.0.0/notlar/08-BUTCE-VE-OLCUM.md +323 -0
  17. sift_cli-1.0.0/notlar/09-ARKA-PLAN-KOMUTLARI.md +127 -0
  18. sift_cli-1.0.0/notlar/10-GIZLILIK.md +108 -0
  19. sift_cli-1.0.0/notlar/11-YAYIN.md +110 -0
  20. sift_cli-1.0.0/notlar/12-CAGIRANIN-SOZ-HAKKI.md +51 -0
  21. sift_cli-1.0.0/notlar/13-DOSYA-DAMITMA.md +39 -0
  22. sift_cli-1.0.0/notlar/14-ARAYAN-PEEK.md +38 -0
  23. sift_cli-1.0.0/notlar/15-COKLU-IS.md +58 -0
  24. sift_cli-1.0.0/notlar/16-HAFIZA.md +41 -0
  25. sift_cli-1.0.0/notlar/17-YOGUN-ARACLAR.md +39 -0
  26. sift_cli-1.0.0/notlar/18-KAPSAM.md +52 -0
  27. sift_cli-1.0.0/notlar/19-KALINTI-MUTASYON.md +188 -0
  28. sift_cli-1.0.0/notlar/20-KAYIT-BIRIMI.md +83 -0
  29. sift_cli-1.0.0/notlar/21-SAKLAMA.md +78 -0
  30. sift_cli-1.0.0/notlar/22-YANIT-ONBELLEGI.md +79 -0
  31. sift_cli-1.0.0/notlar/23-OLCUM-BIRIMI.md +67 -0
  32. sift_cli-1.0.0/notlar/24-ANAHTAR.md +69 -0
  33. sift_cli-1.0.0/notlar/25-TEKLIF.md +73 -0
  34. sift_cli-1.0.0/notlar/26-BEKLEMEK.md +147 -0
  35. sift_cli-1.0.0/notlar/27-WINDOWS.md +95 -0
  36. sift_cli-1.0.0/pyproject.toml +60 -0
  37. sift_cli-1.0.0/src/sift/__init__.py +18 -0
  38. sift_cli-1.0.0/src/sift/answers.py +175 -0
  39. sift_cli-1.0.0/src/sift/background.py +444 -0
  40. sift_cli-1.0.0/src/sift/capture.py +240 -0
  41. sift_cli-1.0.0/src/sift/cli.py +820 -0
  42. sift_cli-1.0.0/src/sift/digest.py +101 -0
  43. sift_cli-1.0.0/src/sift/distill.py +670 -0
  44. sift_cli-1.0.0/src/sift/fallback.py +98 -0
  45. sift_cli-1.0.0/src/sift/hook.py +275 -0
  46. sift_cli-1.0.0/src/sift/lines.py +37 -0
  47. sift_cli-1.0.0/src/sift/many.py +51 -0
  48. sift_cli-1.0.0/src/sift/memory.py +94 -0
  49. sift_cli-1.0.0/src/sift/model.py +433 -0
  50. sift_cli-1.0.0/src/sift/outline.py +117 -0
  51. sift_cli-1.0.0/src/sift/peek.py +161 -0
  52. sift_cli-1.0.0/src/sift/privacy.py +145 -0
  53. sift_cli-1.0.0/src/sift/records.py +95 -0
  54. sift_cli-1.0.0/src/sift/server.py +552 -0
  55. sift_cli-1.0.0/src/sift/store.py +499 -0
  56. sift_cli-1.0.0/src/sift/tools.py +76 -0
  57. sift_cli-1.0.0/src/sift/view.py +317 -0
  58. sift_cli-1.0.0/src/sift/watch.py +166 -0
  59. sift_cli-1.0.0/test/budget.py +307 -0
  60. sift_cli-1.0.0/test/conftest.py +151 -0
  61. sift_cli-1.0.0/test/korpus/README.md +73 -0
  62. sift_cli-1.0.0/test/korpus/ansi-progress.json +23 -0
  63. sift_cli-1.0.0/test/korpus/ansi-progress.txt +17 -0
  64. sift_cli-1.0.0/test/korpus/cargo-de.json +29 -0
  65. sift_cli-1.0.0/test/korpus/cargo-de.txt +43 -0
  66. sift_cli-1.0.0/test/korpus/cmake-vi.json +24 -0
  67. sift_cli-1.0.0/test/korpus/cmake-vi.txt +25 -0
  68. sift_cli-1.0.0/test/korpus/cobol-mainframe.json +36 -0
  69. sift_cli-1.0.0/test/korpus/cobol-mainframe.txt +32 -0
  70. sift_cli-1.0.0/test/korpus/docker-ar.json +33 -0
  71. sift_cli-1.0.0/test/korpus/docker-ar.txt +43 -0
  72. sift_cli-1.0.0/test/korpus/dotnet-fr.json +29 -0
  73. sift_cli-1.0.0/test/korpus/dotnet-fr.txt +15 -0
  74. sift_cli-1.0.0/test/korpus/ffmpeg-fa.json +21 -0
  75. sift_cli-1.0.0/test/korpus/ffmpeg-fa.txt +36 -0
  76. sift_cli-1.0.0/test/korpus/gcc-ru.json +27 -0
  77. sift_cli-1.0.0/test/korpus/gcc-ru.txt +21 -0
  78. sift_cli-1.0.0/test/korpus/git-quiet.json +13 -0
  79. sift_cli-1.0.0/test/korpus/git-quiet.txt +9 -0
  80. sift_cli-1.0.0/test/korpus/gotest-zh.json +33 -0
  81. sift_cli-1.0.0/test/korpus/gotest-zh.txt +23 -0
  82. sift_cli-1.0.0/test/korpus/gradle-ko.json +34 -0
  83. sift_cli-1.0.0/test/korpus/gradle-ko.txt +33 -0
  84. sift_cli-1.0.0/test/korpus/journalctl-it.json +30 -0
  85. sift_cli-1.0.0/test/korpus/journalctl-it.txt +23 -0
  86. sift_cli-1.0.0/test/korpus/kubectl-hi.json +43 -0
  87. sift_cli-1.0.0/test/korpus/kubectl-hi.txt +41 -0
  88. sift_cli-1.0.0/test/korpus/latex-pl.json +24 -0
  89. sift_cli-1.0.0/test/korpus/latex-pl.txt +38 -0
  90. sift_cli-1.0.0/test/korpus/maven-tr.json +40 -0
  91. sift_cli-1.0.0/test/korpus/maven-tr.txt +36 -0
  92. sift_cli-1.0.0/test/korpus/mix-he.json +31 -0
  93. sift_cli-1.0.0/test/korpus/mix-he.txt +19 -0
  94. sift_cli-1.0.0/test/korpus/psql-es.json +25 -0
  95. sift_cli-1.0.0/test/korpus/psql-es.txt +26 -0
  96. sift_cli-1.0.0/test/korpus/pytest-en.json +26 -0
  97. sift_cli-1.0.0/test/korpus/pytest-en.txt +30 -0
  98. sift_cli-1.0.0/test/korpus/rspec-id.json +31 -0
  99. sift_cli-1.0.0/test/korpus/rspec-id.txt +21 -0
  100. sift_cli-1.0.0/test/korpus/terraform-pt.json +20 -0
  101. sift_cli-1.0.0/test/korpus/terraform-pt.txt +40 -0
  102. sift_cli-1.0.0/test/korpus/vite-ja.json +39 -0
  103. sift_cli-1.0.0/test/korpus/vite-ja.txt +28 -0
  104. sift_cli-1.0.0/test/korpus/zig-sw.json +32 -0
  105. sift_cli-1.0.0/test/korpus/zig-sw.txt +22 -0
  106. sift_cli-1.0.0/test/korpus-kaynak/clojure-rapor.json +39 -0
  107. sift_cli-1.0.0/test/korpus-kaynak/clojure-rapor.txt +31 -0
  108. sift_cli-1.0.0/test/korpus-kaynak/cpp-baslik.json +25 -0
  109. sift_cli-1.0.0/test/korpus-kaynak/cpp-baslik.txt +35 -0
  110. sift_cli-1.0.0/test/korpus-kaynak/elixir-onbellek.json +46 -0
  111. sift_cli-1.0.0/test/korpus-kaynak/elixir-onbellek.txt +40 -0
  112. sift_cli-1.0.0/test/korpus-kaynak/go-kuyruk.json +38 -0
  113. sift_cli-1.0.0/test/korpus-kaynak/go-kuyruk.txt +50 -0
  114. sift_cli-1.0.0/test/korpus-kaynak/haskell-ayristirici.json +37 -0
  115. sift_cli-1.0.0/test/korpus-kaynak/haskell-ayristirici.txt +44 -0
  116. sift_cli-1.0.0/test/korpus-kaynak/hcl-altyapi.json +20 -0
  117. sift_cli-1.0.0/test/korpus-kaynak/hcl-altyapi.txt +44 -0
  118. sift_cli-1.0.0/test/korpus-kaynak/java-depo.json +46 -0
  119. sift_cli-1.0.0/test/korpus-kaynak/java-depo.txt +48 -0
  120. sift_cli-1.0.0/test/korpus-kaynak/makefile-yapi.json +45 -0
  121. sift_cli-1.0.0/test/korpus-kaynak/makefile-yapi.txt +27 -0
  122. sift_cli-1.0.0/test/korpus-kaynak/python-akis.json +40 -0
  123. sift_cli-1.0.0/test/korpus-kaynak/python-akis.txt +46 -0
  124. sift_cli-1.0.0/test/korpus-kaynak/qqzz-uydurma.json +40 -0
  125. sift_cli-1.0.0/test/korpus-kaynak/qqzz-uydurma.txt +35 -0
  126. sift_cli-1.0.0/test/korpus-kaynak/ruby-fatura.json +48 -0
  127. sift_cli-1.0.0/test/korpus-kaynak/ruby-fatura.txt +37 -0
  128. sift_cli-1.0.0/test/korpus-kaynak/rust-matris.json +42 -0
  129. sift_cli-1.0.0/test/korpus-kaynak/rust-matris.txt +42 -0
  130. sift_cli-1.0.0/test/korpus-kaynak/shell-dagitim.json +47 -0
  131. sift_cli-1.0.0/test/korpus-kaynak/shell-dagitim.txt +40 -0
  132. sift_cli-1.0.0/test/korpus-kaynak/sql-sema.json +33 -0
  133. sift_cli-1.0.0/test/korpus-kaynak/sql-sema.txt +37 -0
  134. sift_cli-1.0.0/test/korpus_reader.py +134 -0
  135. sift_cli-1.0.0/test/languages.py +129 -0
  136. sift_cli-1.0.0/test/mutations.py +1450 -0
  137. sift_cli-1.0.0/test/outlines.py +162 -0
  138. sift_cli-1.0.0/test/test_answers.py +206 -0
  139. sift_cli-1.0.0/test/test_background.py +660 -0
  140. sift_cli-1.0.0/test/test_capture.py +290 -0
  141. sift_cli-1.0.0/test/test_cli.py +742 -0
  142. sift_cli-1.0.0/test/test_digest.py +108 -0
  143. sift_cli-1.0.0/test/test_distill.py +614 -0
  144. sift_cli-1.0.0/test/test_fallback.py +90 -0
  145. sift_cli-1.0.0/test/test_gc.py +187 -0
  146. sift_cli-1.0.0/test/test_hook.py +75 -0
  147. sift_cli-1.0.0/test/test_hook_setup.py +221 -0
  148. sift_cli-1.0.0/test/test_kaynak.py +209 -0
  149. sift_cli-1.0.0/test/test_korpus.py +271 -0
  150. sift_cli-1.0.0/test/test_lines.py +118 -0
  151. sift_cli-1.0.0/test/test_many.py +166 -0
  152. sift_cli-1.0.0/test/test_model.py +436 -0
  153. sift_cli-1.0.0/test/test_outline.py +247 -0
  154. sift_cli-1.0.0/test/test_package.py +95 -0
  155. sift_cli-1.0.0/test/test_privacy.py +180 -0
  156. sift_cli-1.0.0/test/test_records.py +149 -0
  157. sift_cli-1.0.0/test/test_server.py +330 -0
  158. sift_cli-1.0.0/test/test_setup.py +159 -0
  159. sift_cli-1.0.0/test/test_tokens.py +184 -0
  160. sift_cli-1.0.0/test/wire.py +102 -0
@@ -0,0 +1,21 @@
1
+ # The corpus is data, not source, and its bytes are the measurement.
2
+ #
3
+ # Git's newline conversion is helpful for code and destructive here. Checked out
4
+ # on Windows with the default `core.autocrlf`, every `\n` in a sample becomes
5
+ # `\r\n` -- and these samples exist precisely to hold line endings nobody else
6
+ # counts the same way: a NEL from an EBCDIC conversion, a form feed, a progress
7
+ # bar that redraws with carriage returns. Convert them and the corpus stops
8
+ # being a record of what tools actually print and becomes a record of what git
9
+ # thought it should print.
10
+ #
11
+ # Measured: on windows-latest the `cargo-de` sample arrived with CRLF and the
12
+ # test that proves `str.splitlines()` would miscount it found nothing to prove.
13
+ test/korpus/** -text
14
+ test/korpus-kaynak/** -text
15
+
16
+ # Source keeps LF in the repository whatever the checkout does, so a diff never
17
+ # turns into every line at once.
18
+ *.py text eol=lf
19
+ *.md text eol=lf
20
+ *.toml text eol=lf
21
+ *.yml text eol=lf
@@ -0,0 +1,40 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ check:
14
+ # Windows is here on purpose. `capture.py` takes a different path there --
15
+ # no process groups, no selector on a pipe -- and a path nobody runs is a
16
+ # path nobody knows about.
17
+ runs-on: ${{ matrix.os }}
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ os: [ubuntu-latest, macos-latest, windows-latest]
22
+ python: ["3.12", "3.13"]
23
+
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - uses: astral-sh/setup-uv@v5
28
+ with:
29
+ enable-cache: true
30
+
31
+ - name: Install
32
+ run: uv sync --all-extras --dev
33
+
34
+ - name: Lint
35
+ run: uv run ruff check src test
36
+
37
+ - name: Test
38
+ # SIFT_LIVE is deliberately unset: the suite must not depend on a key or
39
+ # a network. The one test that calls NVIDIA skips itself without it.
40
+ run: uv run pytest -q --no-header
@@ -0,0 +1,77 @@
1
+ name: release
2
+
3
+ # Publishing happens on a tag and nowhere else. A push to main must never be
4
+ # able to put something on PyPI: a version that exists cannot be taken back, and
5
+ # the difference between "merged" and "released" is the whole point of a tag.
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ # The same checks CI runs, run again here against the exact commit the tag
16
+ # points at. CI having been green on that commit earlier is not the same
17
+ # claim: a tag can be moved, and a release is the one place to be sure.
18
+ check:
19
+ runs-on: ${{ matrix.os }}
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ os: [ubuntu-latest, macos-latest, windows-latest]
24
+ python: ["3.12", "3.13"]
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v5
28
+ with:
29
+ enable-cache: true
30
+ - name: Install
31
+ run: uv sync --all-extras --dev
32
+ - name: Lint
33
+ run: uv run ruff check src test
34
+ - name: Test
35
+ run: uv run pytest -q --no-header
36
+
37
+ build:
38
+ needs: check
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: astral-sh/setup-uv@v5
43
+
44
+ # The tag and the version in the package have to be the same thing. They
45
+ # are written in two places by two people at two times, so they are
46
+ # compared here rather than trusted -- a wheel whose name disagrees with
47
+ # its tag is a release nobody can reason about afterwards.
48
+ - name: The tag and the version agree
49
+ run: |
50
+ tag="${GITHUB_REF_NAME#v}"
51
+ version=$(uv run python -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
52
+ echo "tag=$tag version=$version"
53
+ test "$tag" = "$version"
54
+
55
+ - name: Build
56
+ run: uv build
57
+
58
+ - uses: actions/upload-artifact@v4
59
+ with:
60
+ name: dist
61
+ path: dist/
62
+
63
+ publish:
64
+ needs: build
65
+ runs-on: ubuntu-latest
66
+ environment: pypi
67
+ # Trusted publishing: PyPI is told which repository and which workflow may
68
+ # publish this project, and GitHub signs a short-lived token to prove it.
69
+ # No API token is stored anywhere, so there is none to leak or to rotate.
70
+ permissions:
71
+ id-token: write
72
+ steps:
73
+ - uses: actions/download-artifact@v4
74
+ with:
75
+ name: dist
76
+ path: dist/
77
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ uv.lock
10
+ .mutating.json
sift_cli-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 slmnys
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,326 @@
1
+ Metadata-Version: 2.5
2
+ Name: sift-cli
3
+ Version: 1.0.0
4
+ Summary: Runs your command, then gives the model only the lines that matter.
5
+ Project-URL: Homepage, https://github.com/slymnysr/sift
6
+ Author: slmnys
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: cli,context,distillation,llm,mcp
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Build Tools
16
+ Requires-Python: >=3.12
17
+ Provides-Extra: mcp
18
+ Requires-Dist: mcp>=2; extra == 'mcp'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # sift
22
+
23
+ Runs your command, then gives the model only the lines that matter.
24
+
25
+ A test suite prints 4,000 lines and eleven of them are the failure. A build
26
+ prints a progress bar that redraws 900 times. An install lists every package it
27
+ touched. All of it lands in the conversation, and — this is the part that costs
28
+ — it is re-sent in full on every turn that follows.
29
+
30
+ `sift` runs the command itself, keeps every byte on disk, and hands the model a
31
+ view: the failures, the summary, the lines a reader would actually stop on. The
32
+ rest is marked, not deleted.
33
+
34
+ ```
35
+ $ sift run -- pytest
36
+ FAILED test/test_auth.py::test_expired_token - assert 401 == 200
37
+ ...
38
+ ─ 3,914 lines not shown · sift peek 9f2c41ab for any of them ─
39
+ === 1 failed, 212 passed in 18.4s ===
40
+ ```
41
+
42
+ A file you did not produce — a log, a saved CI transcript, a crash dump — gets
43
+ the same treatment without being run:
44
+
45
+ ```
46
+ $ sift digest ci-run-8812.log
47
+ 2026-09-02T04:11:07 Building 214 targets
48
+ ─ 38,904 lines not shown · sift peek ci-run-8812.log for any of them ─
49
+ ERROR: //src/parser:parse_test failed in 4.1s
50
+ ─ 1,022 lines not shown · sift peek ci-run-8812.log for any of them ─
51
+ FAILED: 1 of 214 targets
52
+ ```
53
+
54
+ A list is not read in lines. A JSON array written for a machine often has no
55
+ newlines at all, and there is nothing in a line of one worth choosing, so the
56
+ record becomes the unit and the same rules hold:
57
+
58
+ ```
59
+ $ sift digest export.json
60
+ {"id": 3, "status": "failed", "error": "connection refused"}
61
+ ─ 412 records not shown · sift peek export.json for the text they came from ─
62
+ {"id": 416, "status": "ok"}
63
+ ```
64
+
65
+ The same question, asked about source code instead, is a table of
66
+ contents:
67
+
68
+ ```
69
+ $ sift outline src/parser.rs
70
+ pub struct Parser {
71
+ pub fn parse(input: &str) -> Result<Ast, Error> {
72
+ ─ 34 lines not shown · sift peek src/parser.rs for any of them ─
73
+ impl Iterator for Tokens {
74
+ ```
75
+
76
+ And a command that does not end — a dev server, a log tail, a build you want to
77
+ keep working during — is started and then read a slice at a time:
78
+
79
+ ```
80
+ $ sift run --background -- cargo build --release
81
+ 9f2c41ab
82
+ $ sift follow 9f2c41ab
83
+ warning: unused import: `std::fmt`
84
+ ─ 212 lines not shown · sift peek 9f2c41ab for any of them ─
85
+ error[E0308]: mismatched types
86
+ sift 9f2c41ab · running · new lines 1-247 · 2 shown · nemotron
87
+ $ sift follow 9f2c41ab # only what has arrived since
88
+ $ sift follow --all --wait 30 # every run at once, holding for something new
89
+ $ sift stop 9f2c41ab # ends it, and everything it started
90
+ ```
91
+
92
+ Nothing is shown twice, and the numbers are the run's own: line 247 stays line
93
+ 247 in `sift peek` for as long as the capture exists.
94
+
95
+ ## What decides
96
+
97
+ A free model does. `sift` sends it the numbered lines and asks one question:
98
+ *which numbers matter?* It answers with numbers, and nothing else it says is
99
+ used — the text you read above is printed from the local capture, byte for byte.
100
+
101
+ That is the whole trick, and it is why this is not a summariser. A summariser
102
+ can be wrong about what a line said. `sift` cannot be: it never writes a line,
103
+ it only chooses one.
104
+
105
+ It also means language coverage is not a list. A model reads Turkish, Japanese,
106
+ Arabic and Hindi; it reads Rust, COBOL, Mojo and a language released last week.
107
+ Nothing here enumerates them, so nothing here can be missing one.
108
+
109
+ ## Three rules
110
+
111
+ **Nothing shown is invented.** The judge returns line numbers. Text always comes
112
+ from the local file.
113
+
114
+ **Nothing is thrown away.** `sift peek <handle>` returns the raw capture,
115
+ unchanged. Every gap in a view says how many lines it covers.
116
+
117
+ **Nothing can break your command.** No API key, no network, an overloaded
118
+ endpoint, a nonsense reply, a bug in the distiller, or a decision that nothing
119
+ may leave this machine — every one of these falls back to rules that need none
120
+ of them. The command still runs; you still get its output; you get its exit
121
+ code.
122
+
123
+ ## What leaves the machine
124
+
125
+ One thing: the text of a question. Before it is sent, anything credential-shaped
126
+ is replaced — tokens with a known prefix, JWTs, authorization headers, passwords
127
+ in connection strings, the body of a PEM block.
128
+
129
+ That costs you nothing to read. The model is only ever asked for line numbers,
130
+ and the lines are printed from your own file, so **a line masked on the way out
131
+ is still shown to you in full**.
132
+
133
+ Three switches:
134
+
135
+ ```bash
136
+ SIFT_NO_MODEL=1 # never send anything; use the deterministic view
137
+ SIFT_MASK=0 # send unmasked
138
+ SIFT_CACHE=0 # ask again, even about text already answered
139
+ SIFT_EFFORT=low # let the model think less, and lose some of what matters
140
+ SIFT_PATIENCE=0 # one quick pass only; do not wait out a busy hour
141
+ ```
142
+
143
+ Masking is not complete and does not claim to be: a bare secret shaped like
144
+ nothing in particular gets through. `SIFT_NO_MODEL` is the one that guarantees.
145
+
146
+ The fourth is a measured trade and is off by default. Asked which lines matter
147
+ in a 404-line build, the model writes about 900 tokens of reasoning to produce a
148
+ twelve-token answer, and you wait 15 seconds for it. At `SIFT_EFFORT=low` the
149
+ same question takes 2.4 seconds — and over the corpus it loses 7.8% of the lines
150
+ a reader could not do without, in exactly the places this tool exists for: a
151
+ mainframe job's return code, a crash loop's diagnosis. Speed is available; it is
152
+ not the default, and the price is written down.
153
+
154
+ The third is about not paying twice. An answer already given for exactly these
155
+ bytes and exactly this question is used again instead of bought again — and what
156
+ is kept is the numbers, never the view, so the text is still rendered from your
157
+ own file and the gap still names your own capture. A view that cost no request
158
+ says `(remembered)` where it would otherwise name the model.
159
+
160
+ Captured bytes never leave `$SIFT_HOME` (`~/.cache/sift` by default). Nothing is
161
+ uploaded, nothing is logged elsewhere, and removing a capture directory removes
162
+ everything that was ever kept about it.
163
+
164
+ They also never go away on their own. Nothing here sweeps, expires or tidies in
165
+ the background: `sift gc [DAYS]` is the only thing that deletes a capture, and
166
+ it deletes when you type it and not before. What it leaves is one line per
167
+ handle — when it went and how big it was, never the command — so that a gap
168
+ marker read a fortnight later gets *"removed on the 8th"* instead of the answer
169
+ it would give for a handle you made up.
170
+
171
+ ## Commands
172
+
173
+ ```
174
+ sift run [--timeout SECONDS] [--shell] [--background] [--cwd DIR]
175
+ [--budget LINES] [--keep PATTERN] [--] COMMAND...
176
+ sift follow [HANDLE] [--all] [--wait N]
177
+ what a background run has said since you last looked
178
+ sift stop [HANDLE] end it, and everything it started
179
+ sift outline PATH what a file declares, without its bodies
180
+ sift digest PATH... what is in files somebody else produced
181
+ sift peek HANDLE|PATH [FIRST] [LAST]
182
+ sift hook answer one shell-command event on stdin
183
+ sift tools which dense tools this machine has
184
+ sift tool NAME [ARGS...] run one of them, distilled
185
+ sift memory [TERM] what has been run here before, and how it went
186
+ sift list [COUNT] what is running, and what has been run
187
+ sift stats [COUNT] what the shortening cost, and what it saved
188
+ sift gc [DAYS] remove captures older than that, and say what went
189
+ ```
190
+
191
+ Several paths given to `digest` are asked about at the same time, and `--all`
192
+ follows every running command in one go. Both are the same idea: the waiting is
193
+ the cost, so do it once. `--wait N` holds until a run actually says something
194
+ instead of answering that nothing has happened yet.
195
+
196
+ `sift hook` is the way to catch the shell commands a client runs on its own,
197
+ without a proxy: point the client's pre-tool hook at it and every shell command
198
+ comes here first. **Everything is routed** — nothing here guesses which commands
199
+ are worth catching, because a list of those is a list of tools in disguise and
200
+ how much a command prints is not knowable before it runs. Routing everything
201
+ costs nothing: a view of twelve lines is twelve lines. It fails open, so a bug
202
+ in it leaves your shell exactly as it was, and `SIFT_HOOK=0` switches it off.
203
+
204
+ It is the part of this that pays most and the only part you have to turn on, so
205
+ it offers itself rather than waiting to be found:
206
+
207
+ ```bash
208
+ sift hook --install # says what it gives and what it costs, then asks
209
+ sift hook --uninstall # and takes it back out
210
+ ```
211
+
212
+ It writes one line into your client's settings, keeps a copy of the file as it
213
+ was, and touches nothing else that is in there. `sift run` mentions it once,
214
+ ever, and then stops.
215
+
216
+ `sift tool` runs one of three programs that answer a question *without opening
217
+ the file* — `sg` (ast-grep) for structural search, `diff` (difftastic) for a diff
218
+ that can tell a reindent from a change, `loc` (scc) for the size of a tree. Their
219
+ output is large by nature, which is exactly why they belong here. **No binaries
220
+ ship with this package**: `sift tools` says which of them this machine has and
221
+ what each is called, and installing one stays your decision.
222
+
223
+ `sift memory` asks no model at all. The question is counting — how often, how it
224
+ went, which command has never once worked here — and a model asked to count is
225
+ slower, costs a request and is sometimes wrong. The model decides what cannot be
226
+ computed, and nothing else.
227
+
228
+ `sift stats` says what the shortening saved and what it cost, and keeps those
229
+ two apart: the share is this tool's own arithmetic over bytes it holds, so it is
230
+ exact, while the cost is the endpoint's count of its own tokens, so it is
231
+ measured. A run the endpoint did not count is left out and said so, rather than
232
+ filled in with bytes divided by four.
233
+
234
+ `--keep PATTERN` shows every line matching it whatever else was chosen and
235
+ whatever the budget says. It is your pattern, not one this tool guessed at —
236
+ the only place a pattern decides anything here, and it decides nothing until
237
+ you type it. `--budget LINES` is the ceiling for one view.
238
+
239
+ ## In an agent
240
+
241
+ The same answers are available over MCP, and that is where they pay most: a tool
242
+ result is re-sent on every turn that follows it, so a build log kept out of a
243
+ transcript goes on staying out of it.
244
+
245
+ ```bash
246
+ pip install "sift-cli[mcp]"
247
+ export SIFT_API_KEY=nvapi-... # yours; see Installing
248
+ claude mcp add sift -- sift-mcp
249
+ ```
250
+
251
+ Without a key the server starts and every tool that would need a model declines
252
+ with an explanation, so the agent falls back to its own shell rather than being
253
+ handed a worse answer it cannot tell apart from a good one.
254
+
255
+ Any client that speaks stdio will do — the command is `sift-mcp`. It offers
256
+ `run`, `follow`, `outline`, `digest`, `digest_many`, `tool` and `peek`. `list`
257
+ and `stats` are deliberately not offered: they would hand a model every command lately run on this machine,
258
+ including the ones it never asked about, and the person at a terminal already
259
+ has that access while a model connecting over a socket does not.
260
+
261
+ Because a client never sees stderr, the last line of every result says what you
262
+ are looking at: which handle, how the command ended, and whether a model chose
263
+ the lines or none could be reached.
264
+
265
+ ## Installing
266
+
267
+ ```bash
268
+ pip install sift-cli # the command line, no dependencies at all
269
+ pip install "sift-cli[mcp]" # and the MCP server
270
+ ```
271
+
272
+ The package is `sift-cli` and the commands are `sift` and `sift-mcp`. The names
273
+ differ because `sift-mcp` on PyPI belongs to somebody else's project — an
274
+ unrelated MCP server about authorising agent actions. Nothing here is theirs and
275
+ nothing there is this.
276
+
277
+ Python 3.12 or newer, and no dependencies for the command line.
278
+
279
+ ### You need your own key
280
+
281
+ **This is not optional and it is not shipped.** `sift` asks a free NVIDIA model
282
+ which lines matter, and that key has to be yours — one cannot be bundled and one
283
+ cannot be shared. It is free, and it takes a minute:
284
+
285
+ 1. Get a key at **<https://build.nvidia.com>**
286
+ 2. Put it anywhere `sift` looks:
287
+
288
+ ```bash
289
+ export SIFT_API_KEY=nvapi-... # or NVIDIA_API_KEY
290
+ # or, once and for good:
291
+ mkdir -p ~/.config/nvidia && echo 'nvapi-...' > ~/.config/nvidia/api_key
292
+ ```
293
+
294
+ Without it the two callers are answered differently, on purpose:
295
+
296
+ - **At a terminal** everything still runs — the command, the bytes, the exit
297
+ code, the third rule — and a loud banner says no model chose these lines and
298
+ that you are looking at the ends of the output.
299
+ - **Over MCP** the tools decline and say why, and tell the agent to use its own
300
+ shell instead. A person can see a degraded view and judge it; a model is handed
301
+ a short text with a footer it has no reason to distrust, and quietly worse is
302
+ the one thing this will not do to a reader who cannot check.
303
+
304
+ If you *want* to run without a model, say so with `SIFT_NO_MODEL=1`. That is a
305
+ decision rather than an oversight, everything works, and nothing lectures you.
306
+
307
+ ## How it was built
308
+
309
+ Twenty-four phases, each one closed before the next began, each with a note in
310
+ `notlar/` saying what was decided and what it cost. `notlar/00-PLAN.md` is the
311
+ arc, including the things that were deliberately not built and why.
312
+
313
+ The tests are in `test/`. Beside them is `test/mutations.py`, which breaks each
314
+ rule the code follows — 162 of them, one at a time — and checks that the suite
315
+ notices. A green suite says the tests did not object to *this* version of the
316
+ code, not that they would object to a worse one.
317
+
318
+ It also says what happens when that battery is interrupted, because it was: a
319
+ break left on disk survived every ordinary test run and took the machine down
320
+ six times before anybody looked. `test/conftest.py` repairs one now, and the
321
+ rule it was breaking is kept twice over, so that no single edit anywhere can
322
+ turn `sift stop` into a signal to everything you own. That is `notlar/19`.
323
+
324
+ ## License
325
+
326
+ MIT.