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,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
+ ]