coderay 1.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.
coderay/vcs/git.py ADDED
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ import pathspec
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def load_gitignore(repo_root: Path) -> pathspec.PathSpec:
13
+ """Parse .gitignore into a PathSpec matcher."""
14
+ gitignore = repo_root / ".gitignore"
15
+ if not gitignore.is_file():
16
+ return pathspec.PathSpec.from_lines("gitignore", [])
17
+ try:
18
+ lines = gitignore.read_text(encoding="utf-8", errors="replace").splitlines()
19
+ return pathspec.PathSpec.from_lines("gitignore", lines)
20
+ except Exception:
21
+ logger.warning("Failed to parse .gitignore; no patterns loaded")
22
+ return pathspec.PathSpec.from_lines("gitignore", [])
23
+
24
+
25
+ def _parse_status_line(line: str) -> tuple[str, str] | None:
26
+ """Parse one line of git status --short --porcelain."""
27
+ if len(line) < 4:
28
+ return None
29
+ status_xy = line[:2]
30
+ path_part = line[3:].strip()
31
+ if " -> " in path_part:
32
+ path_part = path_part.split(" -> ", 1)[1].strip()
33
+ return (status_xy, path_part)
34
+
35
+
36
+ class Git:
37
+ """Git operations for file discovery and change detection."""
38
+
39
+ def __init__(self, repo_root: str | Path = ".") -> None:
40
+ """Initialize with the repository root."""
41
+ self.repo_root = Path(repo_root)
42
+
43
+ def run(self, *args: str) -> str:
44
+ """Run a git command and return stripped stdout."""
45
+ result = subprocess.run(
46
+ ["git", *args],
47
+ cwd=self.repo_root,
48
+ capture_output=True,
49
+ text=True,
50
+ timeout=30,
51
+ )
52
+ if result.returncode != 0:
53
+ raise RuntimeError(
54
+ f"git {' '.join(args)}: {result.stderr or result.stdout}"
55
+ )
56
+ return (result.stdout or "").strip()
57
+
58
+ def get_head_commit(self) -> str | None:
59
+ """Return current HEAD commit hash, or None if unavailable."""
60
+ try:
61
+ return self.run("rev-parse", "HEAD")
62
+ except (RuntimeError, FileNotFoundError):
63
+ return None
64
+
65
+ def get_current_branch(self) -> str | None:
66
+ """Return current branch name, or None if detached HEAD."""
67
+ try:
68
+ name = self.run("rev-parse", "--abbrev-ref", "HEAD")
69
+ if name and name != "HEAD":
70
+ return name
71
+ except (RuntimeError, FileNotFoundError):
72
+ pass
73
+ return None
74
+
75
+ def is_branch_switched(self, last_branch: str | None = None) -> bool:
76
+ """Return True if the current branch differs from *last_branch*."""
77
+ current_branch = self.get_current_branch()
78
+ return (
79
+ current_branch is not None
80
+ and last_branch is not None
81
+ and current_branch != last_branch
82
+ )
83
+
84
+ def get_files_to_index(
85
+ self, last_commit: str | None
86
+ ) -> tuple[list[Path], list[str]]:
87
+ """Return (paths_to_add, paths_to_remove) since last indexed commit."""
88
+ from coderay.chunking.registry import get_supported_extensions
89
+
90
+ supported = get_supported_extensions()
91
+
92
+ def _is_supported(path: str) -> bool:
93
+ from pathlib import PurePosixPath
94
+
95
+ return PurePosixPath(path).suffix in supported
96
+
97
+ to_add: set[str] = set()
98
+ to_remove: set[str] = set()
99
+
100
+ try:
101
+ head = self.get_head_commit()
102
+ if not head or last_commit is None:
103
+ return [], []
104
+
105
+ # 1. Committed changes since last indexed commit.
106
+ try:
107
+ diff_out = self.run("diff", "--name-status", f"{last_commit}...HEAD")
108
+ for line in diff_out.splitlines():
109
+ parts = line.split("\t")
110
+ if len(parts) < 2:
111
+ continue
112
+ status = parts[0].strip()
113
+ path = (
114
+ parts[-1].strip() if status in ("R", "C") else parts[1].strip()
115
+ )
116
+ if not _is_supported(path):
117
+ continue
118
+ if status == "D":
119
+ to_remove.add(path)
120
+ else:
121
+ to_add.add(path)
122
+ except RuntimeError:
123
+ pass
124
+
125
+ # 2. Uncommitted changes (working tree + staging area).
126
+ # Instead of matching against a status-code set, check
127
+ # the filesystem: if the file exists → re-index,
128
+ # otherwise → remove.
129
+ try:
130
+ status_out = self.run("status", "--short", "--porcelain")
131
+ for line in status_out.splitlines():
132
+ parsed = _parse_status_line(line)
133
+ if not parsed:
134
+ continue
135
+ status_xy, path = parsed
136
+ if status_xy == "!!":
137
+ continue
138
+ if not _is_supported(path):
139
+ continue
140
+ if (self.repo_root / path).is_file():
141
+ to_add.add(path)
142
+ else:
143
+ to_remove.add(path)
144
+ except RuntimeError:
145
+ pass
146
+
147
+ except Exception as e:
148
+ logger.warning("Git sync failed: %s", e)
149
+ return [], []
150
+
151
+ add_paths = [self.repo_root / p for p in sorted(to_add)]
152
+ remove_paths = sorted(to_remove)
153
+ return add_paths, remove_paths
154
+
155
+ def discover_python_files(self) -> list[Path]:
156
+ """List all Python files. Alias for ``discover_files(extensions={'.py'})``."""
157
+ return self.discover_files(extensions={".py"})
158
+
159
+ def discover_files(
160
+ self,
161
+ extensions: set[str] | None = None,
162
+ ) -> list[Path]:
163
+ """List all source files matching the given extensions."""
164
+ if extensions is None:
165
+ from coderay.chunking.registry import get_supported_extensions
166
+
167
+ extensions = get_supported_extensions()
168
+
169
+ if (self.repo_root / ".git").exists():
170
+ try:
171
+ globs = [f"*{ext}" for ext in extensions]
172
+ out = self.run("ls-files", *globs)
173
+ if out:
174
+ lines = [p.strip() for p in out.splitlines() if p.strip()]
175
+ return sorted(self.repo_root / p for p in lines)
176
+ except (RuntimeError, FileNotFoundError):
177
+ pass
178
+
179
+ # Fallback: rglob filtered through .gitignore.
180
+ ignore = load_gitignore(self.repo_root)
181
+ result: list[Path] = []
182
+ for f in self.repo_root.rglob("*"):
183
+ if not f.is_file():
184
+ continue
185
+ if f.suffix not in extensions:
186
+ continue
187
+ rel = str(f.relative_to(self.repo_root))
188
+ if ignore.match_file(rel):
189
+ continue
190
+ if ".git" in Path(rel).parts:
191
+ continue
192
+ result.append(f)
193
+ return sorted(result)
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: coderay
3
+ Version: 1.0.0
4
+ Summary: X-ray your codebase — semantic search, code graphs, file skeletons, and MCP server
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: python-dotenv>=1.0.0
8
+ Requires-Dist: networkx>=3.0
9
+ Requires-Dist: tree-sitter>=0.24.0
10
+ Requires-Dist: tree-sitter-python>=0.25.0
11
+ Requires-Dist: lancedb>=0.5.0
12
+ Requires-Dist: pyyaml>=6.0
13
+ Requires-Dist: click>=8.0
14
+ Requires-Dist: filelock>=3.0
15
+ Requires-Dist: fastembed>=0.4.0
16
+ Requires-Dist: watchdog>=4.0.0
17
+ Requires-Dist: pathspec>=0.12.0
18
+ Provides-Extra: openai
19
+ Requires-Dist: openai>=1.0.0; extra == "openai"
20
+ Provides-Extra: languages
21
+ Requires-Dist: tree-sitter-javascript>=0.23.0; extra == "languages"
22
+ Requires-Dist: tree-sitter-typescript>=0.23.0; extra == "languages"
23
+ Requires-Dist: tree-sitter-go>=0.23.0; extra == "languages"
24
+ Provides-Extra: mcp
25
+ Requires-Dist: mcp>=1.0.0; extra == "mcp"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.8.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
31
+ Requires-Dist: openai>=1.0.0; extra == "dev"
32
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
33
+ Requires-Dist: mcp>=1.0.0; extra == "dev"
34
+ Provides-Extra: maintain
35
+ Requires-Dist: pylance>=0.15.0; extra == "maintain"
36
+ Provides-Extra: all
37
+ Requires-Dist: coderay[dev,languages,maintain,mcp,openai]; extra == "all"
38
+
39
+ # CodeRay
40
+
41
+ A local, offline-first semantic code indexer. Builds a vector index,
42
+ call/import graph, and file skeletons — exposed as an MCP server for
43
+ AI coding assistants and a standalone CLI.
44
+
45
+ ## What you get
46
+
47
+ | Capability | What it does | Why it matters | AI assistant benefit |
48
+ |---|---|---|---|
49
+ | **Semantic search** | Find code by meaning, not keywords. "where do we handle auth errors" returns results even if the code never uses that phrase. | Grep finds text. This finds *intent*. | Better context retrieval for plan and edit modes |
50
+ | **Blast radius** (`get_impact_radius`) | Given a function or module, show every node reachable within N hops via calls, imports, and inheritance. | Before changing `UserService.save()`, see exactly what breaks. | Safer refactors — agent sees downstream impact before editing |
51
+ | **File skeleton** (`get_file_skeleton`) | Signatures, docstrings, imports — no function bodies. The API surface of a file at a glance. | Understand a 500-line file in 30 lines without reading the implementation. | Drastically fewer tokens than reading the full file |
52
+ | **Index status** | Chunk count, schema version, branch, last commit, store health. | Confirm the index is fresh before relying on results. | Agent self-checks before trusting search results |
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install "coderay[all] @ git+https://github.com/bogdan-copocean/coderay.git"
58
+ ```
59
+
60
+ For development:
61
+
62
+ ```bash
63
+ git clone https://github.com/bogdan-copocean/coderay.git
64
+ cd coderay
65
+ pip install -e ".[all]"
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ ```bash
71
+ cd /path/to/your/project
72
+ coderay build --repo .
73
+ coderay search "how does authentication work"
74
+ coderay watch --repo .
75
+ coderay graph --kind calls
76
+ coderay skeleton src/app/main.py
77
+ ```
78
+
79
+ ## MCP server (Claude Code / Cursor)
80
+
81
+ Add to `~/.claude/claude_code_config.json` or Cursor MCP settings:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "coderay": {
87
+ "command": "/path/to/your/.venv/bin/coderay-mcp",
88
+ "args": []
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## CLI reference
95
+
96
+ | Command | Description |
97
+ |---|---|
98
+ | `coderay build [--full] --repo .` | Build index (incremental or full rebuild) |
99
+ | `coderay update --repo .` | Incremental update (changed files only) |
100
+ | `coderay watch --repo . [--debounce N]` | Watch for file changes, re-index automatically |
101
+ | `coderay search "query" [--top-k N]` | Semantic search |
102
+ | `coderay list [--by-file]` | List indexed chunks |
103
+ | `coderay status` | Index state, branch, commit, chunk count |
104
+ | `coderay maintain --repo .` | Compact index, reclaim space |
105
+ | `coderay skeleton FILE` | Print file skeleton |
106
+ | `coderay graph --kind calls\|imports` | List graph edges |
107
+
108
+ ## Configuration
109
+
110
+ Optional `config.yaml` in the index directory:
111
+
112
+ ```yaml
113
+ embedder:
114
+ provider: local # local | openai
115
+ model: all-MiniLM-L6-v2
116
+ dimensions: 384
117
+
118
+ search:
119
+ boost_rules:
120
+ "tests/": 0.5
121
+ "src/core/": 1.2
122
+
123
+ graph:
124
+ exclude_callees:
125
+ - "our_sdk_helper"
126
+ include_callees:
127
+ - "isinstance"
128
+
129
+ watch:
130
+ debounce_seconds: 2
131
+ branch_switch_threshold: 50
132
+ exclude_patterns:
133
+ - "*.log"
134
+ ```
135
+
136
+ ## Development
137
+
138
+ ```bash
139
+ pip install -e ".[dev]"
140
+ make test
141
+ make lint
142
+ make format
143
+ ```
144
+
145
+ Requires Python >= 3.10 and Git.
@@ -0,0 +1,42 @@
1
+ coderay/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
2
+ coderay/chunking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ coderay/chunking/chunker.py,sha256=eTmrdXGRXFGfQIoLR0wpsxoP5g-jQFa8slVnqk7-iTA,4367
4
+ coderay/chunking/registry.py,sha256=0w-oPmVJnjCs-XDxFNuKCsdTudtko2iOpFws2yUPo0E,5513
5
+ coderay/cli/__init__.py,sha256=E2mlFoZoEETEb5D3eZRfWSx1cwRDcRyZtRojFl792Q4,70
6
+ coderay/cli/commands.py,sha256=T7Hh0dCNvUFypUts_2ae2NLhILgECLK8uzVT4tY4KLk,14540
7
+ coderay/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ coderay/core/config.py,sha256=_Pt_UhWxKmfnFm2XujLTqepK-ben0K6RiLZz-cDC4Zo,2098
9
+ coderay/core/lock.py,sha256=mYvz98uObmE2xB9WBGLnKMsjHtNiROFPIycXq6NHVqE,1009
10
+ coderay/core/models.py,sha256=dA4kpnu1Y5CngX-C9q5OOF0mZ4W7bpocdJxEkPekE4M,1544
11
+ coderay/core/timing.py,sha256=zushfkPk2ZW2aexuJjAtjkJMZtPuWB3XVOiIt7qtKT4,1206
12
+ coderay/core/utils.py,sha256=mS7l95e0CHWQyYoCKHbXNutovMrZmM-MuGJsd7q45ps,1007
13
+ coderay/embedding/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ coderay/embedding/base.py,sha256=NfVZYXta8LtQkLxGQnXooO3mvkrDE8c8l_8-SnFZxzo,1898
15
+ coderay/embedding/local.py,sha256=kWD3xxGnyh9jESruzGVnfkpwvHBk2s_IOIEDOf0CrqQ,2270
16
+ coderay/embedding/openai.py,sha256=YV-s_54vg6gmtsgqY_mkazwKsTNpBmG-ewUlsL9POPM,2664
17
+ coderay/graph/__init__.py,sha256=n7xjWW75ixceMYDiFey-rWA0gnv9EiacN9gxC6IW24k,390
18
+ coderay/graph/builder.py,sha256=jLrIeoBZlHowk7LukOXrKt-9EinXiEDG_k2owCykUpk,4178
19
+ coderay/graph/code_graph.py,sha256=K2ot0xTEzr2OnkNZ98r-ENQNI6dtbGuBttSsuVi-aJU,11834
20
+ coderay/graph/extractor.py,sha256=tVYU6DRwuR3Sg5yEWhmwqVvHoFjUu0LuDRkoPenb2WQ,11374
21
+ coderay/mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ coderay/mcp_server/server.py,sha256=FPkQeP0GMMipzb2WE8b1uW8jgfS3ckJLFxDh9UUucbk,5266
23
+ coderay/pipeline/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ coderay/pipeline/indexer.py,sha256=OQoclLmiccp8fePIQZ-EgX_E4GlIzMWmQmqpOylY158,14562
25
+ coderay/pipeline/watcher.py,sha256=qcaf1qn4laWlKPqfnpPj-o8BOVQMB9Llo7D9cvhx7y4,10819
26
+ coderay/retrieval/__init__.py,sha256=wSvm8oqG2cDJ4ZwU6C3Nr53-X6peTKWTjMJ8ppvUoSM,72
27
+ coderay/retrieval/boosting.py,sha256=Dt155PUx6jpNyC-uJXC32aN3U-tSUoYxUyrMM6UjGkw,2708
28
+ coderay/retrieval/search.py,sha256=8HcDxI4ETn68dTTxH-SkcG1rI-u13zvIgvt0YMqAl8c,4082
29
+ coderay/skeleton/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ coderay/skeleton/extractor.py,sha256=8Sj5uRK3aD2N5xodSADyuE1n7Lk-QnY6UgaZaEByOu0,4327
31
+ coderay/state/__init__.py,sha256=S-jhUo2d3MHUpA-iZ7-aBsZpfOiLKZyjiHx5_kXiTMI,170
32
+ coderay/state/machine.py,sha256=9ZLp_ipELbl8zuudt1OkBOHLRd_j8Jd0ZIlDr7nbz6s,8080
33
+ coderay/state/version.py,sha256=7lhm_5P-BZorRWDjdRA6WPsbYd8c6sTyFLgcAVqFeW0,1460
34
+ coderay/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ coderay/storage/lancedb.py,sha256=ObI4FZImt4tliXES9r1ZMBUiXglf8y86b_lAGGpffHQ,9106
36
+ coderay/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ coderay/vcs/git.py,sha256=7BXSD6WiN6x00y1L-AfsmGCluGrADkZBd-PnkMGwK3Q,6873
38
+ coderay-1.0.0.dist-info/METADATA,sha256=KEjusMfKk54wD4mEczTtxOdxEME3OcKTBEr_bMqte1o,4572
39
+ coderay-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
40
+ coderay-1.0.0.dist-info/entry_points.txt,sha256=65dwbAFCsgaWMOU_2t96DdRPQpR8FKS0aNVRpP-z8R8,99
41
+ coderay-1.0.0.dist-info/top_level.txt,sha256=cSLZvS3I0PUDfAXcHaWDg2hqt5GonzlqCBoSaPM7Dxg,8
42
+ coderay-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ coderay = coderay.cli.commands:main
3
+ coderay-mcp = coderay.mcp_server.server:main
@@ -0,0 +1 @@
1
+ coderay