codegraphcontext 0.4.18__py3-none-any.whl → 0.5.1__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.
- codegraphcontext/api/app.py +4 -1
- codegraphcontext/api/mcp_sse.py +10 -5
- codegraphcontext/api/router.py +3 -6
- codegraphcontext/cli/cli_helpers.py +30 -8
- codegraphcontext/cli/config_manager.py +50 -31
- codegraphcontext/cli/hook_manager.py +1 -1
- codegraphcontext/cli/main.py +92 -31
- codegraphcontext/cli/registry_commands.py +11 -4
- codegraphcontext/cli/setup_macos.py +6 -1
- codegraphcontext/cli/visualizer.py +25 -1
- codegraphcontext/core/__init__.py +71 -10
- codegraphcontext/core/cgc_bundle.py +1 -1
- codegraphcontext/core/database_falkordb.py +41 -2
- codegraphcontext/core/database_kuzu.py +93 -53
- codegraphcontext/core/database_ladybug.py +55 -23
- codegraphcontext/core/jobs.py +7 -2
- codegraphcontext/core/watcher.py +132 -275
- codegraphcontext/prompts.py +1 -1
- codegraphcontext/server.py +54 -5
- codegraphcontext/stdlibs.py +18 -0
- codegraphcontext/tool_definitions.py +188 -79
- codegraphcontext/tools/code_finder.py +17 -0
- codegraphcontext/tools/graph_builder.py +260 -8
- codegraphcontext/tools/handlers/analysis_handlers.py +5 -0
- codegraphcontext/tools/handlers/indexing_handlers.py +4 -1
- codegraphcontext/tools/handlers/management_handlers.py +9 -10
- codegraphcontext/tools/handlers/query_handlers.py +21 -3
- codegraphcontext/tools/handlers/watcher_handlers.py +2 -2
- codegraphcontext/tools/indexing/persistence/writer.py +377 -47
- codegraphcontext/tools/indexing/pipeline.py +22 -1
- codegraphcontext/tools/indexing/resolution/__init__.py +11 -1
- codegraphcontext/tools/indexing/resolution/calls.py +485 -53
- codegraphcontext/tools/indexing/resolution/inheritance.py +439 -3
- codegraphcontext/tools/indexing/resolution/post_resolution.py +40 -15
- codegraphcontext/tools/indexing/schema.py +5 -0
- codegraphcontext/tools/indexing/schema_contract.py +7 -0
- codegraphcontext/tools/languages/c.py +154 -2
- codegraphcontext/tools/languages/cpp.py +13 -1
- codegraphcontext/tools/languages/csharp.py +1 -0
- codegraphcontext/tools/languages/css.py +13 -4
- codegraphcontext/tools/languages/dart.py +170 -41
- codegraphcontext/tools/languages/elixir.py +9 -6
- codegraphcontext/tools/languages/go.py +56 -16
- codegraphcontext/tools/languages/haskell.py +80 -8
- codegraphcontext/tools/languages/html.py +23 -8
- codegraphcontext/tools/languages/javascript.py +2 -2
- codegraphcontext/tools/languages/lua.py +61 -19
- codegraphcontext/tools/languages/perl.py +37 -4
- codegraphcontext/tools/languages/php.py +56 -4
- codegraphcontext/tools/languages/python.py +11 -3
- codegraphcontext/tools/languages/rust.py +70 -6
- codegraphcontext/tools/languages/swift.py +61 -11
- codegraphcontext/tools/languages/typescript.py +63 -6
- codegraphcontext/tools/package_resolver.py +159 -364
- codegraphcontext/tools/system.py +8 -1
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/METADATA +12 -6
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/RECORD +61 -60
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/WHEEL +0 -0
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/top_level.txt +0 -0
codegraphcontext/api/app.py
CHANGED
|
@@ -17,7 +17,10 @@ def create_app() -> FastAPI:
|
|
|
17
17
|
app.add_middleware(
|
|
18
18
|
CORSMiddleware,
|
|
19
19
|
allow_origins=["*"], # In production, restrict this
|
|
20
|
-
|
|
20
|
+
# Credentials must stay disabled while origins is a wildcard; the
|
|
21
|
+
# combination is rejected by browsers and would leak cookie-authed
|
|
22
|
+
# responses to any site.
|
|
23
|
+
allow_credentials=False,
|
|
21
24
|
allow_methods=["*"],
|
|
22
25
|
allow_headers=["*"],
|
|
23
26
|
)
|
codegraphcontext/api/mcp_sse.py
CHANGED
|
@@ -8,16 +8,17 @@ from mcp.types import Tool, TextContent, ServerCapabilities, ToolsCapability
|
|
|
8
8
|
from mcp.server.sse import SseServerTransport
|
|
9
9
|
|
|
10
10
|
from codegraphcontext.api.router import get_server
|
|
11
|
-
from codegraphcontext.
|
|
11
|
+
from codegraphcontext.server import _strip_workspace_prefix, _apply_response_token_limit
|
|
12
12
|
|
|
13
13
|
# Create the MCP Server instance using the SDK
|
|
14
14
|
mcp_server = Server("CodeGraphContext")
|
|
15
15
|
|
|
16
16
|
@mcp_server.list_tools()
|
|
17
17
|
async def handle_list_tools() -> list[Tool]:
|
|
18
|
-
"""List available tools."""
|
|
18
|
+
"""List available tools (honors disabledTools from mcp.json)."""
|
|
19
|
+
server = get_server()
|
|
19
20
|
tools = []
|
|
20
|
-
for name, defn in
|
|
21
|
+
for name, defn in server.tools.items():
|
|
21
22
|
tools.append(Tool(
|
|
22
23
|
name=name,
|
|
23
24
|
description=defn["description"],
|
|
@@ -33,12 +34,16 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten
|
|
|
33
34
|
|
|
34
35
|
# Execute via the existing handler logic
|
|
35
36
|
result = await server.handle_tool_call(name, args)
|
|
37
|
+
result = _strip_workspace_prefix(result)
|
|
36
38
|
|
|
37
39
|
if "error" in result:
|
|
38
40
|
return [TextContent(type="text", text=f"Error: {result['error']}")]
|
|
39
41
|
|
|
40
|
-
# Format result as JSON string for the AI
|
|
41
|
-
|
|
42
|
+
# Format result as JSON string for the AI, with the same token budget
|
|
43
|
+
# the stdio transport applies.
|
|
44
|
+
response_text = json.dumps(result, indent=2)
|
|
45
|
+
response_text = _apply_response_token_limit(name, response_text)
|
|
46
|
+
return [TextContent(type="text", text=response_text)]
|
|
42
47
|
|
|
43
48
|
# Create the SSE transport.
|
|
44
49
|
# The messages_url is where the client will POST JSON-RPC messages.
|
codegraphcontext/api/router.py
CHANGED
|
@@ -91,12 +91,9 @@ async def index_repository(
|
|
|
91
91
|
background_tasks: BackgroundTasks,
|
|
92
92
|
server: MCPServer = Depends(get_server)
|
|
93
93
|
):
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"branch": request.branch,
|
|
98
|
-
"force": request.force
|
|
99
|
-
}
|
|
94
|
+
# The add_code_to_graph handler only understands "path" (and is_dependency);
|
|
95
|
+
# repo_name/branch/force from the request model are not supported by it.
|
|
96
|
+
args = {"path": request.path}
|
|
100
97
|
|
|
101
98
|
try:
|
|
102
99
|
result = await server.handle_tool_call(
|
|
@@ -35,6 +35,7 @@ from .config_manager import (
|
|
|
35
35
|
register_repo_in_context,
|
|
36
36
|
ensure_first_run_bootstrap,
|
|
37
37
|
ContextNotFoundError,
|
|
38
|
+
is_db_deletion_allowed,
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
console = Console()
|
|
@@ -673,14 +674,32 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
673
674
|
db_manager.close_driver()
|
|
674
675
|
|
|
675
676
|
|
|
676
|
-
def update_helper(path: str, context: Optional[str] = None):
|
|
677
|
-
"""Update/refresh index for a path (alias for reindex).
|
|
677
|
+
def update_helper(path: str, context: Optional[str] = None, quiet: bool = False):
|
|
678
|
+
"""Update/refresh index for a path (alias for reindex).
|
|
679
|
+
|
|
680
|
+
When *quiet* is True (e.g. when invoked from Git hooks with --quiet),
|
|
681
|
+
Rich console output, including progress rendering, is suppressed.
|
|
682
|
+
"""
|
|
683
|
+
if quiet:
|
|
684
|
+
console.quiet = True
|
|
685
|
+
try:
|
|
686
|
+
reindex_helper(path, context)
|
|
687
|
+
finally:
|
|
688
|
+
console.quiet = False
|
|
689
|
+
return
|
|
678
690
|
console.print("[cyan]Updating repository index...[/cyan]")
|
|
679
691
|
reindex_helper(path, context)
|
|
680
692
|
|
|
681
693
|
|
|
682
694
|
def clean_helper(context: Optional[str] = None):
|
|
683
695
|
"""Remove orphaned nodes and relationships from the database."""
|
|
696
|
+
if not is_db_deletion_allowed():
|
|
697
|
+
console.print(
|
|
698
|
+
"[bold red]Error:[/bold red] Database cleanup is disabled. "
|
|
699
|
+
"Set ALLOW_DB_DELETION=true in config to enable."
|
|
700
|
+
)
|
|
701
|
+
raise typer.Exit(code=1)
|
|
702
|
+
|
|
684
703
|
services = _initialize_services(context)
|
|
685
704
|
if not all(services[:3]):
|
|
686
705
|
_fail_services_init()
|
|
@@ -738,6 +757,9 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
738
757
|
if path:
|
|
739
758
|
# Stats for specific repository
|
|
740
759
|
path_obj = Path(path).resolve()
|
|
760
|
+
# Paths are stored with forward slashes (as_posix) in the graph DB,
|
|
761
|
+
# so lookups must use the same normalization on Windows too.
|
|
762
|
+
repo_path_str = path_obj.as_posix()
|
|
741
763
|
console.print(f"[cyan]📊 Statistics for: {path_obj}[/cyan]\n")
|
|
742
764
|
|
|
743
765
|
with db_manager.get_driver().session() as session:
|
|
@@ -746,7 +768,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
746
768
|
MATCH (r:Repository {path: $path})
|
|
747
769
|
RETURN r
|
|
748
770
|
"""
|
|
749
|
-
result = session.run(repo_query, path=
|
|
771
|
+
result = session.run(repo_query, path=repo_path_str)
|
|
750
772
|
if not result.single():
|
|
751
773
|
console.print(f"[red]Repository not found: {path_obj}[/red]")
|
|
752
774
|
return
|
|
@@ -755,20 +777,20 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
755
777
|
# Get stats using separate queries to handle depth and avoid Cartesian products
|
|
756
778
|
# 1. Files
|
|
757
779
|
file_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(f) as c"
|
|
758
|
-
file_count = session.run(file_query, path=
|
|
780
|
+
file_count = session.run(file_query, path=repo_path_str).single()["c"]
|
|
759
781
|
|
|
760
782
|
# 2. Functions (including methods in classes)
|
|
761
783
|
func_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(func:Function) RETURN count(func) as c"
|
|
762
|
-
func_count = session.run(func_query, path=
|
|
784
|
+
func_count = session.run(func_query, path=repo_path_str).single()["c"]
|
|
763
785
|
|
|
764
786
|
# 3. Classes
|
|
765
787
|
class_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(c:Class) RETURN count(c) as c"
|
|
766
|
-
class_count = session.run(class_query, path=
|
|
788
|
+
class_count = session.run(class_query, path=repo_path_str).single()["c"]
|
|
767
789
|
|
|
768
790
|
# 4. Modules (imported) - Note: Module nodes are outside the repo structure usually, connected via IMPORTS
|
|
769
791
|
# We need to traverse from files to modules
|
|
770
792
|
module_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File)-[:IMPORTS]->(m:Module) RETURN count(DISTINCT m) as c"
|
|
771
|
-
module_count = session.run(module_query, path=
|
|
793
|
+
module_count = session.run(module_query, path=repo_path_str).single()["c"]
|
|
772
794
|
|
|
773
795
|
table = Table(show_header=True, header_style="bold magenta")
|
|
774
796
|
table.add_column("Metric", style="cyan")
|
|
@@ -869,7 +891,7 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
|
|
|
869
891
|
with code_finder.driver.session() as _s:
|
|
870
892
|
_r = _s.run(
|
|
871
893
|
"MATCH (n:File) WHERE n.path STARTS WITH $p RETURN count(n) AS c",
|
|
872
|
-
p=
|
|
894
|
+
p=path_obj.as_posix() + "/"
|
|
873
895
|
)
|
|
874
896
|
_count = _r.single()["c"]
|
|
875
897
|
if _count > 100:
|
|
@@ -15,6 +15,19 @@ import yaml
|
|
|
15
15
|
|
|
16
16
|
console = Console()
|
|
17
17
|
|
|
18
|
+
|
|
19
|
+
def _atomic_write_text(path: Path, content: str, *, secure: bool = False) -> None:
|
|
20
|
+
"""Write *content* to *path* atomically (temp file + replace)."""
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
23
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
24
|
+
f.write(content)
|
|
25
|
+
f.flush()
|
|
26
|
+
os.fsync(f.fileno())
|
|
27
|
+
os.replace(tmp_path, path)
|
|
28
|
+
if secure:
|
|
29
|
+
os.chmod(path, 0o600)
|
|
30
|
+
|
|
18
31
|
# Configuration file location
|
|
19
32
|
CONFIG_DIR = Path.home() / ".codegraphcontext"
|
|
20
33
|
CONFIG_FILE = CONFIG_DIR / ".env"
|
|
@@ -385,30 +398,27 @@ def save_config(config: Dict[str, str], preserve_db_credentials: bool = True):
|
|
|
385
398
|
credentials_to_write[key] = config[key]
|
|
386
399
|
|
|
387
400
|
try:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
f
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
f.write(f"# {description}\n")
|
|
410
|
-
f.write(f"{key}={value}\n\n")
|
|
411
|
-
|
|
401
|
+
lines = [
|
|
402
|
+
"# CodeGraphContext Configuration",
|
|
403
|
+
f"# Location: {CONFIG_FILE}",
|
|
404
|
+
"",
|
|
405
|
+
]
|
|
406
|
+
if credentials_to_write:
|
|
407
|
+
lines.append("# ===== Database Credentials =====")
|
|
408
|
+
for key in sorted(DATABASE_CREDENTIAL_KEYS):
|
|
409
|
+
if key in credentials_to_write:
|
|
410
|
+
lines.append(f"{key}={credentials_to_write[key]}")
|
|
411
|
+
lines.append("")
|
|
412
|
+
lines.append("# ===== Configuration Settings =====")
|
|
413
|
+
for key, value in sorted(config.items()):
|
|
414
|
+
if key in DATABASE_CREDENTIAL_KEYS:
|
|
415
|
+
continue
|
|
416
|
+
description = CONFIG_DESCRIPTIONS.get(key, "")
|
|
417
|
+
if description:
|
|
418
|
+
lines.append(f"# {description}")
|
|
419
|
+
lines.append(f"{key}={value}")
|
|
420
|
+
lines.append("")
|
|
421
|
+
_atomic_write_text(CONFIG_FILE, "\n".join(lines), secure=True)
|
|
412
422
|
console.print(f"[green]✅ Configuration saved to {CONFIG_FILE}[/green]")
|
|
413
423
|
except Exception as e:
|
|
414
424
|
console.print(f"[red]Error saving config: {e}[/red]")
|
|
@@ -804,8 +814,10 @@ def save_context_config(cfg: ContextConfig) -> None:
|
|
|
804
814
|
}
|
|
805
815
|
|
|
806
816
|
try:
|
|
807
|
-
|
|
808
|
-
|
|
817
|
+
_atomic_write_text(
|
|
818
|
+
CONTEXT_CONFIG_FILE,
|
|
819
|
+
yaml.dump(raw, default_flow_style=False, sort_keys=False),
|
|
820
|
+
)
|
|
809
821
|
except Exception as e:
|
|
810
822
|
console.print(f"[red]Error saving config.yaml: {e}[/red]")
|
|
811
823
|
|
|
@@ -887,10 +899,15 @@ def resolve_context(
|
|
|
887
899
|
|
|
888
900
|
inherited_db = load_config().get("DEFAULT_DATABASE", "falkordb")
|
|
889
901
|
|
|
890
|
-
# Copy global .env into local context for easy per-repo tweaking
|
|
902
|
+
# Copy global .env into local context for easy per-repo tweaking.
|
|
903
|
+
# Guard against the self-copy case: when cwd is the home directory,
|
|
904
|
+
# local_cgc resolves to CONFIG_DIR itself, so `local_cgc / ".env"` is
|
|
905
|
+
# CONFIG_FILE. Copying a file onto itself raises shutil.SameFileError,
|
|
906
|
+
# which crashes resolve_context for any session started from home.
|
|
891
907
|
import shutil
|
|
892
|
-
|
|
893
|
-
|
|
908
|
+
_target_env = local_cgc / ".env"
|
|
909
|
+
if CONFIG_FILE.exists() and _target_env.resolve() != CONFIG_FILE.resolve():
|
|
910
|
+
shutil.copy2(CONFIG_FILE, _target_env)
|
|
894
911
|
|
|
895
912
|
local_yaml = local_cgc / "config.yaml"
|
|
896
913
|
if not local_yaml.exists():
|
|
@@ -1172,8 +1189,10 @@ def _save_workspace_mappings(mappings: Dict[str, Dict[str, str]]) -> None:
|
|
|
1172
1189
|
raw = {}
|
|
1173
1190
|
raw["workspace_mappings"] = mappings
|
|
1174
1191
|
try:
|
|
1175
|
-
|
|
1176
|
-
|
|
1192
|
+
_atomic_write_text(
|
|
1193
|
+
CONTEXT_CONFIG_FILE,
|
|
1194
|
+
yaml.dump(raw, default_flow_style=False, sort_keys=False),
|
|
1195
|
+
)
|
|
1177
1196
|
except Exception as e:
|
|
1178
1197
|
console.print(f"[red]Error saving workspace mappings: {e}[/red]")
|
|
1179
1198
|
|
|
@@ -73,7 +73,7 @@ def install_hooks(start: Path | str | None = None, *, force: bool = False) -> Ho
|
|
|
73
73
|
)
|
|
74
74
|
hook_path.write_text(script, encoding="utf-8")
|
|
75
75
|
mode = hook_path.stat().st_mode
|
|
76
|
-
hook_path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP
|
|
76
|
+
hook_path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP)
|
|
77
77
|
|
|
78
78
|
_ensure_gitattributes(repo.root)
|
|
79
79
|
_configure_merge_driver(repo.root)
|
codegraphcontext/cli/main.py
CHANGED
|
@@ -85,8 +85,9 @@ app = typer.Typer(
|
|
|
85
85
|
)
|
|
86
86
|
console = Console(stderr=True)
|
|
87
87
|
|
|
88
|
-
# Configure basic logging for the application.
|
|
89
|
-
|
|
88
|
+
# Configure basic logging for the application. Default to WARNING so CLI
|
|
89
|
+
# output stays clean; the root --debug flag switches this to DEBUG.
|
|
90
|
+
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
|
|
90
91
|
|
|
91
92
|
|
|
92
93
|
def get_version() -> str:
|
|
@@ -142,6 +143,7 @@ def mcp_start():
|
|
|
142
143
|
# This typically happens if credentials are still not found after all checks.
|
|
143
144
|
console.print(f"[bold red]Configuration Error:[/bold red] {e}")
|
|
144
145
|
console.print("Please run `cgc neo4j setup` or use FalkorDB (default).")
|
|
146
|
+
raise typer.Exit(code=1) from e
|
|
145
147
|
except KeyboardInterrupt:
|
|
146
148
|
# Handle graceful shutdown on Ctrl+C.
|
|
147
149
|
console.print("\n[bold yellow]Server stopped by user.[/bold yellow]")
|
|
@@ -177,8 +179,10 @@ def mcp_tools():
|
|
|
177
179
|
except ValueError as e:
|
|
178
180
|
console.print(f"[bold red]Error loading tools:[/bold red] {e}")
|
|
179
181
|
console.print("Please ensure your database is configured correctly.")
|
|
182
|
+
raise typer.Exit(code=1) from e
|
|
180
183
|
except Exception as e:
|
|
181
184
|
console.print(f"[bold red]An unexpected error occurred:[/bold red] {e}")
|
|
185
|
+
raise typer.Exit(code=1) from e
|
|
182
186
|
|
|
183
187
|
# Abbreviation for mcp setup
|
|
184
188
|
@app.command("m", rich_help_panel="Shortcuts")
|
|
@@ -391,7 +395,6 @@ def _load_credentials(cli_context_flag: Optional[str] = None):
|
|
|
391
395
|
# IMPORTANT: DB-selection keys set in the shell must win over .env defaults.
|
|
392
396
|
# E.g. `DEFAULT_DATABASE=falkordb cgc index …` must not be overridden by
|
|
393
397
|
# DEFAULT_DATABASE=neo4j sitting in ~/.codegraphcontext/.env
|
|
394
|
-
DB_OVERRIDE_KEYS = {"CGC_RUNTIME_DB_TYPE", "DEFAULT_DATABASE"}
|
|
395
398
|
for key, value in merged_config.items():
|
|
396
399
|
if value is not None: # Only set non-None values
|
|
397
400
|
if key in runtime_env:
|
|
@@ -627,7 +630,7 @@ def bundle_export(
|
|
|
627
630
|
|
|
628
631
|
services = _initialize_services(context)
|
|
629
632
|
if not all(services[:3]):
|
|
630
|
-
|
|
633
|
+
raise typer.Exit(code=1)
|
|
631
634
|
db_manager, _, code_finder = services[:3]
|
|
632
635
|
|
|
633
636
|
try:
|
|
@@ -697,7 +700,7 @@ def bundle_import(
|
|
|
697
700
|
|
|
698
701
|
services = _initialize_services(context)
|
|
699
702
|
if not all(services[:3]):
|
|
700
|
-
|
|
703
|
+
raise typer.Exit(code=1)
|
|
701
704
|
db_manager, graph_builder, code_finder = services[:3]
|
|
702
705
|
|
|
703
706
|
try:
|
|
@@ -790,6 +793,53 @@ def bundle_load(
|
|
|
790
793
|
console.print("[dim]Use 'cgc registry list' to see available bundles[/dim]")
|
|
791
794
|
raise typer.Exit(code=1)
|
|
792
795
|
|
|
796
|
+
@bundle_app.command("merge")
|
|
797
|
+
def bundle_merge(
|
|
798
|
+
ancestor: str = typer.Argument(..., help="Common ancestor version of the bundle (%O)"),
|
|
799
|
+
current: str = typer.Argument(..., help="Current branch version of the bundle (%A); also the merge result"),
|
|
800
|
+
other: str = typer.Argument(..., help="Other branch version of the bundle (%B)"),
|
|
801
|
+
):
|
|
802
|
+
"""
|
|
803
|
+
Git merge driver for .cgc bundle files.
|
|
804
|
+
|
|
805
|
+
Registered by `cgc hook install` as `merge.cgc-bundle.driver = cgc bundle
|
|
806
|
+
merge %O %A %B`. Git invokes it with temp files holding the ancestor (%O),
|
|
807
|
+
current (%A) and other (%B) versions; the merge result must be left in the
|
|
808
|
+
%A file and the driver must exit 0 for the merge to proceed.
|
|
809
|
+
|
|
810
|
+
Bundles are binary snapshots that cannot be merged line-by-line, so the
|
|
811
|
+
strategy is: if both sides are identical, accept either; otherwise keep
|
|
812
|
+
the current branch's version (already in the %A file) and warn that the
|
|
813
|
+
bundle should be regenerated with `cgc export` after the merge.
|
|
814
|
+
"""
|
|
815
|
+
current_path = Path(current)
|
|
816
|
+
other_path = Path(other)
|
|
817
|
+
|
|
818
|
+
def _read_bytes(p: Path) -> bytes:
|
|
819
|
+
# A missing or empty temp file means the side has no content
|
|
820
|
+
# (e.g. the bundle is a new file on both branches → empty ancestor).
|
|
821
|
+
try:
|
|
822
|
+
return p.read_bytes() if p.exists() else b""
|
|
823
|
+
except OSError:
|
|
824
|
+
return b""
|
|
825
|
+
|
|
826
|
+
current_bytes = _read_bytes(current_path)
|
|
827
|
+
other_bytes = _read_bytes(other_path)
|
|
828
|
+
|
|
829
|
+
if current_bytes == other_bytes:
|
|
830
|
+
# Both branches have identical bundle contents; nothing to do.
|
|
831
|
+
raise typer.Exit(code=0)
|
|
832
|
+
|
|
833
|
+
# Keep the current branch's version (the %A file already contains it).
|
|
834
|
+
console.print(
|
|
835
|
+
"[yellow]⚠ Conflicting changes to a .cgc bundle were detected during merge. "
|
|
836
|
+
"Keeping the current branch's version.[/yellow]"
|
|
837
|
+
)
|
|
838
|
+
console.print(
|
|
839
|
+
"[yellow]The bundle may be stale — regenerate it with 'cgc export' after the merge.[/yellow]"
|
|
840
|
+
)
|
|
841
|
+
raise typer.Exit(code=0)
|
|
842
|
+
|
|
793
843
|
# ============================================================================
|
|
794
844
|
# HOOK COMMAND GROUP - Git integration
|
|
795
845
|
# ============================================================================
|
|
@@ -1146,11 +1196,18 @@ def doctor():
|
|
|
1146
1196
|
all_checks_passed = False
|
|
1147
1197
|
elif default_db == "falkordb":
|
|
1148
1198
|
try:
|
|
1149
|
-
|
|
1150
|
-
|
|
1199
|
+
from codegraphcontext.core import is_falkordb_usable
|
|
1200
|
+
|
|
1201
|
+
if is_falkordb_usable():
|
|
1202
|
+
console.print(" [green]✓[/green] FalkorDB Lite is installed")
|
|
1203
|
+
else:
|
|
1204
|
+
raise ImportError("FalkorDB Lite is not available on this platform")
|
|
1151
1205
|
except ImportError:
|
|
1152
|
-
|
|
1206
|
+
# falkordb is the configured/default backend, so a missing
|
|
1207
|
+
# FalkorDB Lite means the database cannot work — fail the check.
|
|
1208
|
+
console.print(" [red]✗[/red] FalkorDB Lite not installed (Python 3.12+ only)")
|
|
1153
1209
|
console.print(" Run: pip install falkordblite")
|
|
1210
|
+
all_checks_passed = False
|
|
1154
1211
|
else:
|
|
1155
1212
|
console.print(f" [yellow]⚠[/yellow] No connectivity probe for backend '{default_db}'")
|
|
1156
1213
|
except Exception as e:
|
|
@@ -1275,7 +1332,7 @@ def update(
|
|
|
1275
1332
|
_load_credentials()
|
|
1276
1333
|
if path is None:
|
|
1277
1334
|
path = str(Path.cwd())
|
|
1278
|
-
update_helper(path, context)
|
|
1335
|
+
update_helper(path, context, quiet=quiet)
|
|
1279
1336
|
|
|
1280
1337
|
@app.command()
|
|
1281
1338
|
def clean(
|
|
@@ -1344,7 +1401,7 @@ def delete(
|
|
|
1344
1401
|
# Delete all repositories
|
|
1345
1402
|
services = _initialize_services(context)
|
|
1346
1403
|
if not all(services[:3]):
|
|
1347
|
-
|
|
1404
|
+
raise typer.Exit(code=1)
|
|
1348
1405
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1349
1406
|
|
|
1350
1407
|
try:
|
|
@@ -1409,7 +1466,14 @@ def delete(
|
|
|
1409
1466
|
console.print("[red]Error: Please provide a path or use --all to delete all repositories[/red]")
|
|
1410
1467
|
console.print("Usage: cgc delete <path> or cgc delete --all")
|
|
1411
1468
|
raise typer.Exit(code=1)
|
|
1412
|
-
|
|
1469
|
+
|
|
1470
|
+
if not config_manager.is_db_deletion_allowed():
|
|
1471
|
+
console.print(
|
|
1472
|
+
"[bold red]Error:[/bold red] Repository deletion is disabled. "
|
|
1473
|
+
"Set ALLOW_DB_DELETION=true in config to enable."
|
|
1474
|
+
)
|
|
1475
|
+
raise typer.Exit(code=1)
|
|
1476
|
+
|
|
1413
1477
|
delete_helper(path, context)
|
|
1414
1478
|
|
|
1415
1479
|
|
|
@@ -1593,7 +1657,7 @@ def find_by_name(
|
|
|
1593
1657
|
_load_credentials()
|
|
1594
1658
|
services = _initialize_services(context)
|
|
1595
1659
|
if not all(services[:3]):
|
|
1596
|
-
|
|
1660
|
+
raise typer.Exit(code=1)
|
|
1597
1661
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1598
1662
|
|
|
1599
1663
|
# Resolve effective fuzzy setting: CLI flag wins, else config, else true.
|
|
@@ -1725,7 +1789,7 @@ def find_by_pattern(
|
|
|
1725
1789
|
_load_credentials()
|
|
1726
1790
|
services = _initialize_services(context)
|
|
1727
1791
|
if not all(services[:3]):
|
|
1728
|
-
|
|
1792
|
+
raise typer.Exit(code=1)
|
|
1729
1793
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1730
1794
|
|
|
1731
1795
|
try:
|
|
@@ -1818,7 +1882,7 @@ def find_by_type(
|
|
|
1818
1882
|
_load_credentials()
|
|
1819
1883
|
services = _initialize_services(context)
|
|
1820
1884
|
if not all(services[:3]):
|
|
1821
|
-
|
|
1885
|
+
raise typer.Exit(code=1)
|
|
1822
1886
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1823
1887
|
|
|
1824
1888
|
try:
|
|
@@ -1873,7 +1937,7 @@ def find_by_variable(
|
|
|
1873
1937
|
_load_credentials()
|
|
1874
1938
|
services = _initialize_services(context)
|
|
1875
1939
|
if not all(services[:3]):
|
|
1876
|
-
|
|
1940
|
+
raise typer.Exit(code=1)
|
|
1877
1941
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1878
1942
|
|
|
1879
1943
|
try:
|
|
@@ -1919,7 +1983,7 @@ def find_by_content_search(
|
|
|
1919
1983
|
_load_credentials()
|
|
1920
1984
|
services = _initialize_services(context)
|
|
1921
1985
|
if not all(services[:3]):
|
|
1922
|
-
|
|
1986
|
+
raise typer.Exit(code=1)
|
|
1923
1987
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1924
1988
|
|
|
1925
1989
|
try:
|
|
@@ -1978,7 +2042,7 @@ def find_by_decorator_search(
|
|
|
1978
2042
|
_load_credentials()
|
|
1979
2043
|
services = _initialize_services(context)
|
|
1980
2044
|
if not all(services[:3]):
|
|
1981
|
-
|
|
2045
|
+
raise typer.Exit(code=1)
|
|
1982
2046
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1983
2047
|
|
|
1984
2048
|
try:
|
|
@@ -2026,7 +2090,7 @@ def find_by_argument_search(
|
|
|
2026
2090
|
_load_credentials()
|
|
2027
2091
|
services = _initialize_services(context)
|
|
2028
2092
|
if not all(services[:3]):
|
|
2029
|
-
|
|
2093
|
+
raise typer.Exit(code=1)
|
|
2030
2094
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2031
2095
|
|
|
2032
2096
|
try:
|
|
@@ -2082,7 +2146,7 @@ def analyze_calls(
|
|
|
2082
2146
|
_load_credentials()
|
|
2083
2147
|
services = _initialize_services(context)
|
|
2084
2148
|
if not all(services[:3]):
|
|
2085
|
-
|
|
2149
|
+
raise typer.Exit(code=1)
|
|
2086
2150
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2087
2151
|
|
|
2088
2152
|
try:
|
|
@@ -2138,7 +2202,7 @@ def analyze_callers(
|
|
|
2138
2202
|
_load_credentials()
|
|
2139
2203
|
services = _initialize_services(context)
|
|
2140
2204
|
if not all(services[:3]):
|
|
2141
|
-
|
|
2205
|
+
raise typer.Exit(code=1)
|
|
2142
2206
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2143
2207
|
|
|
2144
2208
|
try:
|
|
@@ -2199,7 +2263,7 @@ def analyze_chain(
|
|
|
2199
2263
|
_load_credentials()
|
|
2200
2264
|
services = _initialize_services(context)
|
|
2201
2265
|
if not all(services[:3]):
|
|
2202
|
-
|
|
2266
|
+
raise typer.Exit(code=1)
|
|
2203
2267
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2204
2268
|
|
|
2205
2269
|
try:
|
|
@@ -2269,7 +2333,7 @@ def analyze_kotlin_call_audit(
|
|
|
2269
2333
|
_load_credentials()
|
|
2270
2334
|
services = _initialize_services(context)
|
|
2271
2335
|
if not all(services[:3]):
|
|
2272
|
-
|
|
2336
|
+
raise typer.Exit(code=1)
|
|
2273
2337
|
db_manager, _, code_finder = services[:3]
|
|
2274
2338
|
|
|
2275
2339
|
try:
|
|
@@ -2329,7 +2393,7 @@ def analyze_dependencies(
|
|
|
2329
2393
|
_load_credentials()
|
|
2330
2394
|
services = _initialize_services(context)
|
|
2331
2395
|
if not all(services[:3]):
|
|
2332
|
-
|
|
2396
|
+
raise typer.Exit(code=1)
|
|
2333
2397
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2334
2398
|
|
|
2335
2399
|
try:
|
|
@@ -2394,7 +2458,7 @@ def analyze_inheritance_tree(
|
|
|
2394
2458
|
_load_credentials()
|
|
2395
2459
|
services = _initialize_services(context)
|
|
2396
2460
|
if not all(services[:3]):
|
|
2397
|
-
|
|
2461
|
+
raise typer.Exit(code=1)
|
|
2398
2462
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2399
2463
|
|
|
2400
2464
|
try:
|
|
@@ -2464,7 +2528,7 @@ def analyze_complexity(
|
|
|
2464
2528
|
_load_credentials()
|
|
2465
2529
|
services = _initialize_services(context)
|
|
2466
2530
|
if not all(services[:3]):
|
|
2467
|
-
|
|
2531
|
+
raise typer.Exit(code=1)
|
|
2468
2532
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2469
2533
|
|
|
2470
2534
|
_FILE_EXTENSIONS = ('.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.rb',
|
|
@@ -2541,7 +2605,7 @@ def analyze_dead_code(
|
|
|
2541
2605
|
_load_credentials()
|
|
2542
2606
|
services = _initialize_services(context)
|
|
2543
2607
|
if not all(services[:3]):
|
|
2544
|
-
|
|
2608
|
+
raise typer.Exit(code=1)
|
|
2545
2609
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2546
2610
|
|
|
2547
2611
|
try:
|
|
@@ -2595,7 +2659,7 @@ def analyze_overrides(
|
|
|
2595
2659
|
_load_credentials()
|
|
2596
2660
|
services = _initialize_services(context)
|
|
2597
2661
|
if not all(services[:3]):
|
|
2598
|
-
|
|
2662
|
+
raise typer.Exit(code=1)
|
|
2599
2663
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2600
2664
|
|
|
2601
2665
|
try:
|
|
@@ -2650,7 +2714,7 @@ def analyze_variable_usage(
|
|
|
2650
2714
|
_load_credentials()
|
|
2651
2715
|
services = _initialize_services(context)
|
|
2652
2716
|
if not all(services[:3]):
|
|
2653
|
-
|
|
2717
|
+
raise typer.Exit(code=1)
|
|
2654
2718
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2655
2719
|
|
|
2656
2720
|
try:
|
|
@@ -2854,9 +2918,6 @@ def main(
|
|
|
2854
2918
|
os.environ["CGC_RUNTIME_DB_TYPE"] = database
|
|
2855
2919
|
# Initialize context object for sharing state with subcommands
|
|
2856
2920
|
ctx.ensure_object(dict)
|
|
2857
|
-
|
|
2858
|
-
if database:
|
|
2859
|
-
os.environ["CGC_RUNTIME_DB_TYPE"] = database
|
|
2860
2921
|
|
|
2861
2922
|
# Store visual flag in context for subcommands to access
|
|
2862
2923
|
if visual:
|
|
@@ -289,9 +289,16 @@ def download_bundle(name: str, output_dir: Optional[str] = None, auto_load: bool
|
|
|
289
289
|
if auto_load:
|
|
290
290
|
console.print("[cyan]Using existing bundle for loading...[/cyan]")
|
|
291
291
|
return str(output_path)
|
|
292
|
-
return
|
|
292
|
+
return False
|
|
293
293
|
output_path.unlink()
|
|
294
|
-
|
|
294
|
+
|
|
295
|
+
from ..utils.path_sandbox import is_safe_download_url
|
|
296
|
+
|
|
297
|
+
if not is_safe_download_url(download_url):
|
|
298
|
+
console.print("[bold red]Refusing to download from untrusted URL.[/bold red]")
|
|
299
|
+
console.print("[dim]Only HTTPS downloads from approved hosts are allowed.[/dim]")
|
|
300
|
+
raise typer.Exit(code=1)
|
|
301
|
+
|
|
295
302
|
# Download with progress bar
|
|
296
303
|
try:
|
|
297
304
|
console.print(f"[cyan]Downloading {clean_filename}...[/cyan]")
|
|
@@ -435,8 +442,8 @@ def load_bundle_command(bundle_name: str, clear_existing: bool = False):
|
|
|
435
442
|
stats["nodes"] = int(part.split(":")[1].strip().replace(",", ""))
|
|
436
443
|
elif "Edges:" in part:
|
|
437
444
|
stats["edges"] = int(part.split(":")[1].strip().replace(",", ""))
|
|
438
|
-
except:
|
|
439
|
-
|
|
445
|
+
except Exception as parse_exc:
|
|
446
|
+
console.print(f"[dim]Could not parse bundle stats from message: {parse_exc}[/dim]")
|
|
440
447
|
|
|
441
448
|
return (True, message, stats)
|
|
442
449
|
else:
|
|
@@ -16,12 +16,17 @@ def _brew_install_neo4j(run_command, console) -> str:
|
|
|
16
16
|
def _brew_start(service: str, run_command, console) -> bool:
|
|
17
17
|
return run_command(["brew", "services", "start", service], console, check=False) is not None
|
|
18
18
|
|
|
19
|
+
def _escape_cypher_password(password: str) -> str:
|
|
20
|
+
return password.replace("\\", "\\\\").replace("'", "\\'")
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
def _set_initial_password(new_pw: str, run_command, console) -> bool:
|
|
24
|
+
escaped_pw = _escape_cypher_password(new_pw)
|
|
20
25
|
cmd = [
|
|
21
26
|
"cypher-shell",
|
|
22
27
|
"-u", "neo4j",
|
|
23
28
|
"-p", "neo4j",
|
|
24
|
-
f"ALTER CURRENT USER SET PASSWORD FROM 'neo4j' TO '{
|
|
29
|
+
f"ALTER CURRENT USER SET PASSWORD FROM 'neo4j' TO '{escaped_pw}'",
|
|
25
30
|
]
|
|
26
31
|
return run_command(cmd, console, check=False) is not None
|
|
27
32
|
|