srcodex 0.2.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.
- srcodex/__init__.py +0 -0
- srcodex/backend/__init__.py +0 -0
- srcodex/backend/chat.py +79 -0
- srcodex/backend/main.py +98 -0
- srcodex/backend/services/__init__.py +0 -0
- srcodex/backend/services/claude_service.py +754 -0
- srcodex/backend/services/config_loader.py +113 -0
- srcodex/backend/services/file_access_tools.py +279 -0
- srcodex/backend/services/file_tree.py +480 -0
- srcodex/backend/services/graph_tools.py +874 -0
- srcodex/backend/services/logger_setup.py +91 -0
- srcodex/backend/services/session_manager.py +81 -0
- srcodex/backend/services/status_tracker.py +91 -0
- srcodex/cli.py +255 -0
- srcodex/core/__init__.py +0 -0
- srcodex/core/config.py +113 -0
- srcodex/core/logger.py +23 -0
- srcodex/indexer/__init__.py +0 -0
- srcodex/indexer/cscope_client.py +183 -0
- srcodex/indexer/ctags_compat.py +223 -0
- srcodex/indexer/ctags_parser.py +456 -0
- srcodex/indexer/explorer.py +135 -0
- srcodex/indexer/field_access_analyzer.py +436 -0
- srcodex/indexer/indexer.py +664 -0
- srcodex/indexer/reference_ingestor.py +293 -0
- srcodex/indexer/reference_resolver.py +544 -0
- srcodex/tui/__init__.py +0 -0
- srcodex/tui/app.py +103 -0
- srcodex/tui/app.tcss +24 -0
- srcodex/tui/components/__init__.py +0 -0
- srcodex/tui/components/bars/__init__.py +0 -0
- srcodex/tui/components/bars/chat_header.py +48 -0
- srcodex/tui/components/bars/code_tab_bar.py +157 -0
- srcodex/tui/components/bars/footer_bar.py +128 -0
- srcodex/tui/components/bars/left_tab.py +54 -0
- srcodex/tui/components/logger.py +57 -0
- srcodex/tui/components/panels/__init__.py +0 -0
- srcodex/tui/components/panels/chat_panel.py +523 -0
- srcodex/tui/components/panels/code_panel.py +229 -0
- srcodex/tui/components/panels/side_panel.py +128 -0
- srcodex/tui/components/views/__init__.py +0 -0
- srcodex/tui/components/views/explorer_view.py +20 -0
- srcodex/tui/components/views/search_view.py +148 -0
- srcodex/tui/components/widgets/__init__.py +0 -0
- srcodex/tui/components/widgets/file_browser.py +16 -0
- srcodex/tui/components/widgets/find_box.py +85 -0
- srcodex-0.2.0.dist-info/METADATA +170 -0
- srcodex-0.2.0.dist-info/RECORD +52 -0
- srcodex-0.2.0.dist-info/WHEEL +5 -0
- srcodex-0.2.0.dist-info/entry_points.txt +2 -0
- srcodex-0.2.0.dist-info/licenses/LICENSE +21 -0
- srcodex-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Loader - Reads .srcodex/metadata.json
|
|
3
|
+
Provides project paths and stats to all services
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectConfig:
|
|
11
|
+
"""Loads and provides access to .srcodex/metadata.json"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, project_root: Path = None):
|
|
14
|
+
"""
|
|
15
|
+
Initialize config loader
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
project_root: Path to project root (defaults to searching upwards from cwd)
|
|
19
|
+
"""
|
|
20
|
+
if project_root is None:
|
|
21
|
+
# Search upwards from current directory for .srcodex/
|
|
22
|
+
project_root = self._find_project_root()
|
|
23
|
+
|
|
24
|
+
self.project_root = Path(project_root)
|
|
25
|
+
self.srcodex_dir = self.project_root / ".srcodex"
|
|
26
|
+
self.metadata_file = self.srcodex_dir / "metadata.json"
|
|
27
|
+
|
|
28
|
+
# Load metadata
|
|
29
|
+
if not self.metadata_file.exists():
|
|
30
|
+
raise FileNotFoundError(
|
|
31
|
+
f"No .srcodex/metadata.json found in {self.project_root}\n"
|
|
32
|
+
f"Run indexer first to generate .srcodex/ directory"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
with open(self.metadata_file, 'r') as f:
|
|
36
|
+
self.metadata = json.load(f)
|
|
37
|
+
|
|
38
|
+
def _find_project_root(self) -> Path:
|
|
39
|
+
"""
|
|
40
|
+
Search upwards from current directory for .srcodex/ directory
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Path to project root containing .srcodex/
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
FileNotFoundError if .srcodex/ not found in any parent directory
|
|
47
|
+
"""
|
|
48
|
+
current = Path.cwd()
|
|
49
|
+
|
|
50
|
+
# Search up to 10 levels (prevent infinite loop)
|
|
51
|
+
for _ in range(10):
|
|
52
|
+
srcodex_dir = current / ".srcodex"
|
|
53
|
+
if srcodex_dir.exists() and srcodex_dir.is_dir():
|
|
54
|
+
return current
|
|
55
|
+
|
|
56
|
+
# Move to parent
|
|
57
|
+
parent = current.parent
|
|
58
|
+
if parent == current:
|
|
59
|
+
# Reached filesystem root
|
|
60
|
+
break
|
|
61
|
+
current = parent
|
|
62
|
+
|
|
63
|
+
raise FileNotFoundError(
|
|
64
|
+
f"No .srcodex/ directory found in {Path.cwd()} or any parent directory.\n"
|
|
65
|
+
f"Run indexer first to generate .srcodex/ directory"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
with open(self.metadata_file, 'r') as f:
|
|
69
|
+
self.metadata = json.load(f)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def project_name(self) -> str:
|
|
73
|
+
"""Get project name"""
|
|
74
|
+
return self.metadata["project"]["name"]
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def source_root(self) -> Path:
|
|
78
|
+
"""Get absolute path to source root"""
|
|
79
|
+
return self.project_root / self.metadata["paths"]["source_root"]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def database_path(self) -> Path:
|
|
83
|
+
"""Get absolute path to database"""
|
|
84
|
+
return self.project_root / self.metadata["paths"]["database"]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def stats(self) -> Dict[str, Any]:
|
|
88
|
+
"""Get project statistics"""
|
|
89
|
+
return self.metadata["stats"]
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def indexed_at(self) -> str:
|
|
93
|
+
"""Get indexing timestamp"""
|
|
94
|
+
return self.metadata["project"]["indexed_at"]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Global config instance (singleton pattern)
|
|
98
|
+
_config = None
|
|
99
|
+
|
|
100
|
+
def get_config(project_root: Path = None) -> ProjectConfig:
|
|
101
|
+
"""
|
|
102
|
+
Get global project configuration (singleton)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
project_root: Path to project root (only used on first call)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
ProjectConfig instance
|
|
109
|
+
"""
|
|
110
|
+
global _config
|
|
111
|
+
if _config is None:
|
|
112
|
+
_config = ProjectConfig(project_root)
|
|
113
|
+
return _config
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File Access Tools for Claude
|
|
3
|
+
Provides local filesystem access to Claude via tool calling
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Any
|
|
8
|
+
from .config_loader import get_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Load project configuration
|
|
12
|
+
config = get_config()
|
|
13
|
+
SOURCE_ROOT = config.source_root
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_file(file_path: str):
|
|
17
|
+
"""
|
|
18
|
+
Read contents of a local file
|
|
19
|
+
Args: file_path: Relative path from source root or absolute path
|
|
20
|
+
Returns: Dict with file content or error
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
if not Path(file_path).is_absolute():
|
|
24
|
+
full_path = SOURCE_ROOT / file_path
|
|
25
|
+
else:
|
|
26
|
+
full_path = Path(file_path)
|
|
27
|
+
|
|
28
|
+
# ensure path is within source root
|
|
29
|
+
if not str(full_path.resolve()).startswith(str(SOURCE_ROOT.resolve())):
|
|
30
|
+
return {
|
|
31
|
+
"error": "Access denied - path outside project directory",
|
|
32
|
+
"path": file_path
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Block read_file on indexed code files
|
|
36
|
+
code_extensions = {'.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.py', '.js', '.ts', '.java', '.go', '.rs'}
|
|
37
|
+
if full_path.suffix in code_extensions:
|
|
38
|
+
return {
|
|
39
|
+
"error": f"🚫 read_file() blocked on code files ({full_path.suffix}). Use get_symbols_from_file() for 10-100x better token efficiency!",
|
|
40
|
+
"path": str(full_path),
|
|
41
|
+
"suggestion": f"Try: get_symbols_from_file('{file_path}', include_definitions=False) to see symbols, then get_symbol_definition() for specific ones.",
|
|
42
|
+
"why_blocked": "Reading entire files wastes thousands of tokens. The semantic graph has this indexed - use it!"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Check if file exists
|
|
46
|
+
if not full_path.exists():
|
|
47
|
+
return {
|
|
48
|
+
"error": "File not found",
|
|
49
|
+
"path": str(full_path)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if not full_path.is_file():
|
|
53
|
+
return {
|
|
54
|
+
"error": "Path is a directory, not a file",
|
|
55
|
+
"path": str(full_path)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Read File
|
|
59
|
+
with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
60
|
+
content = f.read()
|
|
61
|
+
|
|
62
|
+
# Get file stats
|
|
63
|
+
stat = full_path.stat()
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"path": str(full_path.relative_to(SOURCE_ROOT)),
|
|
67
|
+
"content": content,
|
|
68
|
+
"size_bytes": stat.st_size,
|
|
69
|
+
"lines": len(content.splitlines())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return {
|
|
74
|
+
"error": str(e),
|
|
75
|
+
"path": file_path
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def list_directory(dir_path: str=""):
|
|
80
|
+
"""
|
|
81
|
+
List contents of a directory
|
|
82
|
+
Args: dir_path: Relative path from source root; Empty string for source root
|
|
83
|
+
Returns: Dict with directory listing or error
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
# Convert to absolute path (paths are relative to SOURCE_ROOT, not PROJECT_ROOT)
|
|
87
|
+
if not dir_path:
|
|
88
|
+
full_path = SOURCE_ROOT
|
|
89
|
+
elif not Path(dir_path).is_absolute():
|
|
90
|
+
full_path = SOURCE_ROOT / dir_path
|
|
91
|
+
else:
|
|
92
|
+
full_path = Path(dir_path)
|
|
93
|
+
|
|
94
|
+
# Security check (must be within SOURCE_ROOT)
|
|
95
|
+
if not str(full_path.resolve()).startswith(str(SOURCE_ROOT.resolve())):
|
|
96
|
+
return {
|
|
97
|
+
"error": "Access denied - path outside source directory",
|
|
98
|
+
"path": dir_path
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Check if directory exists
|
|
102
|
+
if not full_path.exists():
|
|
103
|
+
return {
|
|
104
|
+
"error": "Directory not found",
|
|
105
|
+
"path": str(full_path)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if not full_path.is_dir():
|
|
109
|
+
return {
|
|
110
|
+
"error": "Path is a file, not a directory",
|
|
111
|
+
"path": str(full_path)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# List directory contents (max 100)
|
|
115
|
+
entries = []
|
|
116
|
+
all_items = sorted(full_path.iterdir())
|
|
117
|
+
total_count = len(all_items)
|
|
118
|
+
|
|
119
|
+
for item in all_items[:100]:
|
|
120
|
+
entry = {
|
|
121
|
+
"name": item.name,
|
|
122
|
+
"type": "directory" if item.is_dir() else "file",
|
|
123
|
+
"path": str(item.relative_to(SOURCE_ROOT))
|
|
124
|
+
}
|
|
125
|
+
# Add size for files
|
|
126
|
+
if item.is_file():
|
|
127
|
+
entry["size_bytes"] = item.stat().st_size
|
|
128
|
+
entries.append(entry)
|
|
129
|
+
|
|
130
|
+
result = {
|
|
131
|
+
"path": str(full_path.relative_to(SOURCE_ROOT)) if dir_path else ".",
|
|
132
|
+
"entries": entries,
|
|
133
|
+
"count": len(entries),
|
|
134
|
+
"total_count": total_count
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if total_count > 100:
|
|
138
|
+
result["truncated"] = True
|
|
139
|
+
result["message"] = f"Showing first 100 of {total_count} entries. Use search_files or get_file_by_pattern for specific files."
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return {
|
|
145
|
+
"error": str(e),
|
|
146
|
+
"path": dir_path
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def search_files(pattern: str, search_path: str = ""):
|
|
151
|
+
"""
|
|
152
|
+
Search for files matching a pattern (glob-style)
|
|
153
|
+
Args:
|
|
154
|
+
pattern: Glob pattern (e.g., "*.c", "**/*.h", "main*")
|
|
155
|
+
search_path: Directory to search in (relative to source root, e.g., 'firmware/main/mp1')
|
|
156
|
+
Empty string searches entire source tree
|
|
157
|
+
Returns: Dict with matching files or error
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
# Convert to absolute path (relative to SOURCE_ROOT)
|
|
161
|
+
if not search_path:
|
|
162
|
+
full_path = SOURCE_ROOT
|
|
163
|
+
elif not Path(search_path).is_absolute():
|
|
164
|
+
full_path = SOURCE_ROOT / search_path
|
|
165
|
+
else:
|
|
166
|
+
full_path = Path(search_path)
|
|
167
|
+
|
|
168
|
+
# Security check
|
|
169
|
+
if not str(full_path.resolve()).startswith(str(SOURCE_ROOT.resolve())):
|
|
170
|
+
return {
|
|
171
|
+
"error": "Access denied - path outside project directory",
|
|
172
|
+
"path": search_path
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if not full_path.exists():
|
|
176
|
+
return {
|
|
177
|
+
"error": "Search path not found",
|
|
178
|
+
"path": str(full_path)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Search for matching files
|
|
182
|
+
matches = []
|
|
183
|
+
for match in full_path.glob(pattern):
|
|
184
|
+
if match.is_file():
|
|
185
|
+
matches.append({
|
|
186
|
+
"name": match.name,
|
|
187
|
+
"path": str(match.relative_to(SOURCE_ROOT)),
|
|
188
|
+
"size_bytes": match.stat().st_size
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
# Sort by path
|
|
192
|
+
matches.sort(key=lambda x: x["path"])
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"pattern": pattern,
|
|
196
|
+
"search_path": str(full_path.relative_to(SOURCE_ROOT)) if search_path else ".",
|
|
197
|
+
"matches": matches,
|
|
198
|
+
"count": len(matches)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
return {
|
|
203
|
+
"error": str(e),
|
|
204
|
+
"pattern": pattern,
|
|
205
|
+
"search_path": search_path
|
|
206
|
+
}
|
|
207
|
+
# Tool definitions for Claude API
|
|
208
|
+
TOOL_DEFINITIONS = [
|
|
209
|
+
{
|
|
210
|
+
"name": "read_file",
|
|
211
|
+
"description": "Read the contents of a file. WARNING: Do NOT use this on .c/.h files - they are indexed in the database. For code files, use get_symbols_from_file() instead (10-100x cheaper). Only use read_file for non-code files like .md, .txt, .json, .toml.",
|
|
212
|
+
"input_schema": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"properties": {
|
|
215
|
+
"file_path": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"description": "Relative path to the file from project root (e.g., 'pmfw_source/mp1/main.c')"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"required": ["file_path"]
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "list_directory",
|
|
225
|
+
"description": "List files and directories in a given directory (max 100 entries). For large directories, returns first 100 and sets truncated=true. Use search_files or get_file_by_pattern if you need specific files.",
|
|
226
|
+
"input_schema": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"properties": {
|
|
229
|
+
"dir_path": {
|
|
230
|
+
"type": "string",
|
|
231
|
+
"description": "Relative path to directory from project root. Use empty string or '.' for project root."
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
"required": ["dir_path"]
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "search_files",
|
|
239
|
+
"description": "Search for files matching a glob pattern (e.g., '*.c' for all C files, '**/*.h' for all headers recursively). Use this to find specific types of files.",
|
|
240
|
+
"input_schema": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"properties": {
|
|
243
|
+
"pattern": {
|
|
244
|
+
"type": "string",
|
|
245
|
+
"description": "Glob pattern to match (e.g., '*.c', '**/*.py', 'main*')"
|
|
246
|
+
},
|
|
247
|
+
"search_path": {
|
|
248
|
+
"type": "string",
|
|
249
|
+
"description": "Directory to search within (relative to project root). Use empty string to search entire project."
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
"required": ["pattern"]
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
def execute_tool(tool_name: str, tool_input: Dict[str, Any]) -> Dict[str, Any]:
|
|
258
|
+
"""
|
|
259
|
+
Execute a file tool by name
|
|
260
|
+
Args:
|
|
261
|
+
tool_name: Name of the tool to execute
|
|
262
|
+
tool_input: Input parameters for the tool
|
|
263
|
+
Returns:
|
|
264
|
+
Tool execution result
|
|
265
|
+
"""
|
|
266
|
+
if tool_name == "read_file":
|
|
267
|
+
return read_file(tool_input.get("file_path", ""))
|
|
268
|
+
|
|
269
|
+
elif tool_name == "list_directory":
|
|
270
|
+
return list_directory(tool_input.get("dir_path", ""))
|
|
271
|
+
|
|
272
|
+
elif tool_name == "search_files":
|
|
273
|
+
return search_files(
|
|
274
|
+
pattern=tool_input.get("pattern", "*"),
|
|
275
|
+
search_path=tool_input.get("search_path", "")
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
else:
|
|
279
|
+
return {"error": f"Unknown tool: {tool_name}"}
|