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,874 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic Graph Query Tools
|
|
3
|
+
These tools expose the semantic graph database to Claude,
|
|
4
|
+
Enables graph tools to query codebase database without reading entire source files.
|
|
5
|
+
|
|
6
|
+
Token savings: 50-200x for semantic queries vs file reading.
|
|
7
|
+
"""
|
|
8
|
+
import sqlite3
|
|
9
|
+
from typing import List, Dict, Optional, Any
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import os
|
|
12
|
+
from .config_loader import get_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Load project configuration
|
|
16
|
+
config = get_config()
|
|
17
|
+
SOURCE_ROOT = config.source_root
|
|
18
|
+
DEFAULT_DB_PATH = str(config.database_path)
|
|
19
|
+
|
|
20
|
+
class GraphTools:
|
|
21
|
+
"""Database query tools for exploring semnatic graph"""
|
|
22
|
+
def __init__(self, db_path: str):
|
|
23
|
+
self.db_path = db_path
|
|
24
|
+
self.conn = sqlite3.connect(db_path)
|
|
25
|
+
self.conn.row_factory = sqlite3.Row
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_callers(self, function_name: str):
|
|
29
|
+
"""
|
|
30
|
+
Find all functions that call the given function.
|
|
31
|
+
Answwers: "waht calls FunctionX
|
|
32
|
+
Args:
|
|
33
|
+
function_name: Name of function to find callers for
|
|
34
|
+
Returns:
|
|
35
|
+
List of caller symbols with metadata:
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
'name': 'functionCaller',
|
|
39
|
+
'type': 'function',
|
|
40
|
+
'file_path': 'file_path',
|
|
41
|
+
'line_number': 156,
|
|
42
|
+
'signature': '(void)',
|
|
43
|
+
'call_site_line': 162
|
|
44
|
+
},
|
|
45
|
+
...
|
|
46
|
+
]
|
|
47
|
+
"""
|
|
48
|
+
cursor = self.conn.cursor()
|
|
49
|
+
|
|
50
|
+
# Step 1: Find Target function's symbole ID(s)
|
|
51
|
+
cursor.execute("""
|
|
52
|
+
SELECT id FROM symbols
|
|
53
|
+
WHERE name = ? AND type = 'function'
|
|
54
|
+
""", (function_name,))
|
|
55
|
+
target_ids = [row['id'] for row in cursor.fetchall()]
|
|
56
|
+
|
|
57
|
+
if not target_ids:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
#step 2: Find all CALL edges pointing to this functions
|
|
61
|
+
placeholders = ','.join('?' * len(target_ids))
|
|
62
|
+
cursor.execute(f"""
|
|
63
|
+
SELECT src_symbol_id, line_number as call_site_line
|
|
64
|
+
FROM symbol_edges
|
|
65
|
+
WHERE edge_type = 'CALLS'
|
|
66
|
+
AND dst_symbol_id IN ({placeholders})
|
|
67
|
+
""", target_ids)
|
|
68
|
+
|
|
69
|
+
edges = cursor.fetchall()
|
|
70
|
+
if not edges:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
# Step3: Get caller symbole details
|
|
74
|
+
caller_ids = [edge['src_symbol_id'] for edge in edges]
|
|
75
|
+
caller_placeholders = ','.join('?' * len(caller_ids))
|
|
76
|
+
cursor.execute(f"""
|
|
77
|
+
SELECT id, name, type, file_path, line_number, signature
|
|
78
|
+
FROM symbols
|
|
79
|
+
WHERE id IN ({caller_placeholders})
|
|
80
|
+
""", caller_ids)
|
|
81
|
+
|
|
82
|
+
callers = cursor.fetchall()
|
|
83
|
+
|
|
84
|
+
# Step 4: Merge caller info with call site line numbers
|
|
85
|
+
# Create a map: symbol_id -> call_site_line
|
|
86
|
+
call_sites = {edge['src_symbol_id']: edge['call_site_line'] for edge in edges}
|
|
87
|
+
|
|
88
|
+
results = []
|
|
89
|
+
for caller in callers:
|
|
90
|
+
results.append({
|
|
91
|
+
'name': caller['name'],
|
|
92
|
+
'type': caller['type'],
|
|
93
|
+
'file_path': caller['file_path'],
|
|
94
|
+
'line_number': caller['line_number'],
|
|
95
|
+
'signature': caller['signature'] or '',
|
|
96
|
+
'call_site_line': call_sites.get(caller['id'])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return results
|
|
100
|
+
|
|
101
|
+
def get_callees(self, function_name: str):
|
|
102
|
+
"""
|
|
103
|
+
Find all functions that the given function calls.
|
|
104
|
+
Answers: "What does FunctionX call?"
|
|
105
|
+
Args:
|
|
106
|
+
function_name: Name of function to find callees for
|
|
107
|
+
Returns:
|
|
108
|
+
List of callee symbols with metadata
|
|
109
|
+
"""
|
|
110
|
+
cursor = self.conn.cursor()
|
|
111
|
+
|
|
112
|
+
#Step 1: Find target function
|
|
113
|
+
cursor.execute("""
|
|
114
|
+
SELECT id FROM symbols
|
|
115
|
+
WHERE name = ? AND type = 'function'
|
|
116
|
+
""", (function_name,))
|
|
117
|
+
target_ids = [row['id'] for row in cursor.fetchall()]
|
|
118
|
+
|
|
119
|
+
if not target_ids:
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
#step 2: Find all CALL edges pointing to this functions
|
|
123
|
+
placeholders = ','.join('?' * len(target_ids))
|
|
124
|
+
cursor.execute(f"""
|
|
125
|
+
SELECT dst_symbol_id, line_number as call_site_line
|
|
126
|
+
FROM symbol_edges
|
|
127
|
+
WHERE edge_type = 'CALLS'
|
|
128
|
+
AND src_symbol_id IN ({placeholders})
|
|
129
|
+
""", target_ids)
|
|
130
|
+
|
|
131
|
+
edges = cursor.fetchall()
|
|
132
|
+
if not edges:
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
# Step 3: Get callee symbol details
|
|
136
|
+
callee_ids = [edge['dst_symbol_id'] for edge in edges]
|
|
137
|
+
callee_placeholders = ','.join('?' * len(callee_ids))
|
|
138
|
+
cursor.execute(f"""
|
|
139
|
+
SELECT id, name, type, file_path, line_number, signature
|
|
140
|
+
FROM symbols
|
|
141
|
+
WHERE id IN ({callee_placeholders})
|
|
142
|
+
""", callee_ids)
|
|
143
|
+
|
|
144
|
+
callees = cursor.fetchall()
|
|
145
|
+
|
|
146
|
+
# Step 4: Merge callee info with call site line numbers
|
|
147
|
+
call_sites = {edge['dst_symbol_id']: edge['call_site_line'] for edge in edges}
|
|
148
|
+
|
|
149
|
+
results = []
|
|
150
|
+
for callee in callees:
|
|
151
|
+
results.append({
|
|
152
|
+
'name': callee['name'],
|
|
153
|
+
'type': callee['type'],
|
|
154
|
+
'file_path': callee['file_path'],
|
|
155
|
+
'line_number': callee['line_number'],
|
|
156
|
+
'signature': callee['signature'] or '',
|
|
157
|
+
'call_site_line': call_sites.get(callee['id'])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
def get_call_chain(self, start_function: str, end_function: str, max_depth: int = 10):
|
|
163
|
+
"""
|
|
164
|
+
Find call paths from start_function to end_function.
|
|
165
|
+
Uses breadth-first search through the call graph.
|
|
166
|
+
Answers: How does functionX eventually call functionY
|
|
167
|
+
Args:
|
|
168
|
+
start_function: Starting function name
|
|
169
|
+
end_function: Target function name
|
|
170
|
+
max_depth: Maximum hops to search (default: 5)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of call paths (each path is list of function names):
|
|
174
|
+
[
|
|
175
|
+
['main', 'func1', 'func2'],
|
|
176
|
+
['main', 'func1', 'func2'],
|
|
177
|
+
...
|
|
178
|
+
]
|
|
179
|
+
"""
|
|
180
|
+
cursor = self.conn.cursor()
|
|
181
|
+
|
|
182
|
+
# Step 1: Get start and end function IDs
|
|
183
|
+
cursor.execute("""
|
|
184
|
+
SELECT id, name FROM symbols
|
|
185
|
+
WHERE name IN (?, ?) AND type = 'function'
|
|
186
|
+
""", (start_function, end_function))
|
|
187
|
+
|
|
188
|
+
symbols = {row['name']: row['id'] for row in cursor.fetchall()}
|
|
189
|
+
|
|
190
|
+
if start_function not in symbols or end_function not in symbols:
|
|
191
|
+
return [] # One or both functions don't exist
|
|
192
|
+
|
|
193
|
+
start_id = symbols[start_function]
|
|
194
|
+
end_id = symbols[end_function]
|
|
195
|
+
|
|
196
|
+
# Step 2: BFS to find all paths
|
|
197
|
+
# Queue stores: (current_symbol_id, path_so_far)
|
|
198
|
+
queue = [(start_id, [start_function])]
|
|
199
|
+
found_paths = []
|
|
200
|
+
visited = set() # Track visited nodes to prevent cycles
|
|
201
|
+
|
|
202
|
+
while queue and len(found_paths) < 10: # Limit to 10 paths
|
|
203
|
+
current_id, path = queue.pop(0)
|
|
204
|
+
|
|
205
|
+
# Skip if we've already explored this node at this depth
|
|
206
|
+
if (current_id, len(path)) in visited:
|
|
207
|
+
continue
|
|
208
|
+
visited.add((current_id, len(path)))
|
|
209
|
+
|
|
210
|
+
# Stop if max depth reached
|
|
211
|
+
if len(path) > max_depth:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
# Step 3: Get all callees from current function
|
|
215
|
+
cursor.execute("""
|
|
216
|
+
SELECT dst_symbol_id
|
|
217
|
+
FROM symbol_edges
|
|
218
|
+
WHERE edge_type = 'CALLS' AND src_symbol_id = ?
|
|
219
|
+
""", (current_id,))
|
|
220
|
+
|
|
221
|
+
callee_ids = [row['dst_symbol_id'] for row in cursor.fetchall()]
|
|
222
|
+
|
|
223
|
+
# Step 4: Check each callee
|
|
224
|
+
for callee_id in callee_ids:
|
|
225
|
+
# Found target symbol
|
|
226
|
+
if callee_id == end_id:
|
|
227
|
+
found_paths.append(path + [end_function])
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
# Get callee name and add to queue
|
|
231
|
+
cursor.execute("SELECT name FROM symbols WHERE id = ?", (callee_id,))
|
|
232
|
+
callee_row = cursor.fetchone()
|
|
233
|
+
if callee_row:
|
|
234
|
+
callee_name = callee_row['name']
|
|
235
|
+
# Avoid cycles - don't revisit functions already in this path
|
|
236
|
+
if callee_name not in path:
|
|
237
|
+
queue.append((callee_id, path + [callee_name]))
|
|
238
|
+
|
|
239
|
+
return found_paths
|
|
240
|
+
|
|
241
|
+
def execute_sql(self, query: str, params: tuple = ()):
|
|
242
|
+
"""
|
|
243
|
+
Execute a custom SQL query on the semantic graph database.
|
|
244
|
+
READ-ONLY fallback for complex queries not covered by other tools.
|
|
245
|
+
"""
|
|
246
|
+
# Security: Only allow SELECT queries (read-only)
|
|
247
|
+
query_upper = query.strip().upper()
|
|
248
|
+
if not query_upper.startswith('SELECT'):
|
|
249
|
+
raise ValueError("Only SELECT queries are allowed (read-only access)")
|
|
250
|
+
|
|
251
|
+
# Block dangerous keywords
|
|
252
|
+
dangerous_keywords = ['DROP', 'DELETE', 'INSERT', 'UPDATE', 'ALTER', 'CREATE']
|
|
253
|
+
if any(keyword in query_upper for keyword in dangerous_keywords):
|
|
254
|
+
raise ValueError(f"Query contains disallowed keyword: {query}")
|
|
255
|
+
|
|
256
|
+
cursor = self.conn.cursor()
|
|
257
|
+
cursor.execute(query, params)
|
|
258
|
+
|
|
259
|
+
# Convert rows to dictionaries
|
|
260
|
+
results = []
|
|
261
|
+
for row in cursor.fetchall():
|
|
262
|
+
results.append(dict(row))
|
|
263
|
+
|
|
264
|
+
return results
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_file_by_pattern(self, pattern: str):
|
|
268
|
+
"""
|
|
269
|
+
Find files matching a name pattern (SQL LIKE syntax).
|
|
270
|
+
Args:
|
|
271
|
+
pattern: File name pattern (e.g., 'power.c', '%dpm%', 'thermal%')
|
|
272
|
+
Returns:
|
|
273
|
+
List of matching files with metadata:
|
|
274
|
+
"""
|
|
275
|
+
cursor = self.conn.cursor()
|
|
276
|
+
|
|
277
|
+
cursor.execute("""
|
|
278
|
+
SELECT f.path, f.size, COUNT(s.id) as symbol_count
|
|
279
|
+
FROM files f
|
|
280
|
+
LEFT JOIN symbols s ON s.file_path = f.path
|
|
281
|
+
WHERE f.path LIKE ?
|
|
282
|
+
GROUP BY f.path
|
|
283
|
+
ORDER BY f.path
|
|
284
|
+
LIMIT 50
|
|
285
|
+
""", (pattern,))
|
|
286
|
+
|
|
287
|
+
results = []
|
|
288
|
+
for row in cursor.fetchall():
|
|
289
|
+
results.append({
|
|
290
|
+
'file_path': row['path'],
|
|
291
|
+
'size_bytes': row['size'],
|
|
292
|
+
'symbol_count': row['symbol_count']
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
return results
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_file_info(self, file_path: str):
|
|
299
|
+
"""
|
|
300
|
+
Get detailed information about a specific file.
|
|
301
|
+
Args:
|
|
302
|
+
file_path: Exact file path
|
|
303
|
+
Returns:
|
|
304
|
+
File metadata including symbol count
|
|
305
|
+
"""
|
|
306
|
+
cursor = self.conn.cursor()
|
|
307
|
+
|
|
308
|
+
# Get symbol count for this file
|
|
309
|
+
cursor.execute("""
|
|
310
|
+
SELECT COUNT(*) as symbol_count
|
|
311
|
+
FROM symbols
|
|
312
|
+
WHERE file_path = ?
|
|
313
|
+
""", (file_path,))
|
|
314
|
+
|
|
315
|
+
row = cursor.fetchone()
|
|
316
|
+
if not row:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
'file_path': file_path,
|
|
321
|
+
'symbol_count': row['symbol_count']
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
def list_indexed_files(self, directory_filter: str = None, file_extension: str = None):
|
|
325
|
+
"""
|
|
326
|
+
List all indexed files from database (much faster than filesystem list_directory).
|
|
327
|
+
Use this instead of list_directory to explore indexed code.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
directory_filter: Optional directory prefix (e.g., 'amdgpu', 'pm/swsmu')
|
|
331
|
+
file_extension: Optional extension filter (e.g., '.c', '.h')
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
List of indexed files with metadata (limited to 100 results):
|
|
335
|
+
[
|
|
336
|
+
{
|
|
337
|
+
'file_path': 'amdgpu/amdgpu_device.c',
|
|
338
|
+
'size_bytes': 45123,
|
|
339
|
+
'symbol_count': 87
|
|
340
|
+
},
|
|
341
|
+
...
|
|
342
|
+
]
|
|
343
|
+
"""
|
|
344
|
+
cursor = self.conn.cursor()
|
|
345
|
+
|
|
346
|
+
# Build query with optional filters
|
|
347
|
+
query = """
|
|
348
|
+
SELECT f.path, f.size, COUNT(s.id) as symbol_count
|
|
349
|
+
FROM files f
|
|
350
|
+
LEFT JOIN symbols s ON s.file_path = f.path
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
conditions = []
|
|
354
|
+
params = []
|
|
355
|
+
|
|
356
|
+
if directory_filter:
|
|
357
|
+
conditions.append("f.path LIKE ?")
|
|
358
|
+
params.append(f"{directory_filter}%")
|
|
359
|
+
|
|
360
|
+
if file_extension:
|
|
361
|
+
conditions.append("f.path LIKE ?")
|
|
362
|
+
params.append(f"%{file_extension}")
|
|
363
|
+
|
|
364
|
+
if conditions:
|
|
365
|
+
query += " WHERE " + " AND ".join(conditions)
|
|
366
|
+
|
|
367
|
+
query += """
|
|
368
|
+
GROUP BY f.path
|
|
369
|
+
ORDER BY f.path
|
|
370
|
+
LIMIT 100
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
cursor.execute(query, params)
|
|
374
|
+
|
|
375
|
+
results = []
|
|
376
|
+
for row in cursor.fetchall():
|
|
377
|
+
results.append({
|
|
378
|
+
'file_path': row['path'],
|
|
379
|
+
'size_bytes': row['size'],
|
|
380
|
+
'symbol_count': row['symbol_count']
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
'files': results,
|
|
385
|
+
'count': len(results),
|
|
386
|
+
'truncated': len(results) == 100
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def search_symbols(self, pattern: str, symbol_type: str = None):
|
|
391
|
+
"""
|
|
392
|
+
Search symbols by name pattern, optionally filtered by type.
|
|
393
|
+
Args:
|
|
394
|
+
pattern: Symbol name pattern (SQL LIKE syntax, e.g., '%DPM%', 'Init%')
|
|
395
|
+
symbol_type: Optional filter ('function', 'struct', 'macro', 'variable', etc.)
|
|
396
|
+
Returns:
|
|
397
|
+
List of matching symbols with location info:
|
|
398
|
+
"""
|
|
399
|
+
cursor = self.conn.cursor()
|
|
400
|
+
|
|
401
|
+
if symbol_type:
|
|
402
|
+
cursor.execute("""
|
|
403
|
+
SELECT name, type, file_path, line_number, signature
|
|
404
|
+
FROM symbols
|
|
405
|
+
WHERE name LIKE ? AND type = ?
|
|
406
|
+
ORDER BY name
|
|
407
|
+
LIMIT 100
|
|
408
|
+
""", (pattern, symbol_type))
|
|
409
|
+
else:
|
|
410
|
+
cursor.execute("""
|
|
411
|
+
SELECT name, type, file_path, line_number, signature
|
|
412
|
+
FROM symbols
|
|
413
|
+
WHERE name LIKE ?
|
|
414
|
+
ORDER BY type, name
|
|
415
|
+
LIMIT 100
|
|
416
|
+
""", (pattern,))
|
|
417
|
+
|
|
418
|
+
results = []
|
|
419
|
+
for row in cursor.fetchall():
|
|
420
|
+
results.append({
|
|
421
|
+
'name': row['name'],
|
|
422
|
+
'type': row['type'],
|
|
423
|
+
'file_path': row['file_path'],
|
|
424
|
+
'line_number': row['line_number'],
|
|
425
|
+
'signature': row['signature'] or ''
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
return results
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def get_symbol_definition(self, symbol_name: str, symbol_type: str = None, context_lines: int = 5):
|
|
432
|
+
"""
|
|
433
|
+
Get ONLY the definition of a symbol (function body, struct, etc.) instead of the entire file.
|
|
434
|
+
Args:
|
|
435
|
+
symbol_name: Name of symbol to find
|
|
436
|
+
symbol_type: Optional type filter ('function', 'struct', 'macro')
|
|
437
|
+
context_lines: Number of lines before/after to include (default: 5)
|
|
438
|
+
Returns:
|
|
439
|
+
Symbol definition with surrounding context:
|
|
440
|
+
"""
|
|
441
|
+
cursor = self.conn.cursor()
|
|
442
|
+
|
|
443
|
+
# Find the symbol
|
|
444
|
+
if symbol_type:
|
|
445
|
+
cursor.execute("""
|
|
446
|
+
SELECT name, type, file_path, line_number, signature
|
|
447
|
+
FROM symbols
|
|
448
|
+
WHERE name = ? AND type = ?
|
|
449
|
+
LIMIT 1
|
|
450
|
+
""", (symbol_name, symbol_type))
|
|
451
|
+
else:
|
|
452
|
+
cursor.execute("""
|
|
453
|
+
SELECT name, type, file_path, line_number, signature
|
|
454
|
+
FROM symbols
|
|
455
|
+
WHERE name = ?
|
|
456
|
+
LIMIT 1
|
|
457
|
+
""", (symbol_name,))
|
|
458
|
+
|
|
459
|
+
row = cursor.fetchone()
|
|
460
|
+
if not row:
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
# Read the file and extract the definition
|
|
464
|
+
file_path = SOURCE_ROOT / row['file_path']
|
|
465
|
+
|
|
466
|
+
if not file_path.exists():
|
|
467
|
+
return {
|
|
468
|
+
'error': f"File not found: {row['file_path']}",
|
|
469
|
+
'name': row['name'],
|
|
470
|
+
'type': row['type']
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
475
|
+
lines = f.readlines()
|
|
476
|
+
|
|
477
|
+
# Extract definition based on symbol type
|
|
478
|
+
start_line = max(0, row['line_number'] - 1 - context_lines)
|
|
479
|
+
|
|
480
|
+
# For functions/structs, try to find the closing brace
|
|
481
|
+
if row['type'] in ['function', 'struct']:
|
|
482
|
+
end_line = self._find_closing_brace(lines, row['line_number'] - 1)
|
|
483
|
+
if end_line:
|
|
484
|
+
end_line = min(len(lines), end_line + context_lines)
|
|
485
|
+
else:
|
|
486
|
+
# Fallback: just grab context lines
|
|
487
|
+
end_line = min(len(lines), row['line_number'] + context_lines)
|
|
488
|
+
else:
|
|
489
|
+
# For macros, variables, etc - just grab surrounding lines
|
|
490
|
+
end_line = min(len(lines), row['line_number'] + context_lines)
|
|
491
|
+
|
|
492
|
+
definition_lines = lines[start_line:end_line]
|
|
493
|
+
definition = ''.join(definition_lines)
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
'name': row['name'],
|
|
497
|
+
'type': row['type'],
|
|
498
|
+
'file_path': row['file_path'],
|
|
499
|
+
'line_number': row['line_number'],
|
|
500
|
+
'signature': row['signature'] or '',
|
|
501
|
+
'definition': definition,
|
|
502
|
+
'start_line': start_line + 1,
|
|
503
|
+
'end_line': end_line,
|
|
504
|
+
'lines_returned': len(definition_lines)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
except Exception as e:
|
|
508
|
+
return {
|
|
509
|
+
'error': str(e),
|
|
510
|
+
'name': row['name'],
|
|
511
|
+
'type': row['type']
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _find_closing_brace(self, lines: List[str], start_line: int) -> Optional[int]:
|
|
516
|
+
"""
|
|
517
|
+
Find the closing brace for a function/struct starting at start_line.
|
|
518
|
+
Simple brace matching
|
|
519
|
+
"""
|
|
520
|
+
brace_count = 0
|
|
521
|
+
found_opening = False
|
|
522
|
+
|
|
523
|
+
for i in range(start_line, len(lines)):
|
|
524
|
+
line = lines[i]
|
|
525
|
+
|
|
526
|
+
for char in line:
|
|
527
|
+
if char == '{':
|
|
528
|
+
brace_count += 1
|
|
529
|
+
found_opening = True
|
|
530
|
+
elif char == '}':
|
|
531
|
+
brace_count -= 1
|
|
532
|
+
if found_opening and brace_count == 0:
|
|
533
|
+
return i
|
|
534
|
+
|
|
535
|
+
return None # Couldn't find closing brace
|
|
536
|
+
|
|
537
|
+
def get_symbols_from_file(self, file_path: str, symbol_types: list = None, include_definitions: bool = False):
|
|
538
|
+
"""
|
|
539
|
+
Get ALL symbols from a specific file (or filtered by type).
|
|
540
|
+
Args:
|
|
541
|
+
file_path: File path
|
|
542
|
+
symbol_types: Optional filter list (e.g., ['struct', 'enum', 'macro'])
|
|
543
|
+
include_definitions: If True, includes code snippets for each symbol (slower, more tokens)
|
|
544
|
+
Returns:
|
|
545
|
+
List of symbols with optional definitions:
|
|
546
|
+
[
|
|
547
|
+
{
|
|
548
|
+
'name': 'DpmManager_t',
|
|
549
|
+
'type': 'struct',
|
|
550
|
+
'line_number': 42,
|
|
551
|
+
'signature': '...',
|
|
552
|
+
'definition': '...' (if include_definitions=True)
|
|
553
|
+
},
|
|
554
|
+
...
|
|
555
|
+
]
|
|
556
|
+
"""
|
|
557
|
+
cursor = self.conn.cursor()
|
|
558
|
+
|
|
559
|
+
# Build query based on filters
|
|
560
|
+
if symbol_types:
|
|
561
|
+
placeholders = ','.join('?' * len(symbol_types))
|
|
562
|
+
cursor.execute(f"""
|
|
563
|
+
SELECT name, type, file_path, line_number, signature
|
|
564
|
+
FROM symbols
|
|
565
|
+
WHERE file_path = ? AND type IN ({placeholders})
|
|
566
|
+
ORDER BY line_number
|
|
567
|
+
""", [file_path] + symbol_types)
|
|
568
|
+
else:
|
|
569
|
+
cursor.execute("""
|
|
570
|
+
SELECT name, type, file_path, line_number, signature
|
|
571
|
+
FROM symbols
|
|
572
|
+
WHERE file_path = ?
|
|
573
|
+
ORDER BY line_number
|
|
574
|
+
""", (file_path,))
|
|
575
|
+
|
|
576
|
+
results = []
|
|
577
|
+
for row in cursor.fetchall():
|
|
578
|
+
symbol_data = {
|
|
579
|
+
'name': row['name'],
|
|
580
|
+
'type': row['type'],
|
|
581
|
+
'line_number': row['line_number'],
|
|
582
|
+
'signature': row['signature'] or ''
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# Optionally include definitions
|
|
586
|
+
if include_definitions:
|
|
587
|
+
definition_result = self.get_symbol_definition(row['name'], row['type'], context_lines=3)
|
|
588
|
+
if definition_result and 'definition' in definition_result:
|
|
589
|
+
symbol_data['definition'] = definition_result['definition']
|
|
590
|
+
symbol_data['lines_returned'] = definition_result['lines_returned']
|
|
591
|
+
|
|
592
|
+
results.append(symbol_data)
|
|
593
|
+
|
|
594
|
+
return results
|
|
595
|
+
|
|
596
|
+
def close(self):
|
|
597
|
+
"""Close the database connection."""
|
|
598
|
+
if self.conn:
|
|
599
|
+
self.conn.close()
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def execute_graph_tool(tool_name: str, tool_input: Dict[str, Any], db_path: str = None):
|
|
603
|
+
"""
|
|
604
|
+
Execute a graph tool by name
|
|
605
|
+
Args:
|
|
606
|
+
tool_name: Name of the tool to execute
|
|
607
|
+
tool_input: Input parameters for the tool
|
|
608
|
+
db_path: Path to the semantic graph database (defaults to .srcodex/data/)
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
Tool execution result
|
|
612
|
+
"""
|
|
613
|
+
if db_path is None:
|
|
614
|
+
db_path = DEFAULT_DB_PATH
|
|
615
|
+
|
|
616
|
+
graph = GraphTools(db_path)
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
if tool_name == "get_callers":
|
|
620
|
+
result = graph.get_callers(tool_input.get("function_name", ""))
|
|
621
|
+
return {"callers": result, "count": len(result)}
|
|
622
|
+
|
|
623
|
+
elif tool_name == "get_callees":
|
|
624
|
+
result = graph.get_callees(tool_input.get("function_name", ""))
|
|
625
|
+
return {"callees": result, "count": len(result)}
|
|
626
|
+
|
|
627
|
+
elif tool_name == "get_call_chain":
|
|
628
|
+
result = graph.get_call_chain(
|
|
629
|
+
start_function=tool_input.get("start_function", ""),
|
|
630
|
+
end_function=tool_input.get("end_function", ""),
|
|
631
|
+
max_depth=tool_input.get("max_depth", 5)
|
|
632
|
+
)
|
|
633
|
+
return {"paths": result, "count": len(result)}
|
|
634
|
+
|
|
635
|
+
elif tool_name == "execute_sql":
|
|
636
|
+
result = graph.execute_sql(
|
|
637
|
+
query=tool_input.get("query", ""),
|
|
638
|
+
params=tuple(tool_input.get("params", []))
|
|
639
|
+
)
|
|
640
|
+
return {"results": result, "count": len(result)}
|
|
641
|
+
|
|
642
|
+
elif tool_name == "get_file_by_pattern":
|
|
643
|
+
result = graph.get_file_by_pattern(tool_input.get("pattern", ""))
|
|
644
|
+
return {"files": result, "count": len(result)}
|
|
645
|
+
|
|
646
|
+
elif tool_name == "get_file_info":
|
|
647
|
+
result = graph.get_file_info(tool_input.get("file_path", ""))
|
|
648
|
+
return result if result else {"error": "File not found"}
|
|
649
|
+
|
|
650
|
+
elif tool_name == "list_indexed_files":
|
|
651
|
+
result = graph.list_indexed_files(
|
|
652
|
+
directory_filter=tool_input.get("directory_filter"),
|
|
653
|
+
file_extension=tool_input.get("file_extension")
|
|
654
|
+
)
|
|
655
|
+
return result
|
|
656
|
+
|
|
657
|
+
elif tool_name == "search_symbols":
|
|
658
|
+
result = graph.search_symbols(
|
|
659
|
+
pattern=tool_input.get("pattern", ""),
|
|
660
|
+
symbol_type=tool_input.get("symbol_type")
|
|
661
|
+
)
|
|
662
|
+
return {"symbols": result, "count": len(result)}
|
|
663
|
+
|
|
664
|
+
elif tool_name == "get_symbol_definition":
|
|
665
|
+
result = graph.get_symbol_definition(
|
|
666
|
+
symbol_name=tool_input.get("symbol_name", ""),
|
|
667
|
+
symbol_type=tool_input.get("symbol_type"),
|
|
668
|
+
context_lines=tool_input.get("context_lines", 5)
|
|
669
|
+
)
|
|
670
|
+
return result if result else {"error": "Symbol not found"}
|
|
671
|
+
|
|
672
|
+
elif tool_name == "get_symbols_from_file":
|
|
673
|
+
result = graph.get_symbols_from_file(
|
|
674
|
+
file_path=tool_input.get("file_path", ""),
|
|
675
|
+
symbol_types=tool_input.get("symbol_types"),
|
|
676
|
+
include_definitions=tool_input.get("include_definitions", False)
|
|
677
|
+
)
|
|
678
|
+
return {"symbols": result, "count": len(result)}
|
|
679
|
+
|
|
680
|
+
else:
|
|
681
|
+
return {"error": f"Unknown graph tool: {tool_name}"}
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
return {"error": str(e)}
|
|
685
|
+
|
|
686
|
+
finally:
|
|
687
|
+
graph.close()
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
# Tool definitions for Claude API
|
|
691
|
+
TOOLS = [
|
|
692
|
+
{
|
|
693
|
+
"name": "get_callers",
|
|
694
|
+
"description": "Find all functions that call a given function. Answers 'What calls FunctionX?' Use this to understand who depends on a function or to trace backwards through the call graph.",
|
|
695
|
+
"input_schema": {
|
|
696
|
+
"type": "object",
|
|
697
|
+
"properties": {
|
|
698
|
+
"function_name": {
|
|
699
|
+
"type": "string",
|
|
700
|
+
"description": "Name of the function to find callers for (e.g., 'EnableDldo')"
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
"required": ["function_name"]
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
"name": "get_callees",
|
|
708
|
+
"description": "Find all functions that a given function calls. Answers 'What does FunctionX call?' Use this to understand what a function does or to trace forward through the call graph.",
|
|
709
|
+
"input_schema": {
|
|
710
|
+
"type": "object",
|
|
711
|
+
"properties": {
|
|
712
|
+
"function_name": {
|
|
713
|
+
"type": "string",
|
|
714
|
+
"description": "Name of the function to find callees for (e.g., 'main')"
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
"required": ["function_name"]
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
"name": "get_call_chain",
|
|
722
|
+
"description": "Find execution paths from one function to another through the call graph. Answers 'How does A reach B?' Use this to trace complex execution flows or understand how one function eventually calls another.",
|
|
723
|
+
"input_schema": {
|
|
724
|
+
"type": "object",
|
|
725
|
+
"properties": {
|
|
726
|
+
"start_function": {
|
|
727
|
+
"type": "string",
|
|
728
|
+
"description": "Starting function name (e.g., 'main')"
|
|
729
|
+
},
|
|
730
|
+
"end_function": {
|
|
731
|
+
"type": "string",
|
|
732
|
+
"description": "Target function name (e.g., 'EnableDldo')"
|
|
733
|
+
},
|
|
734
|
+
"max_depth": {
|
|
735
|
+
"type": "integer",
|
|
736
|
+
"description": "Maximum number of hops to search (default: 5)",
|
|
737
|
+
"default": 5
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
"required": ["start_function", "end_function"]
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"name": "execute_sql",
|
|
745
|
+
"description": "Execute a custom read-only SQL query on the semantic graph database. Use this for complex queries not covered by other tools (e.g., field access patterns, statistics, filtering by file/type). Only SELECT queries allowed.",
|
|
746
|
+
"input_schema": {
|
|
747
|
+
"type": "object",
|
|
748
|
+
"properties": {
|
|
749
|
+
"query": {
|
|
750
|
+
"type": "string",
|
|
751
|
+
"description": "SQL SELECT query with ? placeholders for parameters"
|
|
752
|
+
},
|
|
753
|
+
"params": {
|
|
754
|
+
"type": "array",
|
|
755
|
+
"items": {"type": "string"},
|
|
756
|
+
"description": "Query parameters (optional, for ? placeholders)",
|
|
757
|
+
"default": []
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
"required": ["query"]
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
"name": "get_file_by_pattern",
|
|
765
|
+
"description": "Find files matching a name pattern. Use this to discover files without knowing exact paths. Much faster than filesystem search.",
|
|
766
|
+
"input_schema": {
|
|
767
|
+
"type": "object",
|
|
768
|
+
"properties": {
|
|
769
|
+
"pattern": {
|
|
770
|
+
"type": "string",
|
|
771
|
+
"description": "SQL LIKE pattern (e.g., '%power%', 'dpm.c', 'thermal%')"
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
"required": ["pattern"]
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
"name": "get_file_info",
|
|
779
|
+
"description": "Get metadata about a specific file including symbol count.",
|
|
780
|
+
"input_schema": {
|
|
781
|
+
"type": "object",
|
|
782
|
+
"properties": {
|
|
783
|
+
"file_path": {
|
|
784
|
+
"type": "string",
|
|
785
|
+
"description": "Exact file path (e.g., 'mp1/src/app/power.c')"
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
"required": ["file_path"]
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
"name": "list_indexed_files",
|
|
793
|
+
"description": "List all indexed files from the database (MUCH faster and cheaper than list_directory). Use this instead of list_directory when exploring indexed code. Returns file paths with symbol counts. Limited to 100 results.",
|
|
794
|
+
"input_schema": {
|
|
795
|
+
"type": "object",
|
|
796
|
+
"properties": {
|
|
797
|
+
"directory_filter": {
|
|
798
|
+
"type": "string",
|
|
799
|
+
"description": "Optional directory prefix filter (e.g., 'amdgpu', 'pm/swsmu', 'drivers/gpu')"
|
|
800
|
+
},
|
|
801
|
+
"file_extension": {
|
|
802
|
+
"type": "string",
|
|
803
|
+
"description": "Optional file extension filter (e.g., '.c', '.h')"
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
"required": []
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
"name": "search_symbols",
|
|
811
|
+
"description": "Search for symbols (functions, structs, macros) by name pattern. Faster than execute_sql for common symbol searches.",
|
|
812
|
+
"input_schema": {
|
|
813
|
+
"type": "object",
|
|
814
|
+
"properties": {
|
|
815
|
+
"pattern": {
|
|
816
|
+
"type": "string",
|
|
817
|
+
"description": "SQL LIKE pattern (e.g., '%DPM%', 'Init%', '%handler')"
|
|
818
|
+
},
|
|
819
|
+
"symbol_type": {
|
|
820
|
+
"type": "string",
|
|
821
|
+
"description": "Optional filter: 'function', 'struct', 'macro', 'variable', 'enum', 'typedef'"
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
"required": ["pattern"]
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
"name": "get_symbol_definition",
|
|
829
|
+
"description": "Get ONLY the definition of a symbol (function body, struct definition, etc.) instead of reading the entire file. MASSIVE token savings - use this instead of read_file when you only need one symbol.",
|
|
830
|
+
"input_schema": {
|
|
831
|
+
"type": "object",
|
|
832
|
+
"properties": {
|
|
833
|
+
"symbol_name": {
|
|
834
|
+
"type": "string",
|
|
835
|
+
"description": "Name of symbol to get definition for (e.g., 'InitPower', 'DpmManager_t')"
|
|
836
|
+
},
|
|
837
|
+
"symbol_type": {
|
|
838
|
+
"type": "string",
|
|
839
|
+
"description": "Optional type filter: 'function', 'struct', 'macro'"
|
|
840
|
+
},
|
|
841
|
+
"context_lines": {
|
|
842
|
+
"type": "integer",
|
|
843
|
+
"description": "Number of context lines before/after (default: 5, max recommended: 10). Large values (>20) defeat the purpose of symbol-based queries - use read_file instead if you need that much context.",
|
|
844
|
+
"default": 5
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
"required": ["symbol_name"]
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
"name": "get_symbols_from_file",
|
|
852
|
+
"description": "Get ALL symbols from a file (or filtered by type). Use this instead of read_file when you need multiple symbols from one file. IMPORTANT: Use two-step approach for best token efficiency: (1) Get metadata first (include_definitions=false) to see what symbols exist, (2) Then use get_symbol_definition() for only the specific symbols you need. Only set include_definitions=true if you genuinely need ALL symbol definitions from the file.",
|
|
853
|
+
"input_schema": {
|
|
854
|
+
"type": "object",
|
|
855
|
+
"properties": {
|
|
856
|
+
"file_path": {
|
|
857
|
+
"type": "string",
|
|
858
|
+
"description": "File path (e.g., 'mp1/src/app/dpm.h')"
|
|
859
|
+
},
|
|
860
|
+
"symbol_types": {
|
|
861
|
+
"type": "array",
|
|
862
|
+
"items": {"type": "string"},
|
|
863
|
+
"description": "Optional filter: ['struct', 'enum', 'macro', 'function'] - leave empty for all symbols"
|
|
864
|
+
},
|
|
865
|
+
"include_definitions": {
|
|
866
|
+
"type": "boolean",
|
|
867
|
+
"description": "WARNING: Setting this to true returns code for EVERY symbol, which can be 5-10x more tokens than the file itself. Default: false (metadata only). Only use true when you genuinely need all definitions. For selective definitions, use get_symbol_definition() instead.",
|
|
868
|
+
"default": False
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
"required": ["file_path"]
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
]
|