ctxcode 0.2.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.
- ctxcode-0.2.0/.gitignore +7 -0
- ctxcode-0.2.0/LICENSE +21 -0
- ctxcode-0.2.0/PKG-INFO +202 -0
- ctxcode-0.2.0/README.md +182 -0
- ctxcode-0.2.0/pyproject.toml +39 -0
- ctxcode-0.2.0/src/codecontext/__init__.py +0 -0
- ctxcode-0.2.0/src/codecontext/cli.py +249 -0
- ctxcode-0.2.0/src/codecontext/git_utils.py +47 -0
- ctxcode-0.2.0/src/codecontext/scanner.py +156 -0
ctxcode-0.2.0/.gitignore
ADDED
ctxcode-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohammed Hanan
|
|
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.
|
ctxcode-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ctxcode
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Extract your codebase context for AI assistants
|
|
5
|
+
Project-URL: Homepage, https://github.com/hanu-14/codecontext
|
|
6
|
+
Project-URL: Repository, https://github.com/hanu-14/codecontext
|
|
7
|
+
Project-URL: Issues, https://github.com/hanu-14/codecontext/issues
|
|
8
|
+
Author-email: Mohammed Hanan <hananmtp313@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,cli,code-review,developer-tools,productivity
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# codecontext
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<b>Extract your codebase context for AI assistants.</b><br>
|
|
25
|
+
<a href="https://github.com/hanu-14/codecontext/actions/workflows/ci.yml">
|
|
26
|
+
<img src="https://github.com/hanu-14/codecontext/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
27
|
+
</a>
|
|
28
|
+
<img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+">
|
|
29
|
+
<img src="https://img.shields.io/badge/license-MIT-green" alt="MIT">
|
|
30
|
+
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero dependencies">
|
|
31
|
+
<img src="https://img.shields.io/badge/version-0.2.0-blue" alt="v0.2.0">
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
A zero-dependency CLI tool that packages your project structure, key files, and git history into a formatted prompt for Claude, ChatGPT, and other AI coding assistants.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ctxcode
|
|
38
|
+
cd my-project
|
|
39
|
+
codecontext
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Why?
|
|
43
|
+
|
|
44
|
+
When asking AI assistants about your code, you spend minutes copy-pasting file trees, key files, and git history. `codecontext` does it in one command — giving the AI full project context for better answers.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install codecontext
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or run directly:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl -O https://raw.githubusercontent.com/hanu-14/codecontext/main/src/codecontext/cli.py
|
|
56
|
+
python cli.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Full context (tree + files + git history)
|
|
63
|
+
codecontext
|
|
64
|
+
|
|
65
|
+
# Specific project
|
|
66
|
+
codecontext path/to/project
|
|
67
|
+
|
|
68
|
+
# Output formats
|
|
69
|
+
codecontext --format markdown # default - ready for AI chat
|
|
70
|
+
codecontext --format plain # plain text
|
|
71
|
+
codecontext --format json # machine-readable
|
|
72
|
+
|
|
73
|
+
# Copy to clipboard
|
|
74
|
+
codecontext --clipboard
|
|
75
|
+
|
|
76
|
+
# Save to file
|
|
77
|
+
codecontext -o context.md
|
|
78
|
+
|
|
79
|
+
# Exclude custom patterns
|
|
80
|
+
codecontext --exclude "*.log" "tmp/" "*.csv"
|
|
81
|
+
|
|
82
|
+
# Show hidden files
|
|
83
|
+
codecontext --hidden
|
|
84
|
+
|
|
85
|
+
# Exclude git info
|
|
86
|
+
codecontext --no-git
|
|
87
|
+
|
|
88
|
+
# Adjust limits
|
|
89
|
+
codecontext --max-files 100 --max-lines 150
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Short alias
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
ctx # same as codecontext
|
|
96
|
+
ctx ./src # specific directory
|
|
97
|
+
ctx --clipboard # copy to clipboard
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
Place a `.codecontext.toml` in your project root:
|
|
103
|
+
|
|
104
|
+
```toml
|
|
105
|
+
git = true
|
|
106
|
+
max_files = 300
|
|
107
|
+
max_lines = 250
|
|
108
|
+
format = "markdown"
|
|
109
|
+
exclude = ["*.log", "tmp/"]
|
|
110
|
+
hidden = false
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
JSON and YAML formats are also supported (`.codecontext.json`, `.codecontext.yml`).
|
|
114
|
+
|
|
115
|
+
## Output
|
|
116
|
+
|
|
117
|
+
The tool produces a structured document with:
|
|
118
|
+
|
|
119
|
+
1. **Project metadata** — path, git remote, branch
|
|
120
|
+
2. **Git history** — recent commits and uncommitted changes
|
|
121
|
+
3. **File tree** — with file sizes for every entry
|
|
122
|
+
4. **Key files** — source files with syntax highlighting
|
|
123
|
+
5. **Stats summary** — total files and size at a glance
|
|
124
|
+
|
|
125
|
+
Perfect for pasting directly into Claude, ChatGPT, or any AI coding assistant.
|
|
126
|
+
|
|
127
|
+
## Features
|
|
128
|
+
|
|
129
|
+
- **Zero dependencies** — pure Python, no pip installs beyond this package
|
|
130
|
+
- **Smart ignore** — automatically skips `.git`, `node_modules`, `__pycache__`, binaries, and more
|
|
131
|
+
- **Git aware** — includes branch, remote, recent commits, and diff stats
|
|
132
|
+
- **File limit safety** — won't overwhelm your context window
|
|
133
|
+
- **Language detection** — syntax-highlighted code blocks for 30+ languages
|
|
134
|
+
- **Clipboard support** — `--clipboard` copies output directly to your clipboard
|
|
135
|
+
- **Multiple formats** — markdown, plain text, or JSON
|
|
136
|
+
- **Config file** — project-specific settings via `.codecontext.toml`
|
|
137
|
+
- **Custom excludes** — ignore additional patterns with `--exclude`
|
|
138
|
+
- **File sizes** — every file in the tree shows its size
|
|
139
|
+
|
|
140
|
+
## Example
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
$ codecontext --format markdown
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Produces:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
# Codebase Context
|
|
150
|
+
|
|
151
|
+
Generated from: /Users/me/my-project
|
|
152
|
+
Remote: git@github.com:user/my-project.git
|
|
153
|
+
Branch: main
|
|
154
|
+
|
|
155
|
+
Recent commits:
|
|
156
|
+
abc1234 feat: add user authentication
|
|
157
|
+
def5678 fix: resolve payment timeout
|
|
158
|
+
|
|
159
|
+
## Project Structure
|
|
160
|
+
|
|
161
|
+
my-project/
|
|
162
|
+
├── src/
|
|
163
|
+
│ ├── main.py (2KB)
|
|
164
|
+
│ └── utils.py (1KB)
|
|
165
|
+
├── tests/
|
|
166
|
+
│ └── test_main.py (3KB)
|
|
167
|
+
├── pyproject.toml (1KB)
|
|
168
|
+
└── README.md (3KB)
|
|
169
|
+
|
|
170
|
+
## Key Files (6 files, 10KB)
|
|
171
|
+
|
|
172
|
+
### `src/main.py`
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
def greet(name):
|
|
176
|
+
return f"Hello, {name}!"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
...
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Changelog
|
|
183
|
+
|
|
184
|
+
### v0.2.0
|
|
185
|
+
- Clipboard support (`--clipboard`)
|
|
186
|
+
- Multiple output formats: markdown, plain, JSON
|
|
187
|
+
- Configuration file support (`.codecontext.toml`, `.json`, `.yml`)
|
|
188
|
+
- File sizes in tree output
|
|
189
|
+
- Custom exclude patterns (`--exclude`)
|
|
190
|
+
- Hidden files flag (`--hidden`)
|
|
191
|
+
- Stats summary in output
|
|
192
|
+
|
|
193
|
+
### v0.1.0
|
|
194
|
+
- Initial release
|
|
195
|
+
- File tree with smart ignore patterns
|
|
196
|
+
- Git history integration
|
|
197
|
+
- Syntax highlighting for 30+ languages
|
|
198
|
+
- CLI alias `ctx`
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
ctxcode-0.2.0/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# codecontext
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>Extract your codebase context for AI assistants.</b><br>
|
|
5
|
+
<a href="https://github.com/hanu-14/codecontext/actions/workflows/ci.yml">
|
|
6
|
+
<img src="https://github.com/hanu-14/codecontext/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
7
|
+
</a>
|
|
8
|
+
<img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+">
|
|
9
|
+
<img src="https://img.shields.io/badge/license-MIT-green" alt="MIT">
|
|
10
|
+
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero dependencies">
|
|
11
|
+
<img src="https://img.shields.io/badge/version-0.2.0-blue" alt="v0.2.0">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
A zero-dependency CLI tool that packages your project structure, key files, and git history into a formatted prompt for Claude, ChatGPT, and other AI coding assistants.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install ctxcode
|
|
18
|
+
cd my-project
|
|
19
|
+
codecontext
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Why?
|
|
23
|
+
|
|
24
|
+
When asking AI assistants about your code, you spend minutes copy-pasting file trees, key files, and git history. `codecontext` does it in one command — giving the AI full project context for better answers.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install codecontext
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or run directly:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl -O https://raw.githubusercontent.com/hanu-14/codecontext/main/src/codecontext/cli.py
|
|
36
|
+
python cli.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Full context (tree + files + git history)
|
|
43
|
+
codecontext
|
|
44
|
+
|
|
45
|
+
# Specific project
|
|
46
|
+
codecontext path/to/project
|
|
47
|
+
|
|
48
|
+
# Output formats
|
|
49
|
+
codecontext --format markdown # default - ready for AI chat
|
|
50
|
+
codecontext --format plain # plain text
|
|
51
|
+
codecontext --format json # machine-readable
|
|
52
|
+
|
|
53
|
+
# Copy to clipboard
|
|
54
|
+
codecontext --clipboard
|
|
55
|
+
|
|
56
|
+
# Save to file
|
|
57
|
+
codecontext -o context.md
|
|
58
|
+
|
|
59
|
+
# Exclude custom patterns
|
|
60
|
+
codecontext --exclude "*.log" "tmp/" "*.csv"
|
|
61
|
+
|
|
62
|
+
# Show hidden files
|
|
63
|
+
codecontext --hidden
|
|
64
|
+
|
|
65
|
+
# Exclude git info
|
|
66
|
+
codecontext --no-git
|
|
67
|
+
|
|
68
|
+
# Adjust limits
|
|
69
|
+
codecontext --max-files 100 --max-lines 150
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Short alias
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ctx # same as codecontext
|
|
76
|
+
ctx ./src # specific directory
|
|
77
|
+
ctx --clipboard # copy to clipboard
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
Place a `.codecontext.toml` in your project root:
|
|
83
|
+
|
|
84
|
+
```toml
|
|
85
|
+
git = true
|
|
86
|
+
max_files = 300
|
|
87
|
+
max_lines = 250
|
|
88
|
+
format = "markdown"
|
|
89
|
+
exclude = ["*.log", "tmp/"]
|
|
90
|
+
hidden = false
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
JSON and YAML formats are also supported (`.codecontext.json`, `.codecontext.yml`).
|
|
94
|
+
|
|
95
|
+
## Output
|
|
96
|
+
|
|
97
|
+
The tool produces a structured document with:
|
|
98
|
+
|
|
99
|
+
1. **Project metadata** — path, git remote, branch
|
|
100
|
+
2. **Git history** — recent commits and uncommitted changes
|
|
101
|
+
3. **File tree** — with file sizes for every entry
|
|
102
|
+
4. **Key files** — source files with syntax highlighting
|
|
103
|
+
5. **Stats summary** — total files and size at a glance
|
|
104
|
+
|
|
105
|
+
Perfect for pasting directly into Claude, ChatGPT, or any AI coding assistant.
|
|
106
|
+
|
|
107
|
+
## Features
|
|
108
|
+
|
|
109
|
+
- **Zero dependencies** — pure Python, no pip installs beyond this package
|
|
110
|
+
- **Smart ignore** — automatically skips `.git`, `node_modules`, `__pycache__`, binaries, and more
|
|
111
|
+
- **Git aware** — includes branch, remote, recent commits, and diff stats
|
|
112
|
+
- **File limit safety** — won't overwhelm your context window
|
|
113
|
+
- **Language detection** — syntax-highlighted code blocks for 30+ languages
|
|
114
|
+
- **Clipboard support** — `--clipboard` copies output directly to your clipboard
|
|
115
|
+
- **Multiple formats** — markdown, plain text, or JSON
|
|
116
|
+
- **Config file** — project-specific settings via `.codecontext.toml`
|
|
117
|
+
- **Custom excludes** — ignore additional patterns with `--exclude`
|
|
118
|
+
- **File sizes** — every file in the tree shows its size
|
|
119
|
+
|
|
120
|
+
## Example
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
$ codecontext --format markdown
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Produces:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
# Codebase Context
|
|
130
|
+
|
|
131
|
+
Generated from: /Users/me/my-project
|
|
132
|
+
Remote: git@github.com:user/my-project.git
|
|
133
|
+
Branch: main
|
|
134
|
+
|
|
135
|
+
Recent commits:
|
|
136
|
+
abc1234 feat: add user authentication
|
|
137
|
+
def5678 fix: resolve payment timeout
|
|
138
|
+
|
|
139
|
+
## Project Structure
|
|
140
|
+
|
|
141
|
+
my-project/
|
|
142
|
+
├── src/
|
|
143
|
+
│ ├── main.py (2KB)
|
|
144
|
+
│ └── utils.py (1KB)
|
|
145
|
+
├── tests/
|
|
146
|
+
│ └── test_main.py (3KB)
|
|
147
|
+
├── pyproject.toml (1KB)
|
|
148
|
+
└── README.md (3KB)
|
|
149
|
+
|
|
150
|
+
## Key Files (6 files, 10KB)
|
|
151
|
+
|
|
152
|
+
### `src/main.py`
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
def greet(name):
|
|
156
|
+
return f"Hello, {name}!"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Changelog
|
|
163
|
+
|
|
164
|
+
### v0.2.0
|
|
165
|
+
- Clipboard support (`--clipboard`)
|
|
166
|
+
- Multiple output formats: markdown, plain, JSON
|
|
167
|
+
- Configuration file support (`.codecontext.toml`, `.json`, `.yml`)
|
|
168
|
+
- File sizes in tree output
|
|
169
|
+
- Custom exclude patterns (`--exclude`)
|
|
170
|
+
- Hidden files flag (`--hidden`)
|
|
171
|
+
- Stats summary in output
|
|
172
|
+
|
|
173
|
+
### v0.1.0
|
|
174
|
+
- Initial release
|
|
175
|
+
- File tree with smart ignore patterns
|
|
176
|
+
- Git history integration
|
|
177
|
+
- Syntax highlighting for 30+ languages
|
|
178
|
+
- CLI alias `ctx`
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ctxcode"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Extract your codebase context for AI assistants"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Mohammed Hanan", email = "hananmtp313@gmail.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["cli", "ai", "code-review", "productivity", "developer-tools"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Software Development :: Code Generators",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/hanu-14/codecontext"
|
|
28
|
+
Repository = "https://github.com/hanu-14/codecontext"
|
|
29
|
+
Issues = "https://github.com/hanu-14/codecontext/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
codecontext = "codecontext.cli:main"
|
|
33
|
+
ctx = "codecontext.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/codecontext"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["/src"]
|
|
File without changes
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
import textwrap
|
|
6
|
+
|
|
7
|
+
from . import scanner
|
|
8
|
+
from . import git_utils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
LANG_MAP = {
|
|
12
|
+
".py": "python", ".js": "javascript", ".ts": "typescript",
|
|
13
|
+
".tsx": "tsx", ".jsx": "jsx", ".rs": "rust", ".go": "go",
|
|
14
|
+
".java": "java", ".c": "c", ".cpp": "cpp", ".h": "c",
|
|
15
|
+
".hpp": "cpp", ".rb": "ruby", ".php": "php", ".swift": "swift",
|
|
16
|
+
".kt": "kotlin", ".scala": "scala", ".r": "r",
|
|
17
|
+
".yaml": "yaml", ".yml": "yaml", ".json": "json",
|
|
18
|
+
".toml": "toml", ".xml": "xml", ".md": "markdown",
|
|
19
|
+
".html": "html", ".css": "css", ".scss": "scss",
|
|
20
|
+
".sql": "sql", ".sh": "bash", ".bash": "bash",
|
|
21
|
+
".zsh": "bash", ".dockerfile": "dockerfile",
|
|
22
|
+
".txt": "text", ".cfg": "ini", ".ini": "ini",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_stats(root, key_files):
|
|
27
|
+
total_files = len(key_files)
|
|
28
|
+
total_size = 0
|
|
29
|
+
for _, abs_path in key_files:
|
|
30
|
+
try:
|
|
31
|
+
total_size += os.path.getsize(abs_path)
|
|
32
|
+
except OSError:
|
|
33
|
+
pass
|
|
34
|
+
return {"files": total_files, "size": total_size, "dir": root}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_markdown(root, include_git, max_files, max_lines, extra_ignore, show_hidden):
|
|
38
|
+
blocks = []
|
|
39
|
+
|
|
40
|
+
blocks.append("# Codebase Context\n")
|
|
41
|
+
blocks.append(f"Generated from: `{os.path.abspath(root)}`\n")
|
|
42
|
+
|
|
43
|
+
if include_git:
|
|
44
|
+
info = git_utils.get_git_info(root)
|
|
45
|
+
if info.get("remote"):
|
|
46
|
+
blocks.append(f"Remote: `{info['remote']}`")
|
|
47
|
+
if info.get("branch"):
|
|
48
|
+
blocks.append(f"Branch: `{info['branch']}`")
|
|
49
|
+
if info.get("uncommitted"):
|
|
50
|
+
blocks.append(f"\nUncommitted changes:\n```\n{info['uncommitted']}\n```")
|
|
51
|
+
if info.get("recent_commits"):
|
|
52
|
+
blocks.append(f"\nRecent commits:\n```\n{info['recent_commits']}\n```")
|
|
53
|
+
|
|
54
|
+
tree = scanner.get_tree(root, max_files=max_files, extra_ignore=extra_ignore, show_hidden=show_hidden)
|
|
55
|
+
blocks.append(f"\n## Project Structure\n\n```\n{tree}\n```\n")
|
|
56
|
+
|
|
57
|
+
key_files = scanner.find_key_files(root, extra_ignore=extra_ignore)
|
|
58
|
+
stats = build_stats(root, key_files)
|
|
59
|
+
|
|
60
|
+
blocks.append(f"\n## Key Files ({stats['files']} files, {stats['size'] / 1024:.0f}KB)\n")
|
|
61
|
+
|
|
62
|
+
for rel_path, abs_path in key_files:
|
|
63
|
+
ext = os.path.splitext(rel_path)[1].lower()
|
|
64
|
+
lang = LANG_MAP.get(ext, "")
|
|
65
|
+
content = scanner.read_file(abs_path, max_lines=max_lines)
|
|
66
|
+
blocks.append(f"\n### `{rel_path}`\n\n```{lang}\n{content}```")
|
|
67
|
+
|
|
68
|
+
blocks.append("\n---\n*Generated by [codecontext](https://github.com/hanu-14/codecontext)*")
|
|
69
|
+
return "\n".join(blocks)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def format_plain(root, include_git, max_files, max_lines, extra_ignore, show_hidden):
|
|
73
|
+
lines = []
|
|
74
|
+
|
|
75
|
+
lines.append(f"Project: {os.path.abspath(root)}\n")
|
|
76
|
+
|
|
77
|
+
if include_git:
|
|
78
|
+
info = git_utils.get_git_info(root)
|
|
79
|
+
if info.get("remote"):
|
|
80
|
+
lines.append(f"Remote: {info['remote']}")
|
|
81
|
+
if info.get("branch"):
|
|
82
|
+
lines.append(f"Branch: {info['branch']}")
|
|
83
|
+
if info.get("uncommitted"):
|
|
84
|
+
lines.append(f"\nUncommitted:\n{info['uncommitted']}")
|
|
85
|
+
|
|
86
|
+
lines.append(f"\nProject Structure:\n{scanner.get_tree(root, max_files=max_files, extra_ignore=extra_ignore, show_hidden=show_hidden)}\n")
|
|
87
|
+
|
|
88
|
+
key_files = scanner.find_key_files(root, extra_ignore=extra_ignore)
|
|
89
|
+
for rel_path, abs_path in key_files:
|
|
90
|
+
content = scanner.read_file(abs_path, max_lines=max_lines)
|
|
91
|
+
sep = "=" * 60
|
|
92
|
+
lines.append(f"\n{sep}\nFile: {rel_path}\n{sep}\n{content}")
|
|
93
|
+
|
|
94
|
+
return "\n".join(lines)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def format_json(root, include_git, max_files, max_lines, extra_ignore, show_hidden):
|
|
98
|
+
info = {}
|
|
99
|
+
info["root"] = os.path.abspath(root)
|
|
100
|
+
|
|
101
|
+
if include_git:
|
|
102
|
+
info["git"] = git_utils.get_git_info(root)
|
|
103
|
+
|
|
104
|
+
info["tree"] = scanner.get_tree(root, max_files=max_files, extra_ignore=extra_ignore, show_hidden=show_hidden)
|
|
105
|
+
|
|
106
|
+
key_files = scanner.find_key_files(root, extra_ignore=extra_ignore)
|
|
107
|
+
info["files"] = []
|
|
108
|
+
for rel_path, abs_path in key_files:
|
|
109
|
+
info["files"].append({
|
|
110
|
+
"path": rel_path,
|
|
111
|
+
"content": scanner.read_file(abs_path, max_lines=max_lines),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
info["stats"] = build_stats(root, key_files)
|
|
115
|
+
return json.dumps(info, indent=2)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
FORMATTERS = {
|
|
119
|
+
"markdown": format_markdown,
|
|
120
|
+
"plain": format_plain,
|
|
121
|
+
"json": format_json,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def merge_config_with_args(config, args):
|
|
126
|
+
if args.no_git is None and "git" in config:
|
|
127
|
+
args.no_git = not config["git"]
|
|
128
|
+
if args.max_files is None and "max_files" in config:
|
|
129
|
+
args.max_files = config["max_files"]
|
|
130
|
+
if args.max_lines is None and "max_lines" in config:
|
|
131
|
+
args.max_lines = config["max_lines"]
|
|
132
|
+
if args.format is None and "format" in config:
|
|
133
|
+
args.format = config["format"]
|
|
134
|
+
if args.exclude is None and "exclude" in config:
|
|
135
|
+
args.exclude = config["exclude"]
|
|
136
|
+
if args.output is None and "output" in config:
|
|
137
|
+
args.output = config["output"]
|
|
138
|
+
if args.hidden is None and "hidden" in config:
|
|
139
|
+
args.hidden = config["hidden"]
|
|
140
|
+
return args
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main():
|
|
144
|
+
import argparse
|
|
145
|
+
parser = argparse.ArgumentParser(
|
|
146
|
+
description="Extract codebase context for AI assistants.",
|
|
147
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
148
|
+
epilog=textwrap.dedent("""
|
|
149
|
+
Examples:
|
|
150
|
+
codecontext # full context (markdown)
|
|
151
|
+
codecontext --format json # JSON output
|
|
152
|
+
codecontext --clipboard # copy to clipboard
|
|
153
|
+
codecontext --exclude "*.log" "tmp/" # custom excludes
|
|
154
|
+
codecontext --hidden # include dotfiles
|
|
155
|
+
codecontext -o context.txt --format plain # plain text file
|
|
156
|
+
"""),
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"path", nargs="?", default=None,
|
|
160
|
+
help="Project path (default: current directory)"
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--no-git", action="store_true", default=None,
|
|
164
|
+
help="Skip git information"
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--max-files", type=int, default=None,
|
|
168
|
+
help="Maximum files to show in tree (default: 200)"
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--max-lines", type=int, default=None,
|
|
172
|
+
help="Maximum lines per file (default: 200)"
|
|
173
|
+
)
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--format", "-f", default=None, choices=["markdown", "plain", "json"],
|
|
176
|
+
help="Output format (default: markdown)"
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"--clipboard", "-c", action="store_true",
|
|
180
|
+
help="Copy output to clipboard"
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"--output", "-o", default=None,
|
|
184
|
+
help="Write output to file"
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--exclude", nargs="*", default=None,
|
|
188
|
+
help="Additional exclude patterns (glob)"
|
|
189
|
+
)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--hidden", action="store_true", default=None,
|
|
192
|
+
help="Show hidden files"
|
|
193
|
+
)
|
|
194
|
+
parser.add_argument(
|
|
195
|
+
"--quiet", "-q", action="store_true",
|
|
196
|
+
help="Suppress stderr messages"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
args = parser.parse_args()
|
|
200
|
+
|
|
201
|
+
search_root = os.path.abspath(args.path or ".")
|
|
202
|
+
config = scanner.load_config(search_root)
|
|
203
|
+
args = merge_config_with_args(config, args)
|
|
204
|
+
|
|
205
|
+
if args.path is None:
|
|
206
|
+
args.path = "."
|
|
207
|
+
|
|
208
|
+
root = os.path.abspath(args.path)
|
|
209
|
+
if not os.path.isdir(root):
|
|
210
|
+
print(f"error: {root} is not a directory", file=sys.stderr)
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
fmt = args.format or "markdown"
|
|
214
|
+
formatter = FORMATTERS.get(fmt, FORMATTERS["markdown"])
|
|
215
|
+
|
|
216
|
+
extra_ignore = args.exclude or []
|
|
217
|
+
context = formatter(
|
|
218
|
+
root,
|
|
219
|
+
include_git=not args.no_git if args.no_git is not None else True,
|
|
220
|
+
max_files=args.max_files or 200,
|
|
221
|
+
max_lines=args.max_lines or 200,
|
|
222
|
+
extra_ignore=extra_ignore,
|
|
223
|
+
show_hidden=args.hidden or False,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if args.clipboard:
|
|
227
|
+
try:
|
|
228
|
+
shutil.copy(context, None)
|
|
229
|
+
except Exception:
|
|
230
|
+
import subprocess
|
|
231
|
+
try:
|
|
232
|
+
proc = subprocess.Popen(["clip"], stdin=subprocess.PIPE, shell=True)
|
|
233
|
+
proc.communicate(input=context.encode("utf-8"))
|
|
234
|
+
if not args.quiet:
|
|
235
|
+
print("Copied to clipboard!", file=sys.stderr)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print(f"Clipboard error: {e}", file=sys.stderr)
|
|
238
|
+
|
|
239
|
+
if args.output:
|
|
240
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
241
|
+
f.write(context)
|
|
242
|
+
if not args.quiet:
|
|
243
|
+
print(f"Written to {args.output}", file=sys.stderr)
|
|
244
|
+
else:
|
|
245
|
+
print(context)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_git_info(root):
|
|
6
|
+
info = {}
|
|
7
|
+
try:
|
|
8
|
+
result = subprocess.run(
|
|
9
|
+
["git", "log", "--oneline", "-20"],
|
|
10
|
+
capture_output=True, text=True, cwd=root, timeout=10
|
|
11
|
+
)
|
|
12
|
+
if result.returncode == 0:
|
|
13
|
+
info["recent_commits"] = result.stdout.strip()
|
|
14
|
+
except Exception:
|
|
15
|
+
info["recent_commits"] = ""
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
["git", "remote", "get-url", "origin"],
|
|
20
|
+
capture_output=True, text=True, cwd=root, timeout=5
|
|
21
|
+
)
|
|
22
|
+
if result.returncode == 0:
|
|
23
|
+
info["remote"] = result.stdout.strip()
|
|
24
|
+
except Exception:
|
|
25
|
+
info["remote"] = ""
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
["git", "branch", "--show-current"],
|
|
30
|
+
capture_output=True, text=True, cwd=root, timeout=5
|
|
31
|
+
)
|
|
32
|
+
if result.returncode == 0:
|
|
33
|
+
info["branch"] = result.stdout.strip()
|
|
34
|
+
except Exception:
|
|
35
|
+
info["branch"] = ""
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["git", "diff", "--stat"],
|
|
40
|
+
capture_output=True, text=True, cwd=root, timeout=5
|
|
41
|
+
)
|
|
42
|
+
if result.returncode == 0:
|
|
43
|
+
info["uncommitted"] = result.stdout.strip()
|
|
44
|
+
except Exception:
|
|
45
|
+
info["uncommitted"] = ""
|
|
46
|
+
|
|
47
|
+
return info
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import fnmatch
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
IGNORE_PATTERNS = {
|
|
6
|
+
".git", "__pycache__", "node_modules", ".venv", "venv", "env",
|
|
7
|
+
".tox", ".eggs", "*.egg-info", "dist", "build", ".next",
|
|
8
|
+
".nuxt", "target", "bin", "obj", ".idea", ".vscode",
|
|
9
|
+
"*.pyc", "*.pyo", "*.so", "*.dll", "*.dylib",
|
|
10
|
+
"*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.ico",
|
|
11
|
+
"*.ttf", "*.woff", "*.woff2", "*.eot",
|
|
12
|
+
"*.zip", "*.tar", "*.gz", "*.7z", "*.rar",
|
|
13
|
+
".DS_Store", "Thumbs.db", "*.lock", "package-lock.json",
|
|
14
|
+
"*.min.js", "*.min.css", "*.map",
|
|
15
|
+
".coverage", ".pytest_cache", ".mypy_cache", ".ruff_cache",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
BINARY_EXTENSIONS = {
|
|
19
|
+
".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg",
|
|
20
|
+
".ttf", ".woff", ".woff2", ".eot",
|
|
21
|
+
".zip", ".tar", ".gz", ".7z", ".rar",
|
|
22
|
+
".so", ".dll", ".dylib", ".exe", ".bin",
|
|
23
|
+
".pyc", ".pyo",
|
|
24
|
+
".pdf", ".doc", ".docx", ".xls", ".xlsx",
|
|
25
|
+
".mp3", ".mp4", ".avi", ".mov",
|
|
26
|
+
".woff2", ".woff", ".eot",
|
|
27
|
+
".map", ".min.js", ".min.css",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
CONFIG_FILENAMES = [".codecontext.toml", ".codecontext.json", ".codecontext.yml", ".codecontext.yaml"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def should_ignore(name, extra_patterns=None):
|
|
34
|
+
if any(fnmatch.fnmatch(name, p) for p in IGNORE_PATTERNS):
|
|
35
|
+
return True
|
|
36
|
+
if extra_patterns:
|
|
37
|
+
for p in extra_patterns:
|
|
38
|
+
if fnmatch.fnmatch(name, p):
|
|
39
|
+
return True
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _format_size(size):
|
|
44
|
+
if size < 1024:
|
|
45
|
+
return f"{size}B"
|
|
46
|
+
elif size < 1024 ** 2:
|
|
47
|
+
return f"{size / 1024:.0f}KB"
|
|
48
|
+
elif size < 1024 ** 3:
|
|
49
|
+
return f"{size / 1024 ** 2:.1f}MB"
|
|
50
|
+
return f"{size / 1024 ** 3:.1f}GB"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def scan_tree(root, prefix="", max_files=200, _count=None, extra_ignore=None, show_hidden=False):
|
|
54
|
+
if _count is None:
|
|
55
|
+
_count = [0]
|
|
56
|
+
if _count[0] >= max_files:
|
|
57
|
+
return []
|
|
58
|
+
lines = []
|
|
59
|
+
try:
|
|
60
|
+
entries = sorted(os.listdir(root))
|
|
61
|
+
except PermissionError:
|
|
62
|
+
return lines
|
|
63
|
+
|
|
64
|
+
entries = [e for e in entries if not should_ignore(e, extra_ignore)]
|
|
65
|
+
if not show_hidden:
|
|
66
|
+
entries = [e for e in entries if not e.startswith(".") or e in (".gitignore", ".env.example")]
|
|
67
|
+
|
|
68
|
+
for i, name in enumerate(entries):
|
|
69
|
+
if _count[0] >= max_files:
|
|
70
|
+
break
|
|
71
|
+
path = os.path.join(root, name)
|
|
72
|
+
is_last = i == len(entries) - 1
|
|
73
|
+
connector = "└── " if is_last else "├── "
|
|
74
|
+
|
|
75
|
+
if os.path.isdir(path):
|
|
76
|
+
lines.append(f"{prefix}{connector}{name}/")
|
|
77
|
+
_count[0] += 1
|
|
78
|
+
extension = " " if is_last else "│ "
|
|
79
|
+
lines.extend(scan_tree(path, prefix + extension, max_files, _count, extra_ignore, show_hidden))
|
|
80
|
+
elif os.path.isfile(path):
|
|
81
|
+
try:
|
|
82
|
+
size = os.path.getsize(path)
|
|
83
|
+
size_str = _format_size(size)
|
|
84
|
+
except OSError:
|
|
85
|
+
size_str = "?"
|
|
86
|
+
lines.append(f"{prefix}{connector}{name} ({size_str})")
|
|
87
|
+
_count[0] += 1
|
|
88
|
+
return lines
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_tree(root, max_files=200, extra_ignore=None, show_hidden=False):
|
|
92
|
+
name = os.path.basename(os.path.abspath(root))
|
|
93
|
+
lines = [f"{name}/"]
|
|
94
|
+
lines.extend(scan_tree(root, max_files=max_files, extra_ignore=extra_ignore, show_hidden=show_hidden))
|
|
95
|
+
return "\n".join(lines)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def find_key_files(root, max_size=50_000, extra_ignore=None):
|
|
99
|
+
key_files = []
|
|
100
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
101
|
+
dirnames[:] = [d for d in dirnames if not should_ignore(d, extra_ignore)]
|
|
102
|
+
for f in filenames:
|
|
103
|
+
if should_ignore(f, extra_ignore):
|
|
104
|
+
continue
|
|
105
|
+
path = os.path.join(dirpath, f)
|
|
106
|
+
ext = os.path.splitext(f)[1].lower()
|
|
107
|
+
if ext in BINARY_EXTENSIONS:
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
size = os.path.getsize(path)
|
|
111
|
+
except OSError:
|
|
112
|
+
continue
|
|
113
|
+
if size > max_size:
|
|
114
|
+
continue
|
|
115
|
+
rel = os.path.relpath(path, root)
|
|
116
|
+
key_files.append((rel, path))
|
|
117
|
+
key_files.sort()
|
|
118
|
+
return key_files
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def read_file(path, max_lines=200):
|
|
122
|
+
try:
|
|
123
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
124
|
+
lines = f.readlines()
|
|
125
|
+
if len(lines) > max_lines:
|
|
126
|
+
lines = lines[:max_lines]
|
|
127
|
+
lines.append(f"... ({len(lines)} lines shown, truncated)\n")
|
|
128
|
+
return "".join(lines)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return f"// Error reading file: {e}\n"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_config(root):
|
|
134
|
+
for filename in CONFIG_FILENAMES:
|
|
135
|
+
path = os.path.join(root, filename)
|
|
136
|
+
if not os.path.isfile(path):
|
|
137
|
+
continue
|
|
138
|
+
try:
|
|
139
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
140
|
+
raw = f.read()
|
|
141
|
+
if filename.endswith(".toml"):
|
|
142
|
+
try:
|
|
143
|
+
import tomllib
|
|
144
|
+
return tomllib.loads(raw)
|
|
145
|
+
except ImportError:
|
|
146
|
+
import tomli as tomllib
|
|
147
|
+
return tomllib.loads(raw)
|
|
148
|
+
elif filename.endswith(".json"):
|
|
149
|
+
import json
|
|
150
|
+
return json.loads(raw)
|
|
151
|
+
elif filename.endswith((".yml", ".yaml")):
|
|
152
|
+
import yaml
|
|
153
|
+
return yaml.safe_load(raw)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
return {}
|