contexta-ai 2.0.0__py3-none-any.whl
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.
- contexta_ai-2.0.0.dist-info/METADATA +299 -0
- contexta_ai-2.0.0.dist-info/RECORD +18 -0
- contexta_ai-2.0.0.dist-info/WHEEL +4 -0
- contexta_ai-2.0.0.dist-info/entry_points.txt +2 -0
- contexta_ai-2.0.0.dist-info/licenses/LICENSE +21 -0
- contexta_app/__init__.py +3 -0
- contexta_app/ai_client.py +182 -0
- contexta_app/cache.py +163 -0
- contexta_app/cli.py +188 -0
- contexta_app/context_engine.py +5113 -0
- contexta_app/embedded_icon_data.py +269 -0
- contexta_app/mcp_server.py +327 -0
- contexta_app/renderer.py +1794 -0
- contexta_app/scanner.py +524 -0
- contexta_app/syntax.py +167 -0
- contexta_app/theme.py +126 -0
- contexta_app/ui.py +1283 -0
- contexta_app/utils.py +33 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contexta-ai
|
|
3
|
+
Version: 2.0.0
|
|
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
|
+
Project-URL: Homepage, https://github.com/pablokaua03/contexta
|
|
6
|
+
Project-URL: Repository, https://github.com/pablokaua03/contexta
|
|
7
|
+
Author-email: Pablo Kaua <help@spacegroupusa.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai,claude,codebase,context,cursor,developer-tools,mcp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: charset-normalizer<4,>=3.4
|
|
20
|
+
Requires-Dist: mcp[cli]<2,>=1.9
|
|
21
|
+
Requires-Dist: pathspec<1.1,>=0.12
|
|
22
|
+
Requires-Dist: rapidfuzz<4,>=3.14
|
|
23
|
+
Requires-Dist: tiktoken<1,>=0.12
|
|
24
|
+
Requires-Dist: tree-sitter-language-pack<2,>=1.6
|
|
25
|
+
Requires-Dist: tree-sitter<0.26,>=0.25
|
|
26
|
+
Provides-Extra: all-ai
|
|
27
|
+
Requires-Dist: anthropic<1,>=0.52; extra == 'all-ai'
|
|
28
|
+
Requires-Dist: google-genai<2,>=1.14; extra == 'all-ai'
|
|
29
|
+
Requires-Dist: openai<2,>=1.82; extra == 'all-ai'
|
|
30
|
+
Provides-Extra: claude
|
|
31
|
+
Requires-Dist: anthropic<1,>=0.52; extra == 'claude'
|
|
32
|
+
Provides-Extra: gemini
|
|
33
|
+
Requires-Dist: google-genai<2,>=1.14; extra == 'gemini'
|
|
34
|
+
Provides-Extra: gui
|
|
35
|
+
Provides-Extra: openai
|
|
36
|
+
Requires-Dist: openai<2,>=1.82; extra == 'openai'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
<div align="center">
|
|
40
|
+
|
|
41
|
+
# Contexta
|
|
42
|
+
|
|
43
|
+
**AI-native codebase context engine. Serves intelligent project context to Claude Code, Cursor, Windsurf, and any MCP-compatible tool — or generates curated context packs for manual use.**
|
|
44
|
+
|
|
45
|
+
[](https://python.org)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
[]()
|
|
48
|
+
[]()
|
|
49
|
+
[]()
|
|
50
|
+
|
|
51
|
+
<br>
|
|
52
|
+
|
|
53
|
+
[<img src="https://img.shields.io/badge/pip%20install%20contexta--ai-3776AB?style=for-the-badge&logo=pypi&logoColor=white" height="42">](https://pypi.org/project/contexta-ai/)
|
|
54
|
+
|
|
55
|
+
[<img src="https://img.shields.io/badge/Download%20for%20Windows-0078D4?style=for-the-badge&logo=windows&logoColor=white" height="42">](https://github.com/pablokaua03/Contexta/releases/latest/download/contexta.exe)
|
|
56
|
+
|
|
57
|
+
[<img src="https://img.shields.io/badge/Download%20for%20Linux-E95420?style=for-the-badge&logo=linux&logoColor=white" height="42">](https://github.com/pablokaua03/Contexta/releases/latest/download/contexta-linux.tar.gz)
|
|
58
|
+
|
|
59
|
+
> Install via pip, download the portable executable, or run from source.
|
|
60
|
+
|
|
61
|
+
<br>
|
|
62
|
+
|
|
63
|
+
<picture>
|
|
64
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/dark.png">
|
|
65
|
+
<source media="(prefers-color-scheme: light)" srcset="assets/white.png">
|
|
66
|
+
<img alt="Contexta interface preview" src="assets/dark.png">
|
|
67
|
+
</picture>
|
|
68
|
+
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## What is Contexta
|
|
74
|
+
|
|
75
|
+
Contexta analyzes codebases and serves intelligent, curated context to AI tools. Instead of dumping files blindly, it understands project structure, detects frameworks, ranks files by importance, and delivers exactly what an AI needs.
|
|
76
|
+
|
|
77
|
+
**Three ways to use it:**
|
|
78
|
+
|
|
79
|
+
| Mode | Command | What it does |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| **MCP Server** | `contexta serve` | Runs as a tool server — Claude Code, Cursor, Windsurf call it automatically |
|
|
82
|
+
| **CLI** | `contexta ./project --pack onboarding` | Generates a context pack Markdown file |
|
|
83
|
+
| **GUI** | `contexta` | Desktop app with visual controls |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## MCP Server (recommended)
|
|
88
|
+
|
|
89
|
+
The MCP server is the primary way to use Contexta. AI coding assistants call it as a tool to understand your project — no copy-paste needed.
|
|
90
|
+
|
|
91
|
+
### Setup
|
|
92
|
+
|
|
93
|
+
Add to your MCP configuration (Claude Code, Cursor, Windsurf, etc.):
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"contexta": {
|
|
99
|
+
"command": "contexta",
|
|
100
|
+
"args": ["serve"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or if running from source:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"contexta": {
|
|
112
|
+
"command": "python",
|
|
113
|
+
"args": ["/path/to/contexta_mcp.py"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Available tools
|
|
120
|
+
|
|
121
|
+
| Tool | What it does |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `scan_project` | Quick fingerprint — type, language, frameworks, deps, entry points |
|
|
124
|
+
| `get_architecture` | Module relationships, folder structure, risks, key patterns |
|
|
125
|
+
| `generate_context` | Full AI-optimized context pack with all preset options |
|
|
126
|
+
| `find_files` | Search files by name, extension, or keyword |
|
|
127
|
+
| `read_files` | Read specific file contents |
|
|
128
|
+
| `list_packs` | List available pack presets |
|
|
129
|
+
| `cache_status` | Show cache hit/miss statistics |
|
|
130
|
+
| `refresh_cache` | Force-refresh cached analysis |
|
|
131
|
+
|
|
132
|
+
### Smart cache
|
|
133
|
+
|
|
134
|
+
Results are cached in memory with automatic invalidation. First call analyzes the project (~700ms), subsequent calls return in ~2ms until files change on disk. The cache monitors file mtimes and manifest files — when you edit code, the next call recomputes automatically.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Install
|
|
139
|
+
|
|
140
|
+
### Option A: pip (recommended)
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pip install contexta-ai
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Optional AI API integrations:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install contexta-ai[claude] # Claude API
|
|
150
|
+
pip install contexta-ai[gemini] # Gemini API
|
|
151
|
+
pip install contexta-ai[openai] # OpenAI API
|
|
152
|
+
pip install contexta-ai[all-ai] # All three
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Option B: Portable executables
|
|
156
|
+
|
|
157
|
+
- **Windows**: download [`contexta.exe`](https://github.com/pablokaua03/Contexta/releases/latest/download/contexta.exe)
|
|
158
|
+
- **Linux**: download [`contexta-linux.tar.gz`](https://github.com/pablokaua03/Contexta/releases/latest/download/contexta-linux.tar.gz)
|
|
159
|
+
|
|
160
|
+
### Option C: From source
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git clone https://github.com/pablokaua03/Contexta.git
|
|
164
|
+
cd Contexta
|
|
165
|
+
pip install -r requirements.txt
|
|
166
|
+
python contexta.py
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## CLI usage
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Generate context packs
|
|
175
|
+
contexta ./project # default pack
|
|
176
|
+
contexta ./project --pack onboarding # understand a new codebase
|
|
177
|
+
contexta ./project --pack raw_files # clean path + content dump
|
|
178
|
+
contexta ./project --pack pr_review --diff --copy # review changes
|
|
179
|
+
contexta ./project --mode debug --focus "auth flow" # debug a specific area
|
|
180
|
+
|
|
181
|
+
# AI API integration — send context + question directly to an AI
|
|
182
|
+
contexta ./project --ask "what are the main security risks?"
|
|
183
|
+
contexta ./project --ask "explain the architecture" --provider claude
|
|
184
|
+
|
|
185
|
+
# Configure AI API keys
|
|
186
|
+
contexta --configure-ai
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### CLI flags
|
|
190
|
+
|
|
191
|
+
| Flag | Description |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `--pack` | Preset: `custom`, `chatgpt`, `onboarding`, `pr_review`, `risk_review`, `debug`, `backend`, `frontend`, `changes_related`, `raw_files` |
|
|
194
|
+
| `--mode` | Context mode: `full`, `debug`, `feature`, `diff`, `onboarding`, `refactor` |
|
|
195
|
+
| `--compression` | `full`, `balanced`, `focused`, `signatures`, `lean` |
|
|
196
|
+
| `--ai` | Target AI profile: `generic`, `chatgpt`, `claude`, `gemini`, `copilot` |
|
|
197
|
+
| `--task` | Task profile: `general`, `ai_handoff`, `bug_report`, `code_review`, `explain_project`, `risk_analysis`, `refactor_request`, `pr_summary`, `write_tests`, `find_dead_code` |
|
|
198
|
+
| `--focus` | Bias scoring around a topic (e.g. `"auth flow"`, `"database"`) |
|
|
199
|
+
| `--diff` / `--staged` | Use git changes as context |
|
|
200
|
+
| `--ask` | Send context pack + prompt to an AI API |
|
|
201
|
+
| `--provider` | AI provider: `claude`, `gemini`, `openai` |
|
|
202
|
+
| `--api-key` | API key (also reads `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, `OPENAI_API_KEY`) |
|
|
203
|
+
| `--configure-ai` | Interactive AI API setup |
|
|
204
|
+
| `-c` / `--copy` | Copy output to clipboard |
|
|
205
|
+
| `-o` / `--output` | Custom output path |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Context packs
|
|
210
|
+
|
|
211
|
+
| Pack | Best for |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `onboarding` | Understanding a new codebase fast |
|
|
214
|
+
| `pr_review` | Code review with change context |
|
|
215
|
+
| `risk_review` | Regression hotspots, missing coverage, broad-impact modules |
|
|
216
|
+
| `debug` | Bug hunting with changed/suspicious files prioritized |
|
|
217
|
+
| `backend` / `frontend` | Bias toward one side of the app |
|
|
218
|
+
| `changes_related` | Git changes + nearby relevant files |
|
|
219
|
+
| `raw_files` | Clean dump of important files — path + full content, no analysis |
|
|
220
|
+
| `chatgpt` | General-purpose ChatGPT preset |
|
|
221
|
+
| `custom` | Full manual control |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## What Contexta exports
|
|
226
|
+
|
|
227
|
+
Depending on pack, mode, and task, the output can include:
|
|
228
|
+
|
|
229
|
+
- Project summary with detected technologies, entry points, purpose, and central modules
|
|
230
|
+
- Read-this-first path through the repository
|
|
231
|
+
- Main execution flow narrative
|
|
232
|
+
- Core files, supporting files, related tests, and changed-file context
|
|
233
|
+
- Relationship maps and risk notes
|
|
234
|
+
- Score breakdown explaining why each file was selected
|
|
235
|
+
- Curated Markdown payload ready for any AI tool
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Main features
|
|
240
|
+
|
|
241
|
+
| Feature | Detail |
|
|
242
|
+
|---|---|
|
|
243
|
+
| **MCP Server** | `contexta serve` — AI tools call it as a tool server with smart caching |
|
|
244
|
+
| **AI API integration** | `--ask` sends context directly to Claude, Gemini, or OpenAI |
|
|
245
|
+
| **Smart cache** | ~2ms cached responses with automatic mtime-based invalidation |
|
|
246
|
+
| **Raw files pack** | Clean path + content dump for simple AI workflows |
|
|
247
|
+
| **GUI + CLI** | Desktop app for visual use, CLI for scripting and automation |
|
|
248
|
+
| **Project fingerprinting** | Detects stack, frameworks, domain, and project type automatically |
|
|
249
|
+
| **Syntax-aware analysis** | tree-sitter + heuristic fallback for symbol extraction |
|
|
250
|
+
| **Multi-language** | Python, JS/TS, Go, Rust, PHP, Java, C#, Kotlin, Swift, C++ and more |
|
|
251
|
+
| **Token guidance** | tiktoken-backed estimates for pack sizing |
|
|
252
|
+
| **Inline blob protection** | Collapses base64/binary literals automatically |
|
|
253
|
+
| **PyPI package** | `pip install contexta-ai` with optional AI extras |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Build from source
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
# Windows (requires Visual Studio C++ Build Tools)
|
|
261
|
+
.\build.bat
|
|
262
|
+
|
|
263
|
+
# Linux / macOS
|
|
264
|
+
chmod +x build.sh && ./build.sh
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Build outputs: `dist/contexta.exe` (Windows), `dist/contexta` (Linux/macOS), `dist/contexta-linux.tar.gz` (Linux bundle).
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Run tests
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
python -m pytest tests/ -k "not test_relation_score"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Security and behavior
|
|
280
|
+
|
|
281
|
+
- Read-only: Contexta does not modify the scanned project
|
|
282
|
+
- No runtime telemetry or network requirement (except optional `--ask` AI calls)
|
|
283
|
+
- Scan limits prevent runaway exports
|
|
284
|
+
- Embedded binary/blob payloads are suppressed automatically
|
|
285
|
+
- AI API keys are stored locally in `~/.contexta/ai_config.json` when using `--configure-ai`
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Contributing
|
|
290
|
+
|
|
291
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
292
|
+
|
|
293
|
+
## Changelog
|
|
294
|
+
|
|
295
|
+
See [CHANGELOG.md](CHANGELOG.md)
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
[MIT](LICENSE) © [pablokaua03](https://github.com/pablokaua03)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
contexta_app/__init__.py,sha256=1sFrjiDsOqTfdTMOhOPtx5qNwJ9p_MHr9CEIEQGfb98,38
|
|
2
|
+
contexta_app/ai_client.py,sha256=1WwOY61KtMsb4Wge8RkZXu2IAf7oSnhGxzoCJzTlSSk,5366
|
|
3
|
+
contexta_app/cache.py,sha256=hGqM9vAiZW9TO1FxEuS68iMJ075U6Z6zlu992AxL1KU,4169
|
|
4
|
+
contexta_app/cli.py,sha256=WJv2WJpse0vyxE9gMHTAQK-Scaqp_IH1MjJn_eNT0JA,7725
|
|
5
|
+
contexta_app/context_engine.py,sha256=4Bs0W7Ac_2KNjAAP0No8hnu2Vy5dk7mp4-lPvp-Iehs,231598
|
|
6
|
+
contexta_app/embedded_icon_data.py,sha256=_CImjDgPdmIrTCTe0U7aeUCMlm3wx8hS_6mugi5csoE,33637
|
|
7
|
+
contexta_app/mcp_server.py,sha256=SubradWolkrtAxT_qB-wI94wHFuSBWVotay8bx9KqDg,11317
|
|
8
|
+
contexta_app/renderer.py,sha256=L2CGlgAeZXk-XF8dIFaZgjOnHVZ0LEpMmZCuOWSX0H0,79649
|
|
9
|
+
contexta_app/scanner.py,sha256=mWaRYinpBUpwHx7aie2qpEZbFzX-j9R5wrQpLsfU_vY,13614
|
|
10
|
+
contexta_app/syntax.py,sha256=F41VfxPJDJdgS6DbCiUufqYH6-z3_be53-WGIE9m0Ss,6216
|
|
11
|
+
contexta_app/theme.py,sha256=OgaIwSYCq3MSOg3CX8tC7NbU-n5zsnPTcGTMD561WOM,4700
|
|
12
|
+
contexta_app/ui.py,sha256=gdJiCIbagWciiDIaR4kH3VbQWAVhfvDVMBnldpFtxVs,84704
|
|
13
|
+
contexta_app/utils.py,sha256=DE0QGhNT8-VVdNa2XEZoBclzkRUqlbuEgIm5hJsBu3I,771
|
|
14
|
+
contexta_ai-2.0.0.dist-info/METADATA,sha256=w-eNqTJalqx0e8Ty7usALZtPZv74aS2oihsVbwLrIxI,10799
|
|
15
|
+
contexta_ai-2.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
16
|
+
contexta_ai-2.0.0.dist-info/entry_points.txt,sha256=uiU3v16omDQFbRjybKBk7RIlxIfCy4c3G3KqJ_TDZog,43
|
|
17
|
+
contexta_ai-2.0.0.dist-info/licenses/LICENSE,sha256=Hj-WZf4KcurgEbiLdQnqWwIewx_bJmcrgawyQDraCmE,1068
|
|
18
|
+
contexta_ai-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 pablokaua03
|
|
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.
|
contexta_app/__init__.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""AI API client for sending context packs directly to Claude, Gemini, or OpenAI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
CONFIG_PATH = Path.home() / ".contexta" / "ai_config.json"
|
|
11
|
+
|
|
12
|
+
ENV_VARS = {
|
|
13
|
+
"claude": "ANTHROPIC_API_KEY",
|
|
14
|
+
"gemini": "GOOGLE_API_KEY",
|
|
15
|
+
"openai": "OPENAI_API_KEY",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
DEFAULT_MODELS = {
|
|
19
|
+
"claude": "claude-sonnet-4-20250514",
|
|
20
|
+
"gemini": "gemini-2.5-flash",
|
|
21
|
+
"openai": "gpt-4o",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
PROVIDER_LABELS = {
|
|
25
|
+
"claude": "Claude (Anthropic)",
|
|
26
|
+
"gemini": "Gemini (Google)",
|
|
27
|
+
"openai": "ChatGPT / GPT-4 (OpenAI)",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class AIConfig:
|
|
33
|
+
provider: str = "" # "claude", "gemini", "openai"
|
|
34
|
+
api_key: str = ""
|
|
35
|
+
model: str = ""
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def load() -> "AIConfig":
|
|
39
|
+
if CONFIG_PATH.exists():
|
|
40
|
+
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
41
|
+
return AIConfig(**{k: v for k, v in data.items() if k in AIConfig.__dataclass_fields__})
|
|
42
|
+
return AIConfig()
|
|
43
|
+
|
|
44
|
+
def save(self) -> None:
|
|
45
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
CONFIG_PATH.write_text(
|
|
47
|
+
json.dumps(
|
|
48
|
+
{"provider": self.provider, "api_key": self.api_key, "model": self.model},
|
|
49
|
+
indent=2,
|
|
50
|
+
),
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_available_providers() -> list[str]:
|
|
56
|
+
"""Return provider keys whose SDK is installed."""
|
|
57
|
+
available = []
|
|
58
|
+
try:
|
|
59
|
+
import anthropic # noqa: F401
|
|
60
|
+
available.append("claude")
|
|
61
|
+
except ImportError:
|
|
62
|
+
pass
|
|
63
|
+
try:
|
|
64
|
+
import google.genai # noqa: F401
|
|
65
|
+
available.append("gemini")
|
|
66
|
+
except ImportError:
|
|
67
|
+
pass
|
|
68
|
+
try:
|
|
69
|
+
import openai # noqa: F401
|
|
70
|
+
available.append("openai")
|
|
71
|
+
except ImportError:
|
|
72
|
+
pass
|
|
73
|
+
return available
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_api_key(provider: str, explicit_key: str = "") -> str:
|
|
77
|
+
"""Resolve API key: explicit flag > env var > saved config."""
|
|
78
|
+
if explicit_key:
|
|
79
|
+
return explicit_key
|
|
80
|
+
env_var = ENV_VARS.get(provider, "")
|
|
81
|
+
if env_var:
|
|
82
|
+
env_val = os.environ.get(env_var, "")
|
|
83
|
+
if env_val:
|
|
84
|
+
return env_val
|
|
85
|
+
cfg = AIConfig.load()
|
|
86
|
+
if cfg.provider == provider and cfg.api_key:
|
|
87
|
+
return cfg.api_key
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def send_to_ai(
|
|
92
|
+
context_markdown: str,
|
|
93
|
+
prompt: str,
|
|
94
|
+
provider: str,
|
|
95
|
+
api_key: str,
|
|
96
|
+
model: str = "",
|
|
97
|
+
) -> str:
|
|
98
|
+
"""Send context + prompt to the chosen AI and return the response text."""
|
|
99
|
+
if not model:
|
|
100
|
+
model = DEFAULT_MODELS.get(provider, "")
|
|
101
|
+
|
|
102
|
+
system_msg = (
|
|
103
|
+
"You are a senior software engineer. The user will provide a project context "
|
|
104
|
+
"pack followed by a question or task. Analyze the context and respond helpfully."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if provider == "claude":
|
|
108
|
+
import anthropic
|
|
109
|
+
|
|
110
|
+
client = anthropic.Anthropic(api_key=api_key)
|
|
111
|
+
response = client.messages.create(
|
|
112
|
+
model=model,
|
|
113
|
+
max_tokens=8192,
|
|
114
|
+
system=system_msg,
|
|
115
|
+
messages=[{"role": "user", "content": f"{context_markdown}\n\n---\n\n{prompt}"}],
|
|
116
|
+
)
|
|
117
|
+
return response.content[0].text
|
|
118
|
+
|
|
119
|
+
elif provider == "gemini":
|
|
120
|
+
from google import genai
|
|
121
|
+
|
|
122
|
+
client = genai.Client(api_key=api_key)
|
|
123
|
+
response = client.models.generate_content(
|
|
124
|
+
model=model,
|
|
125
|
+
contents=f"{system_msg}\n\n{context_markdown}\n\n---\n\n{prompt}",
|
|
126
|
+
)
|
|
127
|
+
return response.text
|
|
128
|
+
|
|
129
|
+
elif provider == "openai":
|
|
130
|
+
import openai as oai
|
|
131
|
+
|
|
132
|
+
client = oai.OpenAI(api_key=api_key)
|
|
133
|
+
response = client.chat.completions.create(
|
|
134
|
+
model=model,
|
|
135
|
+
messages=[
|
|
136
|
+
{"role": "system", "content": system_msg},
|
|
137
|
+
{"role": "user", "content": f"{context_markdown}\n\n---\n\n{prompt}"},
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
return response.choices[0].message.content
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
raise ValueError(f"Unknown provider: {provider}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def configure_interactive() -> None:
|
|
147
|
+
"""Interactive setup: choose provider, enter API key and model, save to config."""
|
|
148
|
+
available = get_available_providers()
|
|
149
|
+
all_providers = list(PROVIDER_LABELS.keys())
|
|
150
|
+
|
|
151
|
+
print("AI Configuration for Contexta")
|
|
152
|
+
print("=" * 40)
|
|
153
|
+
print("\nAvailable providers (SDK installed):")
|
|
154
|
+
for p in all_providers:
|
|
155
|
+
installed = p in available
|
|
156
|
+
label = PROVIDER_LABELS[p]
|
|
157
|
+
status = "installed" if installed else "not installed"
|
|
158
|
+
print(f" {p:10s} - {label} [{status}]")
|
|
159
|
+
|
|
160
|
+
print()
|
|
161
|
+
provider = input(f"Choose provider ({', '.join(all_providers)}): ").strip().lower()
|
|
162
|
+
if provider not in all_providers:
|
|
163
|
+
print(f"Unknown provider '{provider}'. Aborting.")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
if provider not in available:
|
|
167
|
+
pkg = {"claude": "anthropic", "gemini": "google-genai", "openai": "openai"}[provider]
|
|
168
|
+
print(f"\nWarning: '{pkg}' is not installed. Install it with: pip install {pkg}")
|
|
169
|
+
|
|
170
|
+
api_key = input("API key: ").strip()
|
|
171
|
+
if not api_key:
|
|
172
|
+
print("No API key provided. Aborting.")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
default_model = DEFAULT_MODELS.get(provider, "")
|
|
176
|
+
model = input(f"Model (default: {default_model}): ").strip()
|
|
177
|
+
if not model:
|
|
178
|
+
model = default_model
|
|
179
|
+
|
|
180
|
+
cfg = AIConfig(provider=provider, api_key=api_key, model=model)
|
|
181
|
+
cfg.save()
|
|
182
|
+
print(f"\nSaved to {CONFIG_PATH}")
|
contexta_app/cache.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cache.py - Smart project cache with file-change detection.
|
|
3
|
+
|
|
4
|
+
Keeps ProjectAnalysis in memory and invalidates when source files change.
|
|
5
|
+
No background threads — checks mtimes on access, so it's safe for MCP's
|
|
6
|
+
async model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from contexta_app.context_engine import ExportConfig, ProjectAnalysis, build_analysis
|
|
16
|
+
from contexta_app.scanner import (
|
|
17
|
+
build_tree,
|
|
18
|
+
count_files,
|
|
19
|
+
get_git_changed_files,
|
|
20
|
+
load_gitignore_patterns,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class _CacheEntry:
|
|
26
|
+
analysis: ProjectAnalysis
|
|
27
|
+
tree: dict
|
|
28
|
+
file_count: int
|
|
29
|
+
mtimes: dict[str, float]
|
|
30
|
+
created_at: float
|
|
31
|
+
hit_count: int = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_NOOP_LOG = lambda msg, tag="": None
|
|
35
|
+
_MAX_CACHE_AGE = 300.0
|
|
36
|
+
_MAX_ENTRIES = 10
|
|
37
|
+
|
|
38
|
+
_cache: dict[str, _CacheEntry] = {}
|
|
39
|
+
_stats = {"hits": 0, "misses": 0, "invalidations": 0}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _snapshot_mtimes(path: Path, file_paths: list[Path]) -> dict[str, float]:
|
|
43
|
+
result = {}
|
|
44
|
+
for fp in file_paths:
|
|
45
|
+
try:
|
|
46
|
+
result[str(fp)] = fp.stat().st_mtime
|
|
47
|
+
except OSError:
|
|
48
|
+
result[str(fp)] = 0.0
|
|
49
|
+
|
|
50
|
+
for marker in ("package.json", "pyproject.toml", "Cargo.toml", "go.mod",
|
|
51
|
+
"requirements.txt", "composer.json", "Gemfile", ".gitignore"):
|
|
52
|
+
candidate = path / marker
|
|
53
|
+
if candidate.exists():
|
|
54
|
+
try:
|
|
55
|
+
result[str(candidate)] = candidate.stat().st_mtime
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _has_changes(entry: _CacheEntry) -> bool:
|
|
62
|
+
if time.time() - entry.created_at > _MAX_CACHE_AGE:
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
for fpath, old_mtime in entry.mtimes.items():
|
|
66
|
+
try:
|
|
67
|
+
current = Path(fpath).stat().st_mtime
|
|
68
|
+
if current != old_mtime:
|
|
69
|
+
return True
|
|
70
|
+
except OSError:
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _evict_oldest():
|
|
76
|
+
if len(_cache) < _MAX_ENTRIES:
|
|
77
|
+
return
|
|
78
|
+
oldest_key = min(_cache, key=lambda k: _cache[k].created_at)
|
|
79
|
+
del _cache[oldest_key]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_analysis(
|
|
83
|
+
project_path: Path,
|
|
84
|
+
config: ExportConfig | None = None,
|
|
85
|
+
force_refresh: bool = False,
|
|
86
|
+
log_cb=None,
|
|
87
|
+
) -> tuple[ProjectAnalysis, dict, int, bool]:
|
|
88
|
+
"""Return (analysis, tree, file_count, was_cached).
|
|
89
|
+
|
|
90
|
+
Uses cached analysis if files haven't changed, otherwise recomputes.
|
|
91
|
+
"""
|
|
92
|
+
if log_cb is None:
|
|
93
|
+
log_cb = _NOOP_LOG
|
|
94
|
+
if config is None:
|
|
95
|
+
config = ExportConfig()
|
|
96
|
+
|
|
97
|
+
key = str(project_path.resolve())
|
|
98
|
+
entry = _cache.get(key)
|
|
99
|
+
|
|
100
|
+
if entry is not None and not force_refresh and not _has_changes(entry):
|
|
101
|
+
entry.hit_count += 1
|
|
102
|
+
_stats["hits"] += 1
|
|
103
|
+
return entry.analysis, entry.tree, entry.file_count, True
|
|
104
|
+
|
|
105
|
+
if entry is not None:
|
|
106
|
+
_stats["invalidations"] += 1
|
|
107
|
+
|
|
108
|
+
_stats["misses"] += 1
|
|
109
|
+
_evict_oldest()
|
|
110
|
+
|
|
111
|
+
gitignore = load_gitignore_patterns(project_path)
|
|
112
|
+
counter = [0]
|
|
113
|
+
tree = build_tree(
|
|
114
|
+
root=project_path,
|
|
115
|
+
include_hidden=False,
|
|
116
|
+
include_unknown=False,
|
|
117
|
+
log_cb=log_cb,
|
|
118
|
+
counter=counter,
|
|
119
|
+
gitignore_patterns=gitignore,
|
|
120
|
+
project_root=project_path,
|
|
121
|
+
)
|
|
122
|
+
file_count = count_files(tree)
|
|
123
|
+
changed = get_git_changed_files(project_path) or []
|
|
124
|
+
analysis = build_analysis(
|
|
125
|
+
project_path=project_path,
|
|
126
|
+
full_tree=tree,
|
|
127
|
+
changed_files=changed,
|
|
128
|
+
config=config,
|
|
129
|
+
log_cb=log_cb,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
all_paths = [f.path for f in analysis.all_files]
|
|
133
|
+
mtimes = _snapshot_mtimes(project_path, all_paths)
|
|
134
|
+
|
|
135
|
+
_cache[key] = _CacheEntry(
|
|
136
|
+
analysis=analysis,
|
|
137
|
+
tree=tree,
|
|
138
|
+
file_count=file_count,
|
|
139
|
+
mtimes=mtimes,
|
|
140
|
+
created_at=time.time(),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return analysis, tree, file_count, False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_cache_stats() -> dict:
|
|
147
|
+
return {
|
|
148
|
+
"entries": len(_cache),
|
|
149
|
+
"projects": list(_cache.keys()),
|
|
150
|
+
**_stats,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def invalidate(project_path: Path | None = None) -> int:
|
|
155
|
+
if project_path is None:
|
|
156
|
+
count = len(_cache)
|
|
157
|
+
_cache.clear()
|
|
158
|
+
return count
|
|
159
|
+
key = str(project_path.resolve())
|
|
160
|
+
if key in _cache:
|
|
161
|
+
del _cache[key]
|
|
162
|
+
return 1
|
|
163
|
+
return 0
|