codegraph-py 1.0.1__tar.gz → 1.1.1__tar.gz
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.
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/PKG-INFO +1 -1
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/__init__.py +1 -1
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/cli.py +148 -53
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/codegraph.py +75 -7
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/db/queries.py +30 -3
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/graph/__init__.py +24 -16
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/types.py +1 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph_py.egg-info/PKG-INFO +1 -1
- codegraph_py-1.1.1/codegraph_py.egg-info/top_level.txt +1 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/pyproject.toml +1 -1
- codegraph_py-1.0.1/codegraph_py.egg-info/top_level.txt +0 -2
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/README.md +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/__main__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/context/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/db/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/db/connection.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/db/schema.sql +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/directory.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/errors.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/base.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/c_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/cpp_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/dart_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/go_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/java_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/javascript_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/kotlin_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/lua_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/python_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/ruby_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/rust_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/scala_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/swift_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/languages/typescript_cfg.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/extraction/tree_sitter_extractor.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/installer/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/mcp/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/project_config.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/resolution/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/search/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/sync/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/ui/__init__.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph/utils.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph_py.egg-info/SOURCES.txt +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph_py.egg-info/dependency_links.txt +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph_py.egg-info/entry_points.txt +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/codegraph_py.egg-info/requires.txt +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/setup.cfg +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/tests/test_codegraph.py +0 -0
- {codegraph_py-1.0.1 → codegraph_py-1.1.1}/tests/test_stress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local.
|
|
5
5
|
Author-email: mading <martin98@bu.edu>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,12 +14,13 @@ from typing import Optional, List, Dict
|
|
|
14
14
|
|
|
15
15
|
import click
|
|
16
16
|
|
|
17
|
+
from codegraph import __version__
|
|
17
18
|
from codegraph.codegraph import CodeGraph
|
|
18
19
|
from codegraph.directory import (
|
|
19
20
|
is_initialized, find_nearest_codegraph_root,
|
|
20
21
|
unsafe_index_root_reason, derive_project_name_tokens,
|
|
21
22
|
)
|
|
22
|
-
from codegraph.types import SearchOptions, GraphStats
|
|
23
|
+
from codegraph.types import SearchOptions, SearchResult, GraphStats
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
# =============================================================================
|
|
@@ -68,6 +69,15 @@ def format_number(n: int) -> str:
|
|
|
68
69
|
return f'{n:,}'
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def _pluralize(word: str) -> str:
|
|
73
|
+
"""Simple English pluralization."""
|
|
74
|
+
if word.endswith(('s', 'x', 'ch', 'sh')):
|
|
75
|
+
return word + 'es'
|
|
76
|
+
if word.endswith('y') and len(word) > 2 and word[-2] not in 'aeiou':
|
|
77
|
+
return word[:-1] + 'ies'
|
|
78
|
+
return word + 's'
|
|
79
|
+
|
|
80
|
+
|
|
71
81
|
def format_duration(ms: float) -> str:
|
|
72
82
|
"""Format duration in milliseconds to human readable."""
|
|
73
83
|
if ms < 1000:
|
|
@@ -106,13 +116,24 @@ def resolve_project_path(path_arg: Optional[str] = None) -> str:
|
|
|
106
116
|
# CLI Commands
|
|
107
117
|
# =============================================================================
|
|
108
118
|
|
|
109
|
-
@click.group()
|
|
110
|
-
@click.version_option(version=
|
|
119
|
+
@click.group(context_settings=dict(help_option_names=['-h', '--help']))
|
|
120
|
+
@click.version_option(version=__version__, prog_name='codegraph')
|
|
111
121
|
def main():
|
|
112
|
-
"""CodeGraph
|
|
113
|
-
|
|
114
|
-
Build a knowledge graph of your codebase for
|
|
115
|
-
|
|
122
|
+
"""CodeGraph — Semantic code intelligence for AI coding agents.
|
|
123
|
+
|
|
124
|
+
Build a knowledge graph of your codebase for instant symbol lookup,
|
|
125
|
+
call-chain analysis, and surgical context extraction.
|
|
126
|
+
|
|
127
|
+
\b
|
|
128
|
+
Common workflows:
|
|
129
|
+
codegraph init Initialize and index current project
|
|
130
|
+
codegraph status View index statistics
|
|
131
|
+
codegraph query "class" Search for symbols matching "class"
|
|
132
|
+
codegraph query --kind class List all classes in the project
|
|
133
|
+
codegraph explore "main" Show source code and call paths for "main"
|
|
134
|
+
codegraph callers "func" Show what calls the function "func"
|
|
135
|
+
codegraph callees "func" Show what "func" calls
|
|
136
|
+
codegraph files List all indexed files
|
|
116
137
|
"""
|
|
117
138
|
pass
|
|
118
139
|
|
|
@@ -308,7 +329,13 @@ def sync(path: Optional[str], quiet: bool):
|
|
|
308
329
|
@click.argument('path', required=False, default=None)
|
|
309
330
|
@click.option('-j', '--json', 'json_output', is_flag=True, help='Output as JSON')
|
|
310
331
|
def status(path: Optional[str], json_output: bool):
|
|
311
|
-
"""Show index status and
|
|
332
|
+
"""Show index status, statistics, and pending changes.
|
|
333
|
+
|
|
334
|
+
Displays file count, node/edge counts, node breakdown by kind,
|
|
335
|
+
and any pending changes since the last index.
|
|
336
|
+
|
|
337
|
+
Use --json/-j for machine-readable output.
|
|
338
|
+
"""
|
|
312
339
|
project_path = resolve_project_path(path)
|
|
313
340
|
|
|
314
341
|
if not is_initialized(project_path):
|
|
@@ -403,14 +430,21 @@ def status(path: Optional[str], json_output: bool):
|
|
|
403
430
|
|
|
404
431
|
|
|
405
432
|
@main.command()
|
|
406
|
-
@click.argument('search')
|
|
433
|
+
@click.argument('search', required=False, default='')
|
|
407
434
|
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
408
|
-
@click.option('-l', '--limit', default=
|
|
435
|
+
@click.option('-l', '--limit', default=20, type=int, help='Maximum results')
|
|
409
436
|
@click.option('-k', '--kind', help='Filter by node kind (function, class, etc.)')
|
|
437
|
+
@click.option('--exact', is_flag=True, help='Exact name match (instead of fuzzy/prefix)')
|
|
438
|
+
@click.option('--fuzzy', is_flag=True, help='Force fuzzy name matching (default)')
|
|
410
439
|
@click.option('-j', '--json', 'json_output', is_flag=True, help='Output as JSON')
|
|
411
440
|
def query(search: str, path_arg: Optional[str], limit: int,
|
|
412
|
-
kind: Optional[str],
|
|
413
|
-
|
|
441
|
+
kind: Optional[str], exact: bool, fuzzy: bool,
|
|
442
|
+
json_output: bool):
|
|
443
|
+
"""Search for symbols in the codebase.
|
|
444
|
+
|
|
445
|
+
If SEARCH is omitted and --kind is provided, lists all symbols of that kind.
|
|
446
|
+
Use --exact for precise symbol name lookup.
|
|
447
|
+
"""
|
|
414
448
|
project_path = resolve_project_path(path_arg)
|
|
415
449
|
|
|
416
450
|
if not is_initialized(project_path):
|
|
@@ -424,7 +458,15 @@ def query(search: str, path_arg: Optional[str], limit: int,
|
|
|
424
458
|
if kind:
|
|
425
459
|
opts.kinds = [kind]
|
|
426
460
|
|
|
427
|
-
|
|
461
|
+
if exact:
|
|
462
|
+
opts.exact_match = True
|
|
463
|
+
|
|
464
|
+
if not search.strip() and kind:
|
|
465
|
+
# No search term — list by kind only
|
|
466
|
+
nodes = cg._queries.get_nodes_by_kind(kind) if kind else []
|
|
467
|
+
results = [SearchResult(node=n, score=1.0) for n in nodes]
|
|
468
|
+
else:
|
|
469
|
+
results = cg.search_nodes(search, opts)
|
|
428
470
|
|
|
429
471
|
if json_output:
|
|
430
472
|
output = []
|
|
@@ -447,9 +489,15 @@ def query(search: str, path_arg: Optional[str], limit: int,
|
|
|
447
489
|
return
|
|
448
490
|
|
|
449
491
|
if not results:
|
|
450
|
-
|
|
492
|
+
if search:
|
|
493
|
+
click.echo(f'No results found for "{search}"')
|
|
494
|
+
elif kind:
|
|
495
|
+
click.echo(f'No symbols of kind "{kind}" found')
|
|
496
|
+
else:
|
|
497
|
+
click.echo('No search term provided. Use --kind to browse by type.')
|
|
451
498
|
else:
|
|
452
|
-
|
|
499
|
+
title = f'\nSearch Results for "{search}":' if search else f'\nAll {_pluralize(kind)}:' if kind else '\nResults:'
|
|
500
|
+
click.echo(bold(title + '\n'))
|
|
453
501
|
for r in results:
|
|
454
502
|
node = r.node
|
|
455
503
|
location = f'{node.file_path}:{node.start_line}'
|
|
@@ -473,6 +521,14 @@ def query(search: str, path_arg: Optional[str], limit: int,
|
|
|
473
521
|
def explore(query_parts: List[str], path_arg: Optional[str], max_files: int):
|
|
474
522
|
"""Explore code: relevant symbols' source + call paths in one shot.
|
|
475
523
|
|
|
524
|
+
QUERY_PARTS is one or more search terms for symbol lookup.
|
|
525
|
+
Combines search, callers/callees, and source display in a single view.
|
|
526
|
+
|
|
527
|
+
\b
|
|
528
|
+
Examples:
|
|
529
|
+
codegraph explore main Find "main" and show source + call context
|
|
530
|
+
codegraph explore ChatBackend Explore ChatBackend class and its relationships
|
|
531
|
+
|
|
476
532
|
Same output as the codegraph_explore MCP tool.
|
|
477
533
|
"""
|
|
478
534
|
query = ' '.join(query_parts)
|
|
@@ -524,9 +580,14 @@ def explore(query_parts: List[str], path_arg: Optional[str], max_files: int):
|
|
|
524
580
|
@main.command()
|
|
525
581
|
@click.argument('symbol')
|
|
526
582
|
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
527
|
-
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth')
|
|
583
|
+
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth (1=direct, 2=indirect)')
|
|
528
584
|
def callers(symbol: str, path_arg: Optional[str], depth: int):
|
|
529
|
-
"""Find what calls a function/method.
|
|
585
|
+
"""Find what calls a function/method.
|
|
586
|
+
|
|
587
|
+
SYMBOL is the symbol name to find callers for (e.g. "ChatBackend", "main").
|
|
588
|
+
|
|
589
|
+
Use -d 2 to include indirect callers (callers of callers).
|
|
590
|
+
"""
|
|
530
591
|
project_path = resolve_project_path(path_arg)
|
|
531
592
|
|
|
532
593
|
if not is_initialized(project_path):
|
|
@@ -569,9 +630,14 @@ def callers(symbol: str, path_arg: Optional[str], depth: int):
|
|
|
569
630
|
@main.command()
|
|
570
631
|
@click.argument('symbol')
|
|
571
632
|
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
572
|
-
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth')
|
|
633
|
+
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth (1=direct, 2=indirect)')
|
|
573
634
|
def callees(symbol: str, path_arg: Optional[str], depth: int):
|
|
574
|
-
"""Find what a function/method calls.
|
|
635
|
+
"""Find what a function/method calls.
|
|
636
|
+
|
|
637
|
+
SYMBOL is the symbol name to find callees for (e.g. "run", "process").
|
|
638
|
+
|
|
639
|
+
Use -d 2 to include indirect callees (callees of callees).
|
|
640
|
+
"""
|
|
575
641
|
project_path = resolve_project_path(path_arg)
|
|
576
642
|
|
|
577
643
|
if not is_initialized(project_path):
|
|
@@ -698,7 +764,7 @@ def affected(files: List[str], path_arg: Optional[str]):
|
|
|
698
764
|
nodes = cg.get_nodes_by_file(filepath)
|
|
699
765
|
for node in nodes:
|
|
700
766
|
# Find what depends on these nodes
|
|
701
|
-
dependents = cg.get_impact_radius(node.id,
|
|
767
|
+
dependents = cg.get_impact_radius(node.id, max_depth=2)
|
|
702
768
|
for n in dependents.nodes.values():
|
|
703
769
|
if n.file_path.endswith('_test.py') or n.file_path.endswith('test_'):
|
|
704
770
|
affected_files.add(n.file_path)
|
|
@@ -730,12 +796,14 @@ def node(name: str, path_arg: Optional[str]):
|
|
|
730
796
|
return
|
|
731
797
|
|
|
732
798
|
try:
|
|
733
|
-
cg = CodeGraph(project_path)
|
|
799
|
+
cg = CodeGraph.open_sync(project_path)
|
|
734
800
|
|
|
735
801
|
# Search for nodes by name
|
|
736
|
-
|
|
802
|
+
opts = SearchOptions(limit=20)
|
|
803
|
+
results = cg.search_nodes(name, opts)
|
|
737
804
|
if not results:
|
|
738
805
|
click.echo(yellow(f'No nodes found matching "{name}"'))
|
|
806
|
+
cg.destroy()
|
|
739
807
|
return
|
|
740
808
|
|
|
741
809
|
# If exact match, show details
|
|
@@ -758,18 +826,18 @@ def node(name: str, path_arg: Optional[str]):
|
|
|
758
826
|
click.echo(f' {dim("Exported:")} {green("yes") if target.is_exported else "no"}')
|
|
759
827
|
|
|
760
828
|
# Show callers
|
|
761
|
-
|
|
762
|
-
if
|
|
763
|
-
click.echo(bold(f'\n Callers ({len(
|
|
764
|
-
for
|
|
765
|
-
click.echo(f' ← {
|
|
829
|
+
caller_pairs = cg.get_callers(target.id)
|
|
830
|
+
if caller_pairs:
|
|
831
|
+
click.echo(bold(f'\n Callers ({len(caller_pairs)}):'))
|
|
832
|
+
for c_node, c_edge in caller_pairs:
|
|
833
|
+
click.echo(f' ← {c_node.name} ({c_node.file_path}:{c_node.start_line})')
|
|
766
834
|
|
|
767
835
|
# Show callees
|
|
768
|
-
|
|
769
|
-
if
|
|
770
|
-
click.echo(bold(f'\n Callees ({len(
|
|
771
|
-
for
|
|
772
|
-
click.echo(f' → {
|
|
836
|
+
callee_pairs = cg.get_callees(target.id)
|
|
837
|
+
if callee_pairs:
|
|
838
|
+
click.echo(bold(f'\n Callees ({len(callee_pairs)}):'))
|
|
839
|
+
for c_node, c_edge in callee_pairs:
|
|
840
|
+
click.echo(f' → {c_node.name} ({c_node.file_path}:{c_node.start_line})')
|
|
773
841
|
|
|
774
842
|
cg.destroy()
|
|
775
843
|
|
|
@@ -781,7 +849,9 @@ def node(name: str, path_arg: Optional[str]):
|
|
|
781
849
|
@main.command()
|
|
782
850
|
@click.option('--path', '-p', 'path_arg', default=None, help='Project path')
|
|
783
851
|
@click.option('--json-output', 'json_output', is_flag=True, help='Output as JSON')
|
|
784
|
-
|
|
852
|
+
@click.option('--by-directory', 'by_directory', is_flag=True, help='Group files by directory')
|
|
853
|
+
@click.option('--stats', 'show_stats', is_flag=True, help='Show per-file symbol stats (default when no flags)')
|
|
854
|
+
def files(path_arg: Optional[str], json_output: bool, by_directory: bool, show_stats: bool):
|
|
785
855
|
"""List indexed files with language and symbol statistics."""
|
|
786
856
|
project_path = resolve_project_path(path_arg)
|
|
787
857
|
|
|
@@ -791,44 +861,69 @@ def files(path_arg: Optional[str], json_output: bool):
|
|
|
791
861
|
return
|
|
792
862
|
|
|
793
863
|
try:
|
|
794
|
-
cg = CodeGraph(project_path)
|
|
864
|
+
cg = CodeGraph.open_sync(project_path)
|
|
795
865
|
stats = cg.get_stats()
|
|
796
|
-
all_files = cg.
|
|
866
|
+
all_files = cg._queries.get_all_file_paths()
|
|
797
867
|
|
|
798
868
|
if json_output:
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
'total_edges': stats.edge_count,
|
|
803
|
-
'files': [],
|
|
804
|
-
}
|
|
805
|
-
for f in all_files:
|
|
806
|
-
nodes = cg.db.get_nodes_by_file(f)
|
|
869
|
+
file_list = []
|
|
870
|
+
for f in sorted(all_files):
|
|
871
|
+
nodes = cg.get_nodes_by_file(f)
|
|
807
872
|
langs = set(n.language for n in nodes if n.language)
|
|
808
|
-
|
|
873
|
+
kinds = {}
|
|
874
|
+
for n in nodes:
|
|
875
|
+
kinds[n.kind] = kinds.get(n.kind, 0) + 1
|
|
876
|
+
file_list.append({
|
|
809
877
|
'path': f,
|
|
810
878
|
'node_count': len(nodes),
|
|
811
879
|
'languages': list(langs),
|
|
880
|
+
'node_kinds': kinds,
|
|
812
881
|
})
|
|
882
|
+
result = {
|
|
883
|
+
'total_files': len(all_files),
|
|
884
|
+
'total_nodes': stats.node_count,
|
|
885
|
+
'total_edges': stats.edge_count,
|
|
886
|
+
'files': file_list,
|
|
887
|
+
}
|
|
813
888
|
click.echo(json.dumps(result, indent=2))
|
|
889
|
+
cg.destroy()
|
|
890
|
+
return
|
|
891
|
+
|
|
892
|
+
show_stats = show_stats or not by_directory
|
|
893
|
+
|
|
894
|
+
if by_directory:
|
|
895
|
+
# Group files by directory
|
|
896
|
+
dirs: Dict[str, List[str]] = {}
|
|
897
|
+
for f in sorted(all_files):
|
|
898
|
+
d = os.path.dirname(f) or '.'
|
|
899
|
+
dirs.setdefault(d, []).append(f)
|
|
900
|
+
click.echo(bold(f'\n📁 Indexed Files ({len(all_files)} total, {len(dirs)} directories)\n'))
|
|
901
|
+
for directory in sorted(dirs):
|
|
902
|
+
click.echo(blue(f' {directory}/'))
|
|
903
|
+
for f in dirs[directory]:
|
|
904
|
+
click.echo(f' {os.path.basename(f)}')
|
|
905
|
+
click.echo()
|
|
814
906
|
else:
|
|
815
907
|
click.echo(bold(f'\n📁 Indexed Files ({len(all_files)} total)\n'))
|
|
816
908
|
for f in sorted(all_files):
|
|
817
|
-
nodes = cg.
|
|
909
|
+
nodes = cg.get_nodes_by_file(f)
|
|
818
910
|
lang = ''
|
|
819
911
|
for n in nodes:
|
|
820
912
|
if n.language and n.language != 'unknown':
|
|
821
913
|
lang = n.language
|
|
822
914
|
break
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
915
|
+
if show_stats:
|
|
916
|
+
kinds = {}
|
|
917
|
+
for n in nodes:
|
|
918
|
+
k = n.kind
|
|
919
|
+
kinds[k] = kinds.get(k, 0) + 1
|
|
920
|
+
kind_str = ', '.join(f'{v} {k}' for k, v in sorted(kinds.items()) if k != 'file')
|
|
921
|
+
if kind_str:
|
|
922
|
+
click.echo(f' {dim(lang+" ") if lang else ""}{f} {dim("("+kind_str+")")}')
|
|
923
|
+
else:
|
|
924
|
+
click.echo(f' {f}')
|
|
830
925
|
else:
|
|
831
|
-
click.echo(f' {f}')
|
|
926
|
+
click.echo(f' {dim(lang+" ") if lang else ""}{f}')
|
|
832
927
|
|
|
833
928
|
cg.destroy()
|
|
834
929
|
|
|
@@ -31,7 +31,7 @@ from codegraph.graph import GraphTraverser, GraphQueryManager
|
|
|
31
31
|
from codegraph.context import ContextBuilder, create_context_builder
|
|
32
32
|
from codegraph.sync import FileWatcher, WatchOptions, PendingFile
|
|
33
33
|
|
|
34
|
-
__version__ = "1.
|
|
34
|
+
__version__ = "1.1.1"
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@dataclass
|
|
@@ -357,25 +357,93 @@ class CodeGraph:
|
|
|
357
357
|
return self._queries.get_nodes_by_file(file_path)
|
|
358
358
|
|
|
359
359
|
def get_callers(self, node_id: str, max_depth: int = 1) -> List[Tuple[Node, Edge]]:
|
|
360
|
-
"""Find what calls a function/method.
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
"""Find what calls a function/method.
|
|
361
|
+
|
|
362
|
+
First tries edge-based lookup (resolved references), then falls back
|
|
363
|
+
to name-based matching on unresolved references for maximum coverage.
|
|
364
|
+
"""
|
|
365
|
+
result: List[Tuple[Node, Edge]] = []
|
|
366
|
+
|
|
367
|
+
# ── Phase 1: edge-based (resolved references) ──
|
|
368
|
+
nodes = self._traverser.getCallers(node_id, max_depth)
|
|
363
369
|
for n in nodes:
|
|
364
370
|
edges = self._queries.get_outgoing_edges(n.id, ['calls', 'references'])
|
|
365
371
|
for e in edges:
|
|
366
372
|
if e.target == node_id:
|
|
367
373
|
result.append((n, e))
|
|
374
|
+
|
|
375
|
+
if result:
|
|
376
|
+
return result
|
|
377
|
+
|
|
378
|
+
# ── Phase 2: fallback via UnresolvedReference name matching ──
|
|
379
|
+
target = self._queries.get_node_by_id(node_id)
|
|
380
|
+
if target:
|
|
381
|
+
target_name = target.name
|
|
382
|
+
# Collect all refs whose name matches the target name (or any dotted suffix)
|
|
383
|
+
refs = self._queries.get_unresolved_refs()
|
|
384
|
+
seen: Set[str] = set()
|
|
385
|
+
for ref in refs:
|
|
386
|
+
ref_name = ref.reference_name
|
|
387
|
+
# Exact match, or last component of dotted name matches
|
|
388
|
+
if ref_name == target_name or (
|
|
389
|
+
'.' in ref_name and ref_name.rsplit('.', 1)[-1] == target_name
|
|
390
|
+
):
|
|
391
|
+
caller = self._queries.get_node_by_id(ref.from_node_id)
|
|
392
|
+
if caller and caller.id not in seen:
|
|
393
|
+
seen.add(caller.id)
|
|
394
|
+
fake_edge = Edge(
|
|
395
|
+
source=caller.id, target=node_id,
|
|
396
|
+
kind=ref.reference_kind or 'references',
|
|
397
|
+
line=ref.line, column=ref.column,
|
|
398
|
+
provenance='unresolved',
|
|
399
|
+
)
|
|
400
|
+
result.append((caller, fake_edge))
|
|
401
|
+
|
|
368
402
|
return result
|
|
369
403
|
|
|
370
404
|
def get_callees(self, node_id: str, max_depth: int = 1) -> List[Tuple[Node, Edge]]:
|
|
371
|
-
"""Find what a function/method calls.
|
|
372
|
-
|
|
373
|
-
|
|
405
|
+
"""Find what a function/method calls.
|
|
406
|
+
|
|
407
|
+
First tries edge-based lookup (resolved references), then falls back
|
|
408
|
+
to name-based matching on unresolved references for maximum coverage.
|
|
409
|
+
"""
|
|
410
|
+
result: List[Tuple[Node, Edge]] = []
|
|
411
|
+
|
|
412
|
+
# ── Phase 1: edge-based (resolved references) ──
|
|
413
|
+
nodes = self._traverser.getCallees(node_id, max_depth)
|
|
374
414
|
for n in nodes:
|
|
375
415
|
edges = self._queries.get_incoming_edges(n.id, ['calls', 'references'])
|
|
376
416
|
for e in edges:
|
|
377
417
|
if e.source == node_id:
|
|
378
418
|
result.append((n, e))
|
|
419
|
+
|
|
420
|
+
if result:
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
# ── Phase 2: fallback via UnresolvedReference name matching ──
|
|
424
|
+
source = self._queries.get_node_by_id(node_id)
|
|
425
|
+
if source:
|
|
426
|
+
refs = self._queries.get_unresolved_refs_by_from_node(node_id)
|
|
427
|
+
seen: Set[str] = set()
|
|
428
|
+
for ref in refs:
|
|
429
|
+
# Try to resolve the referenced name to a real node
|
|
430
|
+
callees_found = self._queries.get_nodes_by_name(ref.reference_name)
|
|
431
|
+
# Also try last component of dotted names
|
|
432
|
+
if not callees_found and '.' in ref.reference_name:
|
|
433
|
+
simple = ref.reference_name.rsplit('.', 1)[-1]
|
|
434
|
+
callees_found = self._queries.get_nodes_by_name(simple)
|
|
435
|
+
|
|
436
|
+
for callee in callees_found:
|
|
437
|
+
if callee.id not in seen:
|
|
438
|
+
seen.add(callee.id)
|
|
439
|
+
fake_edge = Edge(
|
|
440
|
+
source=node_id, target=callee.id,
|
|
441
|
+
kind=ref.reference_kind or 'references',
|
|
442
|
+
line=ref.line, column=ref.column,
|
|
443
|
+
provenance='unresolved',
|
|
444
|
+
)
|
|
445
|
+
result.append((callee, fake_edge))
|
|
446
|
+
|
|
379
447
|
return result
|
|
380
448
|
|
|
381
449
|
def get_impact_radius(self, node_id: str, max_depth: int = 3) -> Subgraph:
|
|
@@ -443,6 +443,13 @@ class QueryBuilder:
|
|
|
443
443
|
)
|
|
444
444
|
return [self._row_to_unresolved(row) for row in cur.fetchall()]
|
|
445
445
|
|
|
446
|
+
def get_unresolved_refs_by_from_node(self, from_node_id: str) -> List[UnresolvedReference]:
|
|
447
|
+
"""Get unresolved references by originating node ID."""
|
|
448
|
+
cur = self._db.execute(
|
|
449
|
+
'SELECT * FROM unresolved_refs WHERE from_node_id = ?', (from_node_id,)
|
|
450
|
+
)
|
|
451
|
+
return [self._row_to_unresolved(row) for row in cur.fetchall()]
|
|
452
|
+
|
|
446
453
|
def clear_unresolved_refs(self) -> None:
|
|
447
454
|
"""Clear all unresolved references."""
|
|
448
455
|
self._db.execute('DELETE FROM unresolved_refs')
|
|
@@ -453,12 +460,32 @@ class QueryBuilder:
|
|
|
453
460
|
|
|
454
461
|
def search_nodes(self, query: str,
|
|
455
462
|
options: Optional[SearchOptions] = None) -> List[SearchResult]:
|
|
456
|
-
"""Search nodes
|
|
463
|
+
"""Search nodes in the codebase.
|
|
464
|
+
|
|
465
|
+
Supports three modes:
|
|
466
|
+
- Exact match (opts.exact_match=True): precise name lookup via SQL
|
|
467
|
+
- FTS5 full-text: prefix-based ranking search
|
|
468
|
+
- LIKE fallback: substring matching when FTS returns nothing
|
|
469
|
+
"""
|
|
457
470
|
opts = options or SearchOptions()
|
|
458
471
|
results: List[SearchResult] = []
|
|
459
472
|
|
|
473
|
+
# ── Exact match mode ──
|
|
474
|
+
if opts.exact_match and query.strip():
|
|
475
|
+
cur = self._db.execute(
|
|
476
|
+
'''SELECT * FROM nodes WHERE name = ?
|
|
477
|
+
ORDER BY file_path, start_line
|
|
478
|
+
LIMIT ?''',
|
|
479
|
+
(query.strip(), opts.limit)
|
|
480
|
+
)
|
|
481
|
+
for row in cur.fetchall():
|
|
482
|
+
node = self._row_to_node(row)
|
|
483
|
+
if self._matches_filters(node, opts):
|
|
484
|
+
results.append(SearchResult(node=node, score=1.0))
|
|
485
|
+
return results
|
|
486
|
+
|
|
487
|
+
# ── FTS5 full-text search ──
|
|
460
488
|
try:
|
|
461
|
-
# FTS5 query with prefix matching
|
|
462
489
|
fts_query = ' OR '.join(
|
|
463
490
|
f'"{word}"*' if len(word) > 1 else word
|
|
464
491
|
for word in query.split()
|
|
@@ -484,7 +511,7 @@ class QueryBuilder:
|
|
|
484
511
|
except Exception:
|
|
485
512
|
pass
|
|
486
513
|
|
|
487
|
-
#
|
|
514
|
+
# ── LIKE fallback ──
|
|
488
515
|
if not results:
|
|
489
516
|
for word in query.split():
|
|
490
517
|
if not word.strip():
|
|
@@ -174,62 +174,70 @@ class GraphTraverser:
|
|
|
174
174
|
dfs(start_id, 0)
|
|
175
175
|
return result
|
|
176
176
|
|
|
177
|
-
def getCallers(self, node_id: str,
|
|
177
|
+
def getCallers(self, node_id: str, max_depth: int = 1) -> List[Node]:
|
|
178
178
|
"""Get nodes that call/ reference the given node.
|
|
179
|
-
|
|
179
|
+
|
|
180
180
|
Args:
|
|
181
181
|
node_id: Target node ID
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
max_depth: How many levels up to traverse.
|
|
183
|
+
1 = direct callers only,
|
|
184
|
+
2 = callers + their callers, etc.
|
|
185
|
+
|
|
184
186
|
Returns:
|
|
185
187
|
List of caller nodes
|
|
186
188
|
"""
|
|
187
189
|
callers: List[Node] = []
|
|
188
190
|
seen: Set[str] = set()
|
|
189
191
|
|
|
190
|
-
def collect_callers(nid: str) -> None:
|
|
192
|
+
def collect_callers(nid: str, depth: int) -> None:
|
|
191
193
|
if nid in seen:
|
|
192
194
|
return
|
|
193
195
|
seen.add(nid)
|
|
194
196
|
|
|
197
|
+
if depth >= max_depth:
|
|
198
|
+
return
|
|
199
|
+
|
|
195
200
|
edges = self._qb.get_incoming_edges(nid, [EdgeKind.CALLS, EdgeKind.REFERENCES])
|
|
196
201
|
for edge in edges:
|
|
197
202
|
caller_node = self._qb.get_node_by_id(edge.source)
|
|
198
203
|
if caller_node and edge.source not in seen:
|
|
199
204
|
callers.append(caller_node)
|
|
200
|
-
|
|
201
|
-
collect_callers(edge.source)
|
|
205
|
+
collect_callers(edge.source, depth + 1)
|
|
202
206
|
|
|
203
|
-
collect_callers(node_id)
|
|
207
|
+
collect_callers(node_id, 0)
|
|
204
208
|
return callers
|
|
205
209
|
|
|
206
|
-
def getCallees(self, node_id: str,
|
|
210
|
+
def getCallees(self, node_id: str, max_depth: int = 1) -> List[Node]:
|
|
207
211
|
"""Get nodes that the given node calls/ references.
|
|
208
|
-
|
|
212
|
+
|
|
209
213
|
Args:
|
|
210
214
|
node_id: Source node ID
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
max_depth: How many levels down to traverse.
|
|
216
|
+
1 = direct callees only,
|
|
217
|
+
2 = callees + their callees, etc.
|
|
218
|
+
|
|
213
219
|
Returns:
|
|
214
220
|
List of callee nodes
|
|
215
221
|
"""
|
|
216
222
|
callees: List[Node] = []
|
|
217
223
|
seen: Set[str] = set()
|
|
218
224
|
|
|
219
|
-
def collect_callees(nid: str) -> None:
|
|
225
|
+
def collect_callees(nid: str, depth: int) -> None:
|
|
220
226
|
if nid in seen:
|
|
221
227
|
return
|
|
222
228
|
seen.add(nid)
|
|
223
229
|
|
|
230
|
+
if depth >= max_depth:
|
|
231
|
+
return
|
|
232
|
+
|
|
224
233
|
edges = self._qb.get_outgoing_edges(nid, [EdgeKind.CALLS, EdgeKind.REFERENCES])
|
|
225
234
|
for edge in edges:
|
|
226
235
|
callee_node = self._qb.get_node_by_id(edge.target)
|
|
227
236
|
if callee_node and edge.target not in seen:
|
|
228
237
|
callees.append(callee_node)
|
|
229
|
-
|
|
230
|
-
collect_callees(edge.target)
|
|
238
|
+
collect_callees(edge.target, depth + 1)
|
|
231
239
|
|
|
232
|
-
collect_callees(node_id)
|
|
240
|
+
collect_callees(node_id, 0)
|
|
233
241
|
return callees
|
|
234
242
|
|
|
235
243
|
def getImpactRadius(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local.
|
|
5
5
|
Author-email: mading <martin98@bu.edu>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codegraph
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codegraph-py"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.1"
|
|
8
8
|
description = "Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|