ctxcode 0.2.0__tar.gz → 0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxcode
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Extract your codebase context for AI assistants
5
5
  Project-URL: Homepage, https://github.com/hanu-14/codecontext
6
6
  Project-URL: Repository, https://github.com/hanu-14/codecontext
@@ -28,7 +28,7 @@ Description-Content-Type: text/markdown
28
28
  <img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+">
29
29
  <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT">
30
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">
31
+ <img src="https://img.shields.io/badge/version-0.2.1-blue" alt="v0.2.1">
32
32
  </p>
33
33
 
34
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.
@@ -46,14 +46,14 @@ When asking AI assistants about your code, you spend minutes copy-pasting file t
46
46
  ## Install
47
47
 
48
48
  ```bash
49
- pip install codecontext
49
+ pip install ctxcode
50
50
  ```
51
51
 
52
52
  Or run directly:
53
53
 
54
54
  ```bash
55
- curl -O https://raw.githubusercontent.com/hanu-14/codecontext/main/src/codecontext/cli.py
56
- python cli.py
55
+ curl -O https://raw.githubusercontent.com/hanu-14/codecontext/main/src/ctxcode/cli.py
56
+ python -m ctxcode
57
57
  ```
58
58
 
59
59
  ## Usage
@@ -8,7 +8,7 @@
8
8
  <img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+">
9
9
  <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT">
10
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">
11
+ <img src="https://img.shields.io/badge/version-0.2.1-blue" alt="v0.2.1">
12
12
  </p>
13
13
 
14
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.
@@ -26,14 +26,14 @@ When asking AI assistants about your code, you spend minutes copy-pasting file t
26
26
  ## Install
27
27
 
28
28
  ```bash
29
- pip install codecontext
29
+ pip install ctxcode
30
30
  ```
31
31
 
32
32
  Or run directly:
33
33
 
34
34
  ```bash
35
- curl -O https://raw.githubusercontent.com/hanu-14/codecontext/main/src/codecontext/cli.py
36
- python cli.py
35
+ curl -O https://raw.githubusercontent.com/hanu-14/codecontext/main/src/ctxcode/cli.py
36
+ python -m ctxcode
37
37
  ```
38
38
 
39
39
  ## Usage
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ctxcode"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Extract your codebase context for AI assistants"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -29,11 +29,11 @@ Repository = "https://github.com/hanu-14/codecontext"
29
29
  Issues = "https://github.com/hanu-14/codecontext/issues"
30
30
 
31
31
  [project.scripts]
32
- codecontext = "codecontext.cli:main"
33
- ctx = "codecontext.cli:main"
32
+ codecontext = "ctxcode.cli:main"
33
+ ctx = "ctxcode.cli:main"
34
34
 
35
35
  [tool.hatch.build.targets.wheel]
36
- packages = ["src/codecontext"]
36
+ packages = ["src/ctxcode"]
37
37
 
38
38
  [tool.hatch.build.targets.sdist]
39
- include = ["/src"]
39
+ include = ["/src/ctxcode"]
File without changes
@@ -1,249 +0,0 @@
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()
@@ -1,47 +0,0 @@
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
@@ -1,156 +0,0 @@
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 {}
File without changes
File without changes