contexta-ai 2.2.0__tar.gz → 2.2.1__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 (144) hide show
  1. contexta_ai-2.2.1/.github/workflows/ci.yml +73 -0
  2. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/.github/workflows/publish-pypi.yml +3 -0
  3. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/CHANGELOG.md +22 -0
  4. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/CONTRIBUTING.md +1 -0
  5. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/PKG-INFO +3 -3
  6. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/README.md +2 -2
  7. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/README.pt-BR.md +2 -2
  8. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/SECURITY.md +4 -3
  9. contexta_ai-2.2.1/assets/dark.png +0 -0
  10. contexta_ai-2.2.1/assets/empty.png +0 -0
  11. contexta_ai-2.2.1/assets/memory.png +0 -0
  12. contexta_ai-2.2.1/assets/result.png +0 -0
  13. contexta_ai-2.2.1/assets/white.png +0 -0
  14. contexta_ai-2.2.1/contexta.py +20 -0
  15. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/__init__.py +1 -1
  16. contexta_ai-2.2.1/contexta_app/__main__.py +4 -0
  17. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/context_engine.py +2 -0
  18. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/fingerprint.py +3 -3
  19. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/focus.py +1 -1
  20. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/insight.py +39 -0
  21. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/models.py +1 -0
  22. contexta_ai-2.2.0/contexta.py → contexta_ai-2.2.1/contexta_app/entry.py +3 -1
  23. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/graph.py +28 -2
  24. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/graph_cli.py +2 -0
  25. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/mcp_server.py +3 -0
  26. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/scanner.py +5 -0
  27. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/theme.py +6 -0
  28. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/ui.py +61 -26
  29. contexta_ai-2.2.1/docs/images/error.png +0 -0
  30. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/version_info.txt +4 -4
  31. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/windows/contexta.iss +1 -1
  32. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/pyproject.toml +2 -3
  33. contexta_ai-2.2.1/requirements-dev.txt +6 -0
  34. contexta_ai-2.2.1/scripts/smoke_wheel.py +101 -0
  35. contexta_ai-2.2.1/tests/test_fingerprint_generic.py +29 -0
  36. contexta_ai-2.2.1/tests/test_graph_cap.py +50 -0
  37. contexta_ai-2.2.1/tests/test_gui_budget_and_layout.py +100 -0
  38. contexta_ai-2.2.1/tests/test_mcp_setup.py +69 -0
  39. contexta_ai-2.2.1/tests/test_packaging.py +49 -0
  40. contexta_ai-2.2.1/tests/test_python_symbols.py +66 -0
  41. contexta_ai-2.2.1/tests/test_symlink_dedup.py +47 -0
  42. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_theme.py +11 -0
  43. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_version_and_brand.py +2 -1
  44. contexta_ai-2.2.0/assets/dark.png +0 -0
  45. contexta_ai-2.2.0/assets/empty.png +0 -0
  46. contexta_ai-2.2.0/assets/memory.png +0 -0
  47. contexta_ai-2.2.0/assets/result.png +0 -0
  48. contexta_ai-2.2.0/assets/white.png +0 -0
  49. contexta_ai-2.2.0/docs/images/error.png +0 -0
  50. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/.github/workflows/build-linux.yml +0 -0
  51. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/.gitignore +0 -0
  52. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/LICENSE +0 -0
  53. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/action.yml +0 -0
  54. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/action.png +0 -0
  55. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/assets/dark.png +0 -0
  56. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/assets/white.png +0 -0
  57. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/brand_assets.py +0 -0
  58. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/cli.png +0 -0
  59. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/graph.png +0 -0
  60. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/icon.ico +0 -0
  61. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/icon.png +0 -0
  62. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/install.png +0 -0
  63. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/logo.png +0 -0
  64. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/logo.svg +0 -0
  65. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/assets/logo@2x.png +0 -0
  66. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/benchmarks/README.md +0 -0
  67. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/benchmarks/bench.py +0 -0
  68. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/benchmarks/results.json +0 -0
  69. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/benchmarks/results.md +0 -0
  70. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/build.bat +0 -0
  71. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/build.sh +0 -0
  72. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/ai_client.py +0 -0
  73. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/budgets.py +0 -0
  74. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/cache.py +0 -0
  75. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/ci_report.py +0 -0
  76. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/cli.py +0 -0
  77. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/cli_style.py +0 -0
  78. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/config.py +0 -0
  79. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/embedded_icon_data.py +0 -0
  80. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/__init__.py +0 -0
  81. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/blobs.py +0 -0
  82. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/constants.py +0 -0
  83. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/excerpts.py +0 -0
  84. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/manifests.py +0 -0
  85. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/pipeline.py +0 -0
  86. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/relations.py +0 -0
  87. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/risk.py +0 -0
  88. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/roles.py +0 -0
  89. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/scoring.py +0 -0
  90. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/selection.py +0 -0
  91. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/summary.py +0 -0
  92. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/engine/surfaces.py +0 -0
  93. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/install.py +0 -0
  94. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/memory.py +0 -0
  95. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/project_meta.py +0 -0
  96. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/redaction.py +0 -0
  97. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/renderer.py +0 -0
  98. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/safety.py +0 -0
  99. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/selector.py +0 -0
  100. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/symbols.py +0 -0
  101. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/syntax.py +0 -0
  102. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_app/utils.py +0 -0
  103. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/contexta_mcp.py +0 -0
  104. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/docs/contexta-1.6-dependency-proposal.md +0 -0
  105. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/docs/contexta.toml.example +0 -0
  106. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/docs/github-action.md +0 -0
  107. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/docs/graph.md +0 -0
  108. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/mdcodebrief.py +0 -0
  109. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/cli_screenshot.py +0 -0
  110. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/gui_screenshots.py +0 -0
  111. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/linux/install-contexta.sh +0 -0
  112. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/make_brand_assets.py +0 -0
  113. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/pyinstaller/contexta-onefile.spec +0 -0
  114. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/pyinstaller/contexta-safe.spec +0 -0
  115. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/pyinstaller/contexta.spec +0 -0
  116. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/pyinstaller/mdcodebrief.spec +0 -0
  117. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/packaging/sync_version.py +0 -0
  118. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/requirements-build.txt +0 -0
  119. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/requirements.txt +0 -0
  120. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/__init__.py +0 -0
  121. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/conftest.py +0 -0
  122. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_action.py +0 -0
  123. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_adoption.py +0 -0
  124. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_benchmark.py +0 -0
  125. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_budget.py +0 -0
  126. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_cli_stdout.py +0 -0
  127. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_cli_style.py +0 -0
  128. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_context_engine.py +0 -0
  129. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_descriptions.py +0 -0
  130. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_engine_layout.py +0 -0
  131. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_graph.py +0 -0
  132. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_graph_cli.py +0 -0
  133. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_install.py +0 -0
  134. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_mcp_v2.py +0 -0
  135. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_memory.py +0 -0
  136. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_memory_ui.py +0 -0
  137. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_pack_graph.py +0 -0
  138. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_performance.py +0 -0
  139. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_polish.py +0 -0
  140. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_renderer.py +0 -0
  141. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_scanner.py +0 -0
  142. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_security.py +0 -0
  143. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_ui.py +0 -0
  144. {contexta_ai-2.2.0 → contexta_ai-2.2.1}/tests/test_version.py +0 -0
@@ -0,0 +1,73 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: ci-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ test:
17
+ name: tests (${{ matrix.os }}, Python ${{ matrix.python }})
18
+ runs-on: ${{ matrix.os }}
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ os: [ubuntu-latest, windows-latest, macos-latest]
23
+ python: ["3.11", "3.13"]
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ with:
27
+ fetch-depth: 0 # the graph and `--for review` read git history
28
+
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ matrix.python }}
32
+ cache: pip
33
+ cache-dependency-path: |
34
+ pyproject.toml
35
+ requirements-dev.txt
36
+
37
+ - name: Linux GUI dependencies
38
+ if: runner.os == 'Linux'
39
+ run: sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb python3-tk
40
+
41
+ - name: Install
42
+ run: python -m pip install --upgrade pip && python -m pip install -e . -r requirements-dev.txt
43
+
44
+ - name: Test (Linux, virtual display)
45
+ if: runner.os == 'Linux'
46
+ run: xvfb-run -a python -m pytest -q
47
+
48
+ - name: Test
49
+ if: runner.os != 'Linux'
50
+ run: python -m pytest -q
51
+
52
+ wheel:
53
+ name: wheel installs and runs
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ - uses: actions/setup-python@v5
58
+ with:
59
+ python-version: "3.11"
60
+ - name: Build the wheel, install it in a clean virtualenv, run the CLI and the MCP server
61
+ run: python -m pip install --upgrade pip build && python scripts/smoke_wheel.py
62
+
63
+ lint:
64
+ name: ruff (informational)
65
+ runs-on: ubuntu-latest
66
+ continue-on-error: true # many pre-existing findings; shown, not enforced yet
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+ - uses: actions/setup-python@v5
70
+ with:
71
+ python-version: "3.11"
72
+ - run: python -m pip install ruff
73
+ - run: ruff check contexta_app contexta.py contexta_mcp.py scripts tests --output-format=github
@@ -29,5 +29,8 @@ jobs:
29
29
  - name: Build package
30
30
  run: python -m build
31
31
 
32
+ - name: Smoke-test the wheel in a clean virtualenv
33
+ run: python scripts/smoke_wheel.py --wheel "$(ls dist/*.whl)"
34
+
32
35
  - name: Publish to PyPI
33
36
  uses: pypa/gh-action-pypi-publish@release/v1
@@ -6,6 +6,28 @@ Versioning: [Semantic Versioning](https://semver.org/)
6
6
 
7
7
  ---
8
8
 
9
+ ## [2.2.1] - 2026-10-24
10
+
11
+ A fix release. **If you installed 2.2.0 or 2.0.0 from PyPI, `contexta` did not start** (`ModuleNotFoundError: No module named 'contexta'`); upgrade.
12
+
13
+ ### Fixed
14
+ - **The PyPI wheel works.** The console script lived in a top-level module the wheel did not include. `main()` is now `contexta_app/entry.py`, `contexta.py` is a thin launcher, and `python -m contexta_app` works. `scripts/smoke_wheel.py` builds the wheel, installs it in a clean virtualenv and checks `contexta --version`, a pack and `contexta serve` over MCP stdio; the publish workflow runs it before uploading
15
+ - `contexta serve --help` printed "unknown option '--help'" and now prints usage
16
+ - The desktop app's editor MCP setup raised `NameError: messagebox` after writing the config. The config it writes now works from the packaged app (`<exe> serve`) and from a pip install (`python -m contexta_app serve`), and an editor config that cannot be parsed is copied to `.bak` instead of being replaced
17
+ - A directory symlink inside the project (`self -> .`) was followed until the 2000-file cap, listing the same files thousands of times. Directory symlinks that stay inside the project are no longer walked; file symlinks and the rule that links leaving the project are ignored are unchanged
18
+ - The project graph shared the 2000-file cap of a pack and hid it, so on a larger repo `graph impact` answered "(none found in graph)". The graph now has its own limit (50000, or `CONTEXTA_GRAPH_MAX_FILES`), and `graph update` and `graph status` warn when it is reached
19
+ - Any desktop Python project was described as "Scan local repositories and generate curated context packs", Contexta's own purpose
20
+ - Python files are now read with `ast` whether or not the optional tree-sitter is installed. Without it, `from .util import slugify` was seen as importing `.util` only, and with it as importing `slugify` as if it were a module (relative imports never resolved to a local file). `--focus slugify` therefore selected different files depending on what happened to be installed. Modules and imported names are now kept apart, and `--focus` matches both
21
+
22
+ ### Added
23
+ - **Token budget** box in the desktop app (`8000`, `8,000` or `32k`), using the same fitter as `--budget`
24
+ - `border_strong` in the theme: inputs, ghost buttons, combo boxes and switch tracks reach 3:1 against their background in both themes
25
+ - CI (`.github/workflows/ci.yml`): the test suite on Linux, Windows and macOS, a wheel smoke test, and ruff in non-blocking mode
26
+
27
+ ### Changed
28
+ - Scan options in the desktop app use one column, so no label is cut off. Entry and Text widgets no longer draw the default white focus ring
29
+ - SECURITY.md and the README now say what touches the network: the one-time download of the `cl100k_base` token encoding, the optional `--ask`, and the GitHub Action's PR comment
30
+
9
31
  ## [2.2.0] - 2026-10-24
10
32
 
11
33
  Contexta now remembers your project. A local graph of files, symbols, tests and git history answers
@@ -8,6 +8,7 @@ Thank you for considering a contribution!
8
8
  - Optional packaging tools such as **Nuitka** or **PyInstaller** must stay out of runtime code paths
9
9
  - Python **3.10+** syntax is acceptable
10
10
  - Every PR must keep the project runnable with `python contexta.py` after `python -m pip install -r requirements.txt`
11
+ - Tests: `python -m pip install -e . -r requirements-dev.txt`, then `python -m pytest -q` (on Linux add `xvfb-run -a` for the GUI tests; the tests that need Pillow or PyYAML skip when those are missing)
11
12
 
12
13
  ## How to contribute
13
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: contexta-ai
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: AI-native codebase context engine. Scans projects and serves intelligent context packs to Claude Code, Cursor, Windsurf, and any MCP-compatible tool.
5
5
  Project-URL: Homepage, https://github.com/pablokaua03/contexta
6
6
  Project-URL: Repository, https://github.com/pablokaua03/contexta
@@ -58,7 +58,7 @@ Contexta reads a codebase, keeps the files that matter for the task, fits them i
58
58
  [![Python](https://img.shields.io/badge/python-3.11%2B-12211c?style=flat-square)](https://python.org)
59
59
  [![License: MIT](https://img.shields.io/badge/license-MIT-12211c?style=flat-square)](LICENSE)
60
60
  [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-12211c?style=flat-square)]()
61
- [![Version](https://img.shields.io/badge/version-2.2.0-12211c?style=flat-square)]()
61
+ [![Version](https://img.shields.io/badge/version-2.2.1-12211c?style=flat-square)]()
62
62
  [![MCP](https://img.shields.io/badge/MCP-compatible-12211c?style=flat-square)]()
63
63
 
64
64
  <br>
@@ -429,7 +429,7 @@ python -m pytest tests/ -k "not test_relation_score"
429
429
  ## Security and behavior
430
430
 
431
431
  - Read-only, with one exception: Contexta never modifies your files. It does keep its project graph in `.contexta/` (names, signatures and relations, never file contents; it ignores itself with a `*` `.gitignore`). Turn it off with `--no-graph` or `graph = false`, move it with `CONTEXTA_GRAPH_DIR`
432
- - No runtime telemetry or network requirement (except optional `--ask` AI calls)
432
+ - No telemetry. Packs, the graph and the MCP server work offline. The only network use: the first token count downloads the `cl100k_base` encoding once (cached by `tiktoken`; offline, Contexta falls back to a character-based estimate), the optional `--ask` calls go to the AI provider you configured, and the GitHub Action posts its PR comment to the GitHub API
433
433
  - Scan limit of 2000 files by default, raised with `--max-files`
434
434
  - Embedded binary/blob payloads are suppressed automatically
435
435
  - Likely secrets are redacted in exports and in MCP `read_files` unless `--no-redact` is set. `--dry-run` shows what would be redacted
@@ -11,7 +11,7 @@ Contexta reads a codebase, keeps the files that matter for the task, fits them i
11
11
  [![Python](https://img.shields.io/badge/python-3.11%2B-12211c?style=flat-square)](https://python.org)
12
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-12211c?style=flat-square)](LICENSE)
13
13
  [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-12211c?style=flat-square)]()
14
- [![Version](https://img.shields.io/badge/version-2.2.0-12211c?style=flat-square)]()
14
+ [![Version](https://img.shields.io/badge/version-2.2.1-12211c?style=flat-square)]()
15
15
  [![MCP](https://img.shields.io/badge/MCP-compatible-12211c?style=flat-square)]()
16
16
 
17
17
  <br>
@@ -382,7 +382,7 @@ python -m pytest tests/ -k "not test_relation_score"
382
382
  ## Security and behavior
383
383
 
384
384
  - Read-only, with one exception: Contexta never modifies your files. It does keep its project graph in `.contexta/` (names, signatures and relations, never file contents; it ignores itself with a `*` `.gitignore`). Turn it off with `--no-graph` or `graph = false`, move it with `CONTEXTA_GRAPH_DIR`
385
- - No runtime telemetry or network requirement (except optional `--ask` AI calls)
385
+ - No telemetry. Packs, the graph and the MCP server work offline. The only network use: the first token count downloads the `cl100k_base` encoding once (cached by `tiktoken`; offline, Contexta falls back to a character-based estimate), the optional `--ask` calls go to the AI provider you configured, and the GitHub Action posts its PR comment to the GitHub API
386
386
  - Scan limit of 2000 files by default, raised with `--max-files`
387
387
  - Embedded binary/blob payloads are suppressed automatically
388
388
  - Likely secrets are redacted in exports and in MCP `read_files` unless `--no-redact` is set. `--dry-run` shows what would be redacted
@@ -11,7 +11,7 @@ O Contexta lê o código, fica com os arquivos que importam para a tarefa, encai
11
11
  [![Python](https://img.shields.io/badge/python-3.11%2B-12211c?style=flat-square)](https://python.org)
12
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-12211c?style=flat-square)](LICENSE)
13
13
  [![Plataforma](https://img.shields.io/badge/plataforma-Windows%20%7C%20macOS%20%7C%20Linux-12211c?style=flat-square)]()
14
- [![Versão](https://img.shields.io/badge/vers%C3%A3o-2.2.0-12211c?style=flat-square)]()
14
+ [![Versão](https://img.shields.io/badge/vers%C3%A3o-2.2.1-12211c?style=flat-square)]()
15
15
  [![MCP](https://img.shields.io/badge/MCP-compat%C3%ADvel-12211c?style=flat-square)]()
16
16
 
17
17
  <br>
@@ -366,7 +366,7 @@ python -m pytest tests/ -k "not test_relation_score"
366
366
  ## Segurança e comportamento
367
367
 
368
368
  - Somente leitura, com uma exceção: o Contexta nunca modifica seus arquivos. Ele guarda o grafo do projeto em `.contexta/` (nomes, assinaturas e relações, nunca o conteúdo dos arquivos; ele se ignora com um `.gitignore` com `*`). Desligue com `--no-graph` ou `graph = false`, mude o local com `CONTEXTA_GRAPH_DIR`
369
- - Sem telemetria ou necessidade de rede (exceto `--ask` opcional)
369
+ - Sem telemetria. Packs, grafo e servidor MCP funcionam offline. Único uso de rede: a primeira contagem de tokens baixa a codificação `cl100k_base` uma vez (guardada em cache pelo `tiktoken`; sem rede, o Contexta usa uma estimativa por caracteres), as chamadas opcionais de `--ask` vão para o provedor de IA que você configurou, e a GitHub Action publica o comentário do PR na API do GitHub
370
370
  - Limite de 2000 arquivos por padrão, ajustável com `--max-files`
371
371
  - Payloads binários/base64 suprimidos automaticamente
372
372
  - Segredos prováveis são redatados nos exports e no `read_files` do MCP, a menos que `--no-redact` seja usado. `--dry-run` mostra o que seria redatado
@@ -21,10 +21,11 @@ You can expect a response within 7 days.
21
21
 
22
22
  ## Security notes
23
23
 
24
- - `Contexta` is a read-only tool; it does not modify, delete, or upload project files
25
- - It makes no network requests of any kind
24
+ - `Contexta` never modifies, deletes, or uploads your project files. It does write its own project graph to `.contexta/` (or `CONTEXTA_GRAPH_DIR`) unless `--no-graph` is set, and `contexta install` writes agent rules and MCP config files you ask for
25
+ - It sends no telemetry. Packs, the project graph and the MCP server do not need the network
26
+ - The network is used in three cases only: the first token count downloads the `cl100k_base` encoding once through `tiktoken` (cached afterwards; offline, Contexta falls back to a character-based estimate), the optional `--ask` feature sends the pack to the AI provider you configured, and the GitHub Action posts its pull request comment to the GitHub API
26
27
  - It uses local runtime dependencies for parsing, token estimation, and repository analysis
27
28
  - On some Linux distributions, `tkinter` may come from a system package such as `python3-tk`
28
29
  - Windows builds use `Nuitka`; Linux packaging uses `PyInstaller`
29
- - All processing happens locally on your machine
30
+ - Analysis happens locally on your machine
30
31
  - Exports replace likely secrets (API keys, private keys, tokens, connection-string passwords) with `[REDACTED]` before the pack is written. This is best-effort and does not make it safe to commit real credentials
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,20 @@
1
+ """
2
+ Contexta — AI-native codebase context engine.
3
+
4
+ Usage:
5
+ contexta serve [--root DIR] Start the MCP server (for Claude Code, Cursor, etc.).
6
+ Tools only read inside --root (repeatable), CONTEXTA_ROOT,
7
+ or the current directory
8
+ contexta graph <command> Ask the project graph: dependents, impact, hot files, repo map
9
+ contexta install <target> Write agent rules or MCP config (cursor, claude, copilot, mcp)
10
+ contexta <project> [opts] Generate a context pack (CLI mode)
11
+ contexta Launch the desktop GUI
12
+ contexta --configure-ai Set up AI API keys
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from contexta_app.entry import main
18
+
19
+ if __name__ == "__main__":
20
+ main()
@@ -2,4 +2,4 @@
2
2
  Contexta application package.
3
3
  """
4
4
 
5
- __version__ = "2.2.0"
5
+ __version__ = "2.2.1"
@@ -0,0 +1,4 @@
1
+ from contexta_app.entry import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -124,10 +124,12 @@ from contexta_app.engine.relations import ( # noqa: F401
124
124
  test_relation_score,
125
125
  )
126
126
  from contexta_app.engine.insight import ( # noqa: F401
127
+ _python_ast_symbols,
127
128
  collect_file_signals,
128
129
  extract_symbols,
129
130
  flatten_tree,
130
131
  make_file_insight,
132
+ python_imported_names,
131
133
  )
132
134
  from contexta_app.engine.roles import ( # noqa: F401
133
135
  _CONTEXTA_MODULE_ROLES,
@@ -326,11 +326,9 @@ def infer_probable_purpose(project_type_label: str | None, domain_label: str | N
326
326
  ("Electron desktop application", "image_editing"): "Provide desktop image or asset-editing workflows through a web-powered desktop shell.",
327
327
  ("Go application", "task_management"): "Provide Go-based services or tooling for task, board, and workflow management operations.",
328
328
  ("Rust application", "graph_analysis"): "Analyze, transform, or serve graph and network data with performance-oriented Rust modules.",
329
- ("Desktop GUI + CLI developer tool", "developer_tooling"): "Scan local repositories and generate curated context packs for AI workflows.",
329
+ ("Desktop GUI + CLI developer tool", "developer_tooling"): "Provide developer tooling, automation, or repository-analysis workflows through a desktop GUI and a command-line entry point.",
330
330
  ("Desktop GUI + CLI developer tool", "image_editing"): "Provide desktop editing workflows for image or asset manipulation through GUI and command-driven tooling.",
331
331
  }
332
- if project_type_label == "Desktop GUI + CLI developer tool":
333
- return "Scan local repositories and generate curated context packs for AI workflows."
334
332
  if (project_type_label, domain_label) in purpose_by_pair:
335
333
  return purpose_by_pair[(project_type_label, domain_label)]
336
334
 
@@ -418,6 +416,8 @@ def infer_probable_purpose(project_type_label: str | None, domain_label: str | N
418
416
  return "Build JVM-based application logic using Scala and functional or object-oriented patterns."
419
417
  if project_type_label == "PHP application":
420
418
  return "Deliver web application functionality using PHP modules and associated server-side tooling."
419
+ if project_type_label == "Desktop GUI + CLI developer tool":
420
+ return "Provide a desktop application with a graphical interface and a command-line entry point."
421
421
  if project_type_label == "Python application":
422
422
  return "Organize Python modules, scripts, and supporting logic into a runnable application."
423
423
  if project_type_label == "JavaScript/TypeScript application":
@@ -32,7 +32,7 @@ def compute_focus_raw(item: FileInsight, query: str) -> float:
32
32
  score = 0.0
33
33
  rel = item.relpath.as_posix().lower()
34
34
  stem = item.path.stem.lower()
35
- imports_blob = " ".join(imported.lower() for imported in item.imports)
35
+ imports_blob = " ".join(imported.lower() for imported in (*item.imports, *item.imported_names))
36
36
  functions_blob = " ".join(func.lower() for func in item.functions)
37
37
  classes_blob = " ".join(cls.lower() for cls in item.classes)
38
38
  content_lower = item.content_lower
@@ -2,7 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import ast
5
6
  import re
7
+ from functools import lru_cache
6
8
  from pathlib import Path
7
9
  from contexta_app.scanner import get_language, read_file_safe
8
10
  from contexta_app.syntax import extract_symbols_with_treesitter
@@ -22,6 +24,7 @@ def make_file_insight(project_path: Path, path: Path) -> FileInsight:
22
24
  content, truncated, total_line_count = read_file_safe(path)
23
25
  lang = get_language(path)
24
26
  functions, classes, imports = extract_symbols(content, lang)
27
+ imported_names = python_imported_names(content) if lang == "python" else []
25
28
  external_imports = [item for item in imports if not item.startswith(".")]
26
29
  return FileInsight(
27
30
  path=path,
@@ -34,10 +37,46 @@ def make_file_insight(project_path: Path, path: Path) -> FileInsight:
34
37
  functions=functions,
35
38
  classes=classes,
36
39
  imports=imports,
40
+ imported_names=imported_names,
37
41
  external_imports=external_imports,
38
42
  )
39
43
 
44
+ @lru_cache(maxsize=64)
45
+ def _python_ast_symbols(content: str) -> tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...], tuple[str, ...]] | None:
46
+ """Top-level functions and classes, imported modules and imported names, from ``ast``.
47
+
48
+ Python is read with the standard library whether or not tree-sitter is installed, so a pack
49
+ does not depend on an optional package. Modules are written the way ``resolve_local_import``
50
+ expects them: ``pkg.core``, or leading dots for a relative import (``.util``).
51
+ """
52
+ try:
53
+ tree = ast.parse(content)
54
+ except (SyntaxError, ValueError, RecursionError):
55
+ return None
56
+ functions = [n.name for n in tree.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
57
+ classes = [n.name for n in tree.body if isinstance(n, ast.ClassDef)]
58
+ modules: list[str] = []
59
+ names: list[str] = []
60
+ for node in ast.walk(tree):
61
+ if isinstance(node, ast.Import):
62
+ modules.extend(alias.name for alias in node.names)
63
+ elif isinstance(node, ast.ImportFrom):
64
+ modules.append("." * (node.level or 0) + (node.module or ""))
65
+ names.extend(alias.name for alias in node.names if alias.name != "*")
66
+ unique = lambda values, limit: tuple(dict.fromkeys(values))[:limit] # noqa: E731
67
+ return unique(functions, 12), unique(classes, 8), unique(modules, 24), unique(names, 40)
68
+
69
+ def python_imported_names(content: str) -> list[str]:
70
+ """Names brought in with ``from x import name``, for focus matching ("slugify" finds its callers)."""
71
+ parsed = _python_ast_symbols(content)
72
+ return list(parsed[3]) if parsed else []
73
+
40
74
  def extract_symbols(content: str, lang: str | None) -> tuple[list[str], list[str], list[str]]:
75
+ if lang == "python":
76
+ parsed = _python_ast_symbols(content)
77
+ if parsed:
78
+ return list(parsed[0]), list(parsed[1]), list(parsed[2])
79
+
41
80
  syntax_symbols = extract_symbols_with_treesitter(content, lang)
42
81
  if syntax_symbols:
43
82
  functions, classes, imports = syntax_symbols
@@ -36,6 +36,7 @@ class FileInsight:
36
36
  functions: list[str] = field(default_factory=list)
37
37
  classes: list[str] = field(default_factory=list)
38
38
  imports: list[str] = field(default_factory=list)
39
+ imported_names: list[str] = field(default_factory=list) # Python: names from `from x import name`
39
40
  local_imports: list[str] = field(default_factory=list)
40
41
  external_imports: list[str] = field(default_factory=list)
41
42
  tags: set[str] = field(default_factory=set)
@@ -1,4 +1,6 @@
1
- """
1
+ """Command-line entry point shared by the `contexta` console script, `python -m contexta_app` and the
2
+ legacy `contexta.py` launcher.
3
+
2
4
  Contexta — AI-native codebase context engine.
3
5
 
4
6
  Usage:
@@ -99,6 +99,7 @@ class UpdateStats:
99
99
  seconds: float = 0.0
100
100
  git_commits: int = 0
101
101
  skipped_reason: str = ""
102
+ truncated: bool = False
102
103
 
103
104
  def to_dict(self) -> dict:
104
105
  return {k: (round(v, 3) if isinstance(v, float) else v) for k, v in self.__dict__.items()}
@@ -106,15 +107,31 @@ class UpdateStats:
106
107
  def summary(self) -> str:
107
108
  if self.skipped_reason:
108
109
  return f"Graph not updated: {self.skipped_reason}"
109
- return (
110
+ text = (
110
111
  f"Graph: {self.total} file(s), {self.added} new, {self.changed} changed, {self.removed} removed, "
111
112
  f"{self.unchanged} unchanged, {self.symbols} symbols, {self.edges} edges ({self.seconds:.2f}s)"
112
113
  )
114
+ if self.truncated:
115
+ text += f"\nWarning: the project has more than {self.total} files; only the first {self.total} were indexed. Raise CONTEXTA_GRAPH_MAX_FILES to index the rest."
116
+ return text
113
117
 
114
118
 
115
119
  # ------------------------------------------------------------------ helpers
116
120
 
117
121
 
122
+ DEFAULT_GRAPH_MAX_FILES = 50000
123
+
124
+
125
+ def graph_max_files() -> int:
126
+ """File cap for the project graph: CONTEXTA_GRAPH_MAX_FILES, else 50000 (packs keep their own 2000 default)."""
127
+ raw = os.environ.get("CONTEXTA_GRAPH_MAX_FILES", "").strip()
128
+ try:
129
+ value = int(raw) if raw else 0
130
+ except ValueError:
131
+ value = 0
132
+ return value if value > 0 else DEFAULT_GRAPH_MAX_FILES
133
+
134
+
118
135
  def classify_kind(rel: str, lang: str | None) -> str:
119
136
  parts = PurePosixPath(rel).parts
120
137
  name = parts[-1]
@@ -350,6 +367,7 @@ class ProjectGraph:
350
367
  self._shared = explicit_db and self.db_path.parent != self.root / GRAPH_DIRNAME
351
368
  self.readonly = readonly
352
369
  self._con: sqlite3.Connection | None = None
370
+ self.truncated = False
353
371
 
354
372
  # connection
355
373
  @property
@@ -427,6 +445,7 @@ class ProjectGraph:
427
445
  started = time.perf_counter()
428
446
  stats = UpdateStats()
429
447
  self._claim()
448
+ self.truncated = False
430
449
  if files is None:
431
450
  files = self._scan()
432
451
  current: dict[str, Path] = {}
@@ -439,6 +458,7 @@ class ProjectGraph:
439
458
  continue
440
459
  current[rel] = path
441
460
  stats.total = len(current)
461
+ stats.truncated = self.truncated
442
462
 
443
463
  con = self.con
444
464
  known = {r["path"]: r for r in con.execute("SELECT path, hash, mtime_ns, size FROM files")}
@@ -485,6 +505,7 @@ class ProjectGraph:
485
505
  if dirty or use_git:
486
506
  self._compute_ranks()
487
507
  self._set_meta("updated_at", str(now))
508
+ self._set_meta("truncated", "1" if self.truncated else "0")
488
509
  con.commit()
489
510
  except Exception:
490
511
  con.rollback()
@@ -532,7 +553,11 @@ class ProjectGraph:
532
553
  from contexta_app.scanner import build_tree, load_gitignore_patterns
533
554
 
534
555
  patterns = load_gitignore_patterns(self.root)
535
- tree = build_tree(self.root, False, False, lambda *_a, **_k: None, [0], patterns, self.root)
556
+ # The memory should cover the whole project, so it does not share the 2000-file cap of a pack.
557
+ limit = graph_max_files()
558
+ notes: list[str] = []
559
+ tree = build_tree(self.root, False, False, lambda msg, *_a, **_k: notes.append(str(msg)), [0], patterns, self.root, limit)
560
+ self.truncated = any("file limit" in n for n in notes)
536
561
  return flatten_tree(tree)
537
562
 
538
563
  def _delete_file(self, rel: str) -> None:
@@ -785,6 +810,7 @@ class ProjectGraph:
785
810
  "updated_at": _iso(float(self.meta("updated_at", "0") or 0)),
786
811
  "db": str(self.db_path.relative_to(self.root)) if self.db_path.is_relative_to(self.root) else "(outside project)",
787
812
  "parsers": {r[0] or "none": r[1] for r in con.execute("SELECT backend, COUNT(*) FROM files GROUP BY backend")},
813
+ "truncated": self.meta("truncated", "0") == "1",
788
814
  }
789
815
 
790
816
  def has_file(self, path: str) -> bool:
@@ -103,6 +103,8 @@ def format_status(info: dict, name: str) -> str:
103
103
  lines.append(" edges: " + (", ".join(f"{k} {v}" for k, v in sorted(info["edges"].items())) or "none"))
104
104
  lines.append(" parsers: " + ", ".join(f"{k} {v}" for k, v in sorted(info["parsers"].items())))
105
105
  lines.append(f" updated {info['updated_at']} db {info['db']}")
106
+ if info.get("truncated"):
107
+ lines.append(" warning: the last update hit the file limit, so the graph is incomplete (raise CONTEXTA_GRAPH_MAX_FILES)")
106
108
  return "\n".join(lines) + "\n"
107
109
 
108
110
 
@@ -55,6 +55,9 @@ def parse_serve_args(args: list[str]) -> list[str]:
55
55
  i = 0
56
56
  while i < len(args):
57
57
  arg = args[i]
58
+ if arg in ("-h", "--help"):
59
+ print("Usage: contexta serve [--root DIR]\n\nStart the MCP server over stdio. Tools only read inside --root\n(repeatable), CONTEXTA_ROOT, or the current directory.")
60
+ raise SystemExit(0)
58
61
  if arg == "--root":
59
62
  if i + 1 >= len(args):
60
63
  raise SystemExit("contexta serve: --root needs a folder")
@@ -561,6 +561,11 @@ def build_tree(
561
561
 
562
562
  if entry.is_symlink() and not _symlink_stays_inside(entry, project_root):
563
563
  continue
564
+ if entry.is_symlink() and entry.is_dir():
565
+ # A directory link that stays inside the project points at something scanned under its real
566
+ # path anyway. Following it only lists the same files twice (or forever for `self -> .`, until
567
+ # the file cap), and could walk a folder the ignore rules skip.
568
+ continue
564
569
 
565
570
  if entry.is_dir():
566
571
  if should_ignore_dir(entry.name, include_hidden):
@@ -4,6 +4,10 @@ theme.py — Color palettes, theme switching, and widget registry.
4
4
  The palette is an ink surface with a single mint accent. Text colors are
5
5
  chosen so body copy and captions stay above a 4.5:1 contrast ratio on the
6
6
  surfaces they sit on. Bright fills use dark ink for their label.
7
+
8
+ ``border`` is the quiet outline of a card. ``border_strong`` is for anything the
9
+ user interacts with (inputs, ghost buttons, switch tracks): at least 3:1 against
10
+ the card, as WCAG 1.4.11 asks for UI components.
7
11
  """
8
12
 
9
13
  import tkinter as tk
@@ -20,6 +24,7 @@ DARK: dict[str, str] = {
20
24
  "input": "#0f1613",
21
25
  "border": "#2c433b",
22
26
  "border2": "#3e5c50",
27
+ "border_strong": "#4f7a6a",
23
28
  "accent": "#3dcc8e",
24
29
  "accent_dk": "#1f8f62",
25
30
  "green": "#3dcc8e",
@@ -49,6 +54,7 @@ LIGHT: dict[str, str] = {
49
54
  "input": "#ffffff",
50
55
  "border": "#d5e2db",
51
56
  "border2": "#b7c9c0",
57
+ "border_strong": "#728a7d",
52
58
  "accent": "#0d7a4e",
53
59
  "accent_dk": "#0b6842",
54
60
  "green": "#0d7a4e",