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.
Files changed (52) hide show
  1. srcodex/__init__.py +0 -0
  2. srcodex/backend/__init__.py +0 -0
  3. srcodex/backend/chat.py +79 -0
  4. srcodex/backend/main.py +98 -0
  5. srcodex/backend/services/__init__.py +0 -0
  6. srcodex/backend/services/claude_service.py +754 -0
  7. srcodex/backend/services/config_loader.py +113 -0
  8. srcodex/backend/services/file_access_tools.py +279 -0
  9. srcodex/backend/services/file_tree.py +480 -0
  10. srcodex/backend/services/graph_tools.py +874 -0
  11. srcodex/backend/services/logger_setup.py +91 -0
  12. srcodex/backend/services/session_manager.py +81 -0
  13. srcodex/backend/services/status_tracker.py +91 -0
  14. srcodex/cli.py +255 -0
  15. srcodex/core/__init__.py +0 -0
  16. srcodex/core/config.py +113 -0
  17. srcodex/core/logger.py +23 -0
  18. srcodex/indexer/__init__.py +0 -0
  19. srcodex/indexer/cscope_client.py +183 -0
  20. srcodex/indexer/ctags_compat.py +223 -0
  21. srcodex/indexer/ctags_parser.py +456 -0
  22. srcodex/indexer/explorer.py +135 -0
  23. srcodex/indexer/field_access_analyzer.py +436 -0
  24. srcodex/indexer/indexer.py +664 -0
  25. srcodex/indexer/reference_ingestor.py +293 -0
  26. srcodex/indexer/reference_resolver.py +544 -0
  27. srcodex/tui/__init__.py +0 -0
  28. srcodex/tui/app.py +103 -0
  29. srcodex/tui/app.tcss +24 -0
  30. srcodex/tui/components/__init__.py +0 -0
  31. srcodex/tui/components/bars/__init__.py +0 -0
  32. srcodex/tui/components/bars/chat_header.py +48 -0
  33. srcodex/tui/components/bars/code_tab_bar.py +157 -0
  34. srcodex/tui/components/bars/footer_bar.py +128 -0
  35. srcodex/tui/components/bars/left_tab.py +54 -0
  36. srcodex/tui/components/logger.py +57 -0
  37. srcodex/tui/components/panels/__init__.py +0 -0
  38. srcodex/tui/components/panels/chat_panel.py +523 -0
  39. srcodex/tui/components/panels/code_panel.py +229 -0
  40. srcodex/tui/components/panels/side_panel.py +128 -0
  41. srcodex/tui/components/views/__init__.py +0 -0
  42. srcodex/tui/components/views/explorer_view.py +20 -0
  43. srcodex/tui/components/views/search_view.py +148 -0
  44. srcodex/tui/components/widgets/__init__.py +0 -0
  45. srcodex/tui/components/widgets/file_browser.py +16 -0
  46. srcodex/tui/components/widgets/find_box.py +85 -0
  47. srcodex-0.2.0.dist-info/METADATA +170 -0
  48. srcodex-0.2.0.dist-info/RECORD +52 -0
  49. srcodex-0.2.0.dist-info/WHEEL +5 -0
  50. srcodex-0.2.0.dist-info/entry_points.txt +2 -0
  51. srcodex-0.2.0.dist-info/licenses/LICENSE +21 -0
  52. 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}"}