codegraphcontext 0.1.13__tar.gz → 0.1.14__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.
Files changed (44) hide show
  1. {codegraphcontext-0.1.13/src/codegraphcontext.egg-info → codegraphcontext-0.1.14}/PKG-INFO +23 -9
  2. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/README.md +20 -8
  3. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/pyproject.toml +4 -2
  4. codegraphcontext-0.1.14/src/codegraphcontext/cli/main.py +288 -0
  5. codegraphcontext-0.1.14/src/codegraphcontext/cli/setup_macos.py +95 -0
  6. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/cli/setup_wizard.py +7 -2
  7. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/core/watcher.py +55 -22
  8. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/server.py +122 -25
  9. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/tools/code_finder.py +3 -3
  10. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/tools/graph_builder.py +54 -13
  11. codegraphcontext-0.1.14/src/codegraphcontext/tools/languages/cpp.py +127 -0
  12. codegraphcontext-0.1.14/src/codegraphcontext/tools/languages/javascript.py +550 -0
  13. codegraphcontext-0.1.14/src/codegraphcontext/tools/languages/rust.py +120 -0
  14. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14/src/codegraphcontext.egg-info}/PKG-INFO +23 -9
  15. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext.egg-info/SOURCES.txt +5 -5
  16. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext.egg-info/requires.txt +2 -0
  17. codegraphcontext-0.1.14/tests/test_end_to_end.py +175 -0
  18. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/tests/test_graph_indexing.py +10 -8
  19. codegraphcontext-0.1.14/tests/test_graph_indexing_js.py +115 -0
  20. codegraphcontext-0.1.13/src/codegraphcontext/cli/main.py +0 -183
  21. codegraphcontext-0.1.13/src/codegraphcontext/tools/languages/javascript.py +0 -302
  22. codegraphcontext-0.1.13/tests/test_cgc_integration.py +0 -30
  23. codegraphcontext-0.1.13/tests/test_graph_connections.py +0 -26
  24. codegraphcontext-0.1.13/tests/test_imports.py +0 -185
  25. codegraphcontext-0.1.13/tests/test_jsonrpc.py +0 -113
  26. codegraphcontext-0.1.13/tests/test_tools.py +0 -268
  27. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/LICENSE +0 -0
  28. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/MANIFEST.in +0 -0
  29. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/setup.cfg +0 -0
  30. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/__init__.py +0 -0
  31. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/__main__.py +0 -0
  32. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/cli/__init__.py +0 -0
  33. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/core/__init__.py +0 -0
  34. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/core/database.py +0 -0
  35. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/core/jobs.py +0 -0
  36. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/prompts.py +0 -0
  37. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/tools/__init__.py +0 -0
  38. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/tools/import_extractor.py +0 -0
  39. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/tools/languages/python.py +0 -0
  40. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/tools/system.py +0 -0
  41. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext/utils/debug_log.py +0 -0
  42. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
  43. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
  44. {codegraphcontext-0.1.13 → codegraphcontext-0.1.14}/src/codegraphcontext.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraphcontext
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: An MCP server that indexes local code into a graph database to provide context to AI assistants.
5
5
  Author-email: Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
6
6
  License: MIT License
@@ -46,6 +46,8 @@ Requires-Dist: inquirerpy>=0.3.4
46
46
  Requires-Dist: python-dotenv>=1.0.0
47
47
  Requires-Dist: tree-sitter==0.20.4
48
48
  Requires-Dist: tree-sitter-languages==1.10.2
49
+ Requires-Dist: pyyaml
50
+ Requires-Dist: pytest
49
51
  Provides-Extra: dev
50
52
  Requires-Dist: pytest>=7.4.0; extra == "dev"
51
53
  Requires-Dist: black>=23.11.0; extra == "dev"
@@ -53,14 +55,26 @@ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
53
55
  Dynamic: license-file
54
56
 
55
57
  # CodeGraphContext
58
+
59
+ <!-- ====== Project stats ====== -->
60
+ <!-- ====== Project stats ====== -->
61
+
62
+ [![Stars](https://img.shields.io/github/stars/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/stargazers)
63
+ [![Forks](https://img.shields.io/github/forks/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/network/members)
64
+ [![Open Issues](https://img.shields.io/github/issues-raw/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/issues)
65
+ [![Open PRs](https://img.shields.io/github/issues-pr/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/pulls)
66
+ [![Closed PRs](https://img.shields.io/github/issues-pr-closed/Shashankss1205/CodeGraphContext?logo=github&color=lightgrey)](https://github.com/Shashankss1205/CodeGraphContext/pulls?q=is%3Apr+is%3Aclosed)
67
+ [![Contributors](https://img.shields.io/github/contributors/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/graphs/contributors)
68
+ [![Languages](https://img.shields.io/github/languages/count/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext)
56
69
  [![Build Status](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml/badge.svg)](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
57
- [![PyPI version](https://img.shields.io/pypi/v/codegraphcontext)](https://pypi.org/project/codegraphcontext/)
58
- [![PyPI downloads](https://img.shields.io/pypi/dm/codegraphcontext)](https://pypi.org/project/codegraphcontext/)
59
- [![GitHub stars](https://img.shields.io/github/stars/Shashankss1205/CodeGraphContext?style=social)](https://github.com/Shashankss1205/CodeGraphContext/stargazers)
60
- [![License](https://img.shields.io/github/license/Shashankss1205/CodeGraphContext)](LICENSE)
61
- [![Website](https://img.shields.io/badge/website-up-brightgreen)](http://codegraphcontext.vercel.app/)
62
- [![Watch on YouTube](https://img.shields.io/badge/YouTube-Watch%20Demo-red?logo=youtube)](https://youtu.be/KYYSdxhg1xU)
63
- [![Join Discord](https://img.shields.io/badge/Discord-Join%20Chat-7289da?logo=discord&logoColor=white)](https://discord.gg/dR4QY32uYQ)
70
+ [![PyPI version](https://img.shields.io/pypi/v/codegraphcontext?)](https://pypi.org/project/codegraphcontext/)
71
+ [![PyPI downloads](https://img.shields.io/pypi/dm/codegraphcontext?)](https://pypi.org/project/codegraphcontext/)
72
+ [![License](https://img.shields.io/github/license/Shashankss1205/CodeGraphContext?)](LICENSE)
73
+ [![Website](https://img.shields.io/badge/website-up-brightgreen?)](http://codegraphcontext.vercel.app/)
74
+ [![YouTube](https://img.shields.io/badge/YouTube-Watch%20Demo-red?logo=youtube)](https://youtu.be/KYYSdxhg1xU)
75
+ [![Discord](https://img.shields.io/badge/Discord-Join%20Chat-7289da?logo=discord&logoColor=white)](https://discord.gg/dR4QY32uYQ)
76
+
77
+
64
78
 
65
79
  An MCP server that indexes local code into a graph database to provide context to AI assistants.
66
80
 
@@ -71,7 +85,7 @@ An MCP server that indexes local code into a graph database to provide context t
71
85
  ![Using the MCP server](images/Usecase.gif)
72
86
 
73
87
  ## Project Details
74
- - **Version:** 0.1.13
88
+ - **Version:** 0.1.14
75
89
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
76
90
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
77
91
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -1,12 +1,24 @@
1
1
  # CodeGraphContext
2
+
3
+ <!-- ====== Project stats ====== -->
4
+ <!-- ====== Project stats ====== -->
5
+
6
+ [![Stars](https://img.shields.io/github/stars/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/stargazers)
7
+ [![Forks](https://img.shields.io/github/forks/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/network/members)
8
+ [![Open Issues](https://img.shields.io/github/issues-raw/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/issues)
9
+ [![Open PRs](https://img.shields.io/github/issues-pr/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/pulls)
10
+ [![Closed PRs](https://img.shields.io/github/issues-pr-closed/Shashankss1205/CodeGraphContext?logo=github&color=lightgrey)](https://github.com/Shashankss1205/CodeGraphContext/pulls?q=is%3Apr+is%3Aclosed)
11
+ [![Contributors](https://img.shields.io/github/contributors/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext/graphs/contributors)
12
+ [![Languages](https://img.shields.io/github/languages/count/Shashankss1205/CodeGraphContext?logo=github)](https://github.com/Shashankss1205/CodeGraphContext)
2
13
  [![Build Status](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml/badge.svg)](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
3
- [![PyPI version](https://img.shields.io/pypi/v/codegraphcontext)](https://pypi.org/project/codegraphcontext/)
4
- [![PyPI downloads](https://img.shields.io/pypi/dm/codegraphcontext)](https://pypi.org/project/codegraphcontext/)
5
- [![GitHub stars](https://img.shields.io/github/stars/Shashankss1205/CodeGraphContext?style=social)](https://github.com/Shashankss1205/CodeGraphContext/stargazers)
6
- [![License](https://img.shields.io/github/license/Shashankss1205/CodeGraphContext)](LICENSE)
7
- [![Website](https://img.shields.io/badge/website-up-brightgreen)](http://codegraphcontext.vercel.app/)
8
- [![Watch on YouTube](https://img.shields.io/badge/YouTube-Watch%20Demo-red?logo=youtube)](https://youtu.be/KYYSdxhg1xU)
9
- [![Join Discord](https://img.shields.io/badge/Discord-Join%20Chat-7289da?logo=discord&logoColor=white)](https://discord.gg/dR4QY32uYQ)
14
+ [![PyPI version](https://img.shields.io/pypi/v/codegraphcontext?)](https://pypi.org/project/codegraphcontext/)
15
+ [![PyPI downloads](https://img.shields.io/pypi/dm/codegraphcontext?)](https://pypi.org/project/codegraphcontext/)
16
+ [![License](https://img.shields.io/github/license/Shashankss1205/CodeGraphContext?)](LICENSE)
17
+ [![Website](https://img.shields.io/badge/website-up-brightgreen?)](http://codegraphcontext.vercel.app/)
18
+ [![YouTube](https://img.shields.io/badge/YouTube-Watch%20Demo-red?logo=youtube)](https://youtu.be/KYYSdxhg1xU)
19
+ [![Discord](https://img.shields.io/badge/Discord-Join%20Chat-7289da?logo=discord&logoColor=white)](https://discord.gg/dR4QY32uYQ)
20
+
21
+
10
22
 
11
23
  An MCP server that indexes local code into a graph database to provide context to AI assistants.
12
24
 
@@ -17,7 +29,7 @@ An MCP server that indexes local code into a graph database to provide context t
17
29
  ![Using the MCP server](images/Usecase.gif)
18
30
 
19
31
  ## Project Details
20
- - **Version:** 0.1.13
32
+ - **Version:** 0.1.14
21
33
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
22
34
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
23
35
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codegraphcontext"
3
- version = "0.1.13"
3
+ version = "0.1.14"
4
4
  description = "An MCP server that indexes local code into a graph database to provide context to AI assistants."
5
5
  authors = [{ name = "Shashank Shekhar Singh", email = "shashankshekharsingh1205@gmail.com" }]
6
6
  readme = "README.md"
@@ -24,7 +24,9 @@ dependencies = [
24
24
  "inquirerpy>=0.3.4",
25
25
  "python-dotenv>=1.0.0",
26
26
  "tree-sitter==0.20.4",
27
- "tree-sitter-languages==1.10.2"
27
+ "tree-sitter-languages==1.10.2",
28
+ "pyyaml",
29
+ "pytest"
28
30
  ]
29
31
 
30
32
  [project.urls]
@@ -0,0 +1,288 @@
1
+ # src/codegraphcontext/cli/main.py
2
+ """
3
+ This module defines the command-line interface (CLI) for the CodeGraphContext application.
4
+ It uses the Typer library to create a user-friendly and well-documented CLI.
5
+
6
+ Commands:
7
+ - setup: Runs an interactive wizard to configure the Neo4j database connection.
8
+ - start: Launches the main MCP server.
9
+ - tool: A placeholder for directly calling server tools (for debugging).
10
+ - help: Displays help information.
11
+ - version: Show the installed version.
12
+ """
13
+ import typer
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+ from typing import Optional
17
+ import asyncio
18
+ import logging
19
+ import json
20
+ import os
21
+ import time
22
+ from pathlib import Path
23
+ from dotenv import load_dotenv, find_dotenv
24
+ from importlib.metadata import version as pkg_version, PackageNotFoundError
25
+ from codegraphcontext.server import MCPServer
26
+ from .setup_wizard import run_setup_wizard
27
+
28
+ # Set the log level for the noisy neo4j logger to WARNING to keep the output clean.
29
+ logging.getLogger("neo4j").setLevel(logging.WARNING)
30
+
31
+ # Initialize the Typer app and Rich console for formatted output.
32
+ app = typer.Typer(
33
+ name="cgc",
34
+ help="CodeGraphContext: An MCP server for AI-powered code analysis.",
35
+ add_completion=False,
36
+ )
37
+ console = Console(stderr=True)
38
+
39
+ # Configure basic logging for the application.
40
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
41
+
42
+
43
+ def get_version() -> str:
44
+ """
45
+ Try to read version from the installed package metadata.
46
+ Fallback to a dev version if not installed.
47
+ """
48
+ try:
49
+ return pkg_version("codegraphcontext") # must match [project].name in pyproject.toml
50
+ except PackageNotFoundError:
51
+ return "0.0.0 (dev)"
52
+
53
+
54
+ @app.command()
55
+ def setup():
56
+ """
57
+ Runs the interactive setup wizard to configure the server and database connection.
58
+ This helps users set up a local Docker-based Neo4j instance or connect to a remote one.
59
+ """
60
+ run_setup_wizard()
61
+
62
+ def _load_credentials():
63
+ """
64
+ Loads Neo4j credentials from various sources into environment variables.
65
+ Priority order:
66
+ 1. Local `mcp.json`
67
+ 2. Global `~/.codegraphcontext/.env`
68
+ 3. Any `.env` file found in the directory tree.
69
+ """
70
+ # 1. Prefer loading from mcp.json
71
+ mcp_file_path = Path.cwd() / "mcp.json"
72
+ if mcp_file_path.exists():
73
+ try:
74
+ with open(mcp_file_path, "r") as f:
75
+ mcp_config = json.load(f)
76
+ server_env = mcp_config.get("mcpServers", {}).get("CodeGraphContext", {}).get("env", {})
77
+ for key, value in server_env.items():
78
+ os.environ[key] = value
79
+ console.print("[green]Loaded Neo4j credentials from local mcp.json.[/green]")
80
+ return
81
+ except Exception as e:
82
+ console.print(f"[bold red]Error loading mcp.json:[/bold red] {e}")
83
+
84
+ # 2. Try global .env file
85
+ global_env_path = Path.home() / ".codegraphcontext" / ".env"
86
+ if global_env_path.exists():
87
+ try:
88
+ load_dotenv(dotenv_path=global_env_path)
89
+ console.print(f"[green]Loaded Neo4j credentials from global .env file: {global_env_path}[/green]")
90
+ return
91
+ except Exception as e:
92
+ console.print(f"[bold red]Error loading global .env file from {global_env_path}:[/bold red] {e}")
93
+
94
+ # 3. Fallback to any discovered .env
95
+ try:
96
+ dotenv_path = find_dotenv(usecwd=True, raise_error_if_not_found=False)
97
+ if dotenv_path:
98
+ load_dotenv(dotenv_path)
99
+ console.print(f"[green]Loaded Neo4j credentials from discovered .env file: {dotenv_path}[/green]")
100
+ else:
101
+ console.print("[yellow]No local mcp.json or .env file found. Credentials may not be set.[/yellow]")
102
+ except Exception as e:
103
+ console.print(f"[bold red]Error loading .env file:[/bold red] {e}")
104
+
105
+
106
+ @app.command()
107
+ def start():
108
+ """
109
+ Starts the CodeGraphContext MCP server, which listens for JSON-RPC requests from stdin.
110
+ """
111
+ console.print("[bold green]Starting CodeGraphContext Server...[/bold green]")
112
+ _load_credentials()
113
+
114
+ server = None
115
+ loop = asyncio.new_event_loop()
116
+ asyncio.set_event_loop(loop)
117
+ try:
118
+ # Initialize and run the main server.
119
+ server = MCPServer(loop=loop)
120
+ loop.run_until_complete(server.run())
121
+ except ValueError as e:
122
+ # This typically happens if credentials are still not found after all checks.
123
+ console.print(f"[bold red]Configuration Error:[/bold red] {e}")
124
+ console.print("Please run `cgc setup` to configure the server.")
125
+ except KeyboardInterrupt:
126
+ # Handle graceful shutdown on Ctrl+C.
127
+ console.print("\n[bold yellow]Server stopped by user.[/bold yellow]")
128
+ finally:
129
+ # Ensure server and event loop are properly closed.
130
+ if server:
131
+ server.shutdown()
132
+ loop.close()
133
+
134
+
135
+
136
+ def _run_tool(tool_name: str, tool_args: dict):
137
+ """Helper function to run a tool and handle the server lifecycle."""
138
+ _load_credentials()
139
+ try:
140
+ loop = asyncio.new_event_loop()
141
+ asyncio.set_event_loop(loop)
142
+ server = MCPServer(loop=loop)
143
+
144
+ result = loop.run_until_complete(server.handle_tool_call(tool_name, tool_args))
145
+
146
+ if isinstance(result, dict) and "job_id" in result:
147
+ job_id = result["job_id"]
148
+ console.print(f"[green]Successfully started job '{job_id}' for tool '{tool_name}'.[/green]")
149
+ console.print(f"Estimated files: {result.get('estimated_files')}, Estimated duration: {result.get('estimated_duration_human')}")
150
+ console.print(f"\n[bold yellow]Polling for completion...[/bold yellow]")
151
+
152
+ while True:
153
+ time.sleep(2)
154
+ status_result = loop.run_until_complete(server.handle_tool_call("check_job_status", {"job_id": job_id}))
155
+ job_status = status_result.get("job", {}).get("status")
156
+ processed_files = status_result.get("job", {}).get("processed_files", 0)
157
+ total_files = status_result.get("job", {}).get("total_files", 0)
158
+ console.print(f"Job status: {job_status} ({processed_files}/{total_files} files)")
159
+ if job_status in ["completed", "failed", "cancelled"]:
160
+ console.print(json.dumps(status_result, indent=2))
161
+ break
162
+ else:
163
+ console.print(json.dumps(result, indent=2))
164
+
165
+ except ValueError as e:
166
+ console.print(f"[bold red]Configuration Error:[/bold red] {e}")
167
+ except Exception as e:
168
+ console.print(f"[bold red]An unexpected error occurred:[/bold red] {e}")
169
+ finally:
170
+ if 'loop' in locals() and loop.is_running():
171
+ loop.close()
172
+
173
+ @app.command()
174
+ def index(path: Optional[str] = typer.Argument(None, help="Path to the directory or file to index. Defaults to the current directory.")):
175
+ """
176
+ Indexes a directory or file by adding it to the code graph.
177
+ If no path is provided, it indexes the current directory.
178
+ """
179
+ if path is None:
180
+ path = "."
181
+ _run_tool("add_code_to_graph", {"path": path})
182
+
183
+ @app.command()
184
+ def delete(path: str = typer.Argument(..., help="Path of the repository to delete from the code graph.")):
185
+ """
186
+ Deletes a repository from the code graph.
187
+ """
188
+ _run_tool("delete_repository", {"repo_path": path})
189
+
190
+ @app.command()
191
+ def visualize(query: Optional[str] = typer.Argument(None, help="The Cypher query to visualize.")):
192
+ """
193
+ Generates a URL to visualize a Cypher query in the Neo4j Browser.
194
+ If no query is provided, a default query will be used.
195
+ """
196
+ if query is None:
197
+ query = "MATCH p=()-->() RETURN p"
198
+ _run_tool("visualize_graph_query", {"cypher_query": query})
199
+
200
+ @app.command(name="list_repos")
201
+ def list_repos():
202
+ """
203
+ Lists all indexed repositories.
204
+ """
205
+ _run_tool("list_indexed_repositories", {})
206
+
207
+ @app.command(name="add_package")
208
+ def add_package(package_name: str = typer.Argument(..., help="Name of the Python package to add.")):
209
+ """
210
+ Adds a Python package to the code graph.
211
+ """
212
+ _run_tool("add_package_to_graph", {"package_name": package_name})
213
+
214
+ @app.command()
215
+ def cypher(query: str = typer.Argument(..., help="The read-only Cypher query to execute.")):
216
+ """
217
+ Executes a read-only Cypher query.
218
+ """
219
+ _run_tool("execute_cypher_query", {"cypher_query": query})
220
+
221
+
222
+ @app.command(name="list_mcp_tools")
223
+ def list_mcp_tools():
224
+ """
225
+ Lists all available tools and their descriptions.
226
+ """
227
+ _load_credentials()
228
+ console.print("[bold green]Available Tools:[/bold green]")
229
+ try:
230
+ # Instantiate the server to access the tool definitions.
231
+ server = MCPServer()
232
+ tools = server.tools.values()
233
+
234
+ table = Table(show_header=True, header_style="bold magenta")
235
+ table.add_column("Tool Name", style="dim", width=30)
236
+ table.add_column("Description")
237
+
238
+ for tool in sorted(tools, key=lambda t: t['name']):
239
+ table.add_row(tool['name'], tool['description'])
240
+
241
+ console.print(table)
242
+
243
+ except ValueError as e:
244
+ console.print(f"[bold red]Error loading tools:[/bold red] {e}")
245
+ console.print("Please ensure your Neo4j credentials are set up correctly (`cgc setup`), as they are needed to initialize the server.")
246
+ except Exception as e:
247
+ console.print(f"[bold red]An unexpected error occurred:[/bold red] {e}")
248
+
249
+
250
+ @app.command()
251
+ def help(ctx: typer.Context):
252
+ """Show the main help message and exit."""
253
+ root_ctx = ctx.parent or ctx
254
+ typer.echo(root_ctx.get_help())
255
+
256
+
257
+ @app.command("version")
258
+ def version_cmd():
259
+ """Show the application version."""
260
+ console.print(f"CodeGraphContext [bold cyan]{get_version()}[/bold cyan]")
261
+
262
+
263
+ @app.callback(invoke_without_command=True)
264
+ def main(
265
+ ctx: typer.Context,
266
+ version_: bool = typer.Option(
267
+ None,
268
+ "--version",
269
+ "-v",
270
+ help="Show the application version and exit.",
271
+ is_eager=True,
272
+ ),
273
+ ):
274
+ """
275
+ Main entry point for the cgc CLI application.
276
+ If no subcommand is provided, it displays a welcome message with instructions.
277
+ """
278
+ if version_:
279
+ console.print(f"CodeGraphContext [bold cyan]{get_version()}[/bold cyan]")
280
+ raise typer.Exit()
281
+
282
+ if ctx.invoked_subcommand is None:
283
+ console.print("[bold green]👋 Welcome to CodeGraphContext (cgc)![/bold green]\n")
284
+ console.print("👉 Run [cyan]cgc setup[/cyan] to configure the server and database.")
285
+ console.print("👉 Run [cyan]cgc start[/cyan] to launch the server.")
286
+ console.print("👉 Run [cyan]cgc help[/cyan] to see all available commands.\n")
287
+ console.print("👉 Run [cyan]cgc --version[/cyan] to check the version.\n")
288
+ console.print("👉 Running [green]codegraphcontext [white]works the same as using [green]cgc")
@@ -0,0 +1,95 @@
1
+ # src/codegraphcontext/cli/setup_macos.py
2
+ import platform
3
+ import time
4
+ from pathlib import Path
5
+
6
+ def _has_brew(run_command, console) -> bool:
7
+ return run_command(["which", "brew"], console, check=False) is not None
8
+
9
+ def _brew_install_neo4j(run_command, console) -> str:
10
+ if run_command(["brew", "install", "neo4j@5"], console, check=False):
11
+ return "neo4j@5"
12
+ if run_command(["brew", "install", "neo4j"], console, check=False):
13
+ return "neo4j"
14
+ return ""
15
+
16
+ def _brew_start(service: str, run_command, console) -> bool:
17
+ return run_command(["brew", "services", "start", service], console, check=False) is not None
18
+
19
+ def _set_initial_password(new_pw: str, run_command, console) -> bool:
20
+ cmd = [
21
+ "cypher-shell",
22
+ "-u", "neo4j",
23
+ "-p", "neo4j",
24
+ f"ALTER CURRENT USER SET PASSWORD FROM 'neo4j' TO '{new_pw}'",
25
+ ]
26
+ return run_command(cmd, console, check=False) is not None
27
+
28
+ def setup_macos_binary(console, prompt, run_command, _generate_mcp_json):
29
+ """Automates Neo4j install & config on macOS via Homebrew."""
30
+ os_name = platform.system()
31
+ console.print(f"Detected Operating System: [bold yellow]{os_name}[/bold yellow]")
32
+
33
+ if os_name != "Darwin":
34
+ console.print("[yellow]This installer is for macOS only.[/yellow]")
35
+ return
36
+
37
+ console.print("[bold]Starting automated Neo4j installation for macOS.[/bold]")
38
+
39
+ if not prompt([{
40
+ "type": "confirm",
41
+ "message": "Proceed with Homebrew-based install of Neo4j?",
42
+ "name": "proceed",
43
+ "default": True
44
+ }]).get("proceed"):
45
+ return
46
+
47
+ console.print("\n[bold]Step: Checking for Homebrew...[/bold]")
48
+ if not _has_brew(run_command, console):
49
+ console.print(
50
+ "[bold red]Homebrew not found.[/bold red] "
51
+ "Install from [bold blue]https://brew.sh[/bold blue] and re-run this setup."
52
+ )
53
+ return
54
+
55
+ console.print("\n[bold]Step: Installing Neo4j via Homebrew...[/bold]")
56
+ service = _brew_install_neo4j(run_command, console)
57
+ if not service:
58
+ console.print("[bold red]Failed to install Neo4j via Homebrew.[/bold red]")
59
+ return
60
+
61
+ console.print(f"\n[bold]Step: Starting Neo4j service ({service})...[/bold]")
62
+ if not _brew_start(service, run_command, console):
63
+ console.print("[bold red]Failed to start Neo4j with brew services.[/bold red]")
64
+ return
65
+
66
+ while True:
67
+ answers = prompt([
68
+ {"type": "password", "message": "Enter a new password for Neo4j:", "name": "pw"},
69
+ {"type": "password", "message": "Confirm the new password:", "name": "pw2"},
70
+ ]) or {}
71
+ if not answers:
72
+ return
73
+ pw, pw2 = answers.get("pw"), answers.get("pw2")
74
+ if pw and pw == pw2:
75
+ new_password = pw
76
+ break
77
+ console.print("[red]Passwords do not match or are empty. Please try again.[/red]")
78
+
79
+ console.print("\n[yellow]Waiting 10 seconds for Neo4j to finish starting...[/yellow]")
80
+ time.sleep(10)
81
+
82
+ console.print("\n[bold]Step: Setting initial password with cypher-shell...[/bold]")
83
+ if not _set_initial_password(new_password, run_command, console):
84
+ console.print(
85
+ "[bold red]Failed to set the initial password.[/bold red]\n"
86
+ "Try manually:\n"
87
+ " cypher-shell -u neo4j -p neo4j \"ALTER CURRENT USER SET PASSWORD FROM 'neo4j' TO '<your_pw>'\""
88
+ )
89
+ return
90
+
91
+ creds = {"uri": "neo4j://localhost:7687", "username": "neo4j", "password": new_password}
92
+ _generate_mcp_json(creds)
93
+
94
+ env_path = Path.home() / ".codegraphcontext" / ".env"
95
+ console.print(f"\n[bold green]All done![/bold green] Neo4j running. Credentials saved to: [bold]{env_path}[/bold]")
@@ -374,11 +374,16 @@ def setup_local_db():
374
374
  ]
375
375
  result = prompt(questions)
376
376
  local_method = result.get("local_method")
377
-
377
+
378
378
  if local_method and "Docker" in local_method:
379
379
  setup_docker()
380
380
  elif local_method:
381
- setup_local_binary()
381
+ if platform.system() == "Darwin":
382
+ # lazy import to avoid circular import
383
+ from .setup_macos import setup_macos_binary
384
+ setup_macos_binary(console, prompt, run_command, _generate_mcp_json)
385
+ else:
386
+ setup_local_binary()
382
387
 
383
388
  def setup_docker():
384
389
  """Creates Docker files and runs docker-compose for Neo4j."""
@@ -25,7 +25,7 @@ class RepositoryEventHandler(FileSystemEventHandler):
25
25
  to build a baseline and then uses this cached state to perform efficient
26
26
  updates when files are changed, created, or deleted.
27
27
  """
28
- def __init__(self, graph_builder: "GraphBuilder", repo_path: Path, debounce_interval=2.0):
28
+ def __init__(self, graph_builder: "GraphBuilder", repo_path: Path, debounce_interval=2.0, perform_initial_scan: bool = True):
29
29
  """
30
30
  Initializes the event handler.
31
31
 
@@ -33,6 +33,7 @@ class RepositoryEventHandler(FileSystemEventHandler):
33
33
  graph_builder: An instance of the GraphBuilder to perform graph operations.
34
34
  repo_path: The absolute path to the repository directory to watch.
35
35
  debounce_interval: The time in seconds to wait for more changes before processing an event.
36
+ perform_initial_scan: Whether to perform an initial scan of the repository.
36
37
  """
37
38
  super().__init__()
38
39
  self.graph_builder = graph_builder
@@ -45,7 +46,8 @@ class RepositoryEventHandler(FileSystemEventHandler):
45
46
  self.imports_map = {}
46
47
 
47
48
  # Perform the initial scan and linking when the watcher is created.
48
- self._initial_scan()
49
+ if perform_initial_scan:
50
+ self._initial_scan()
49
51
 
50
52
  def _initial_scan(self):
51
53
  """Scans the entire repository, parses all files, and builds the initial graph."""
@@ -82,30 +84,39 @@ class RepositoryEventHandler(FileSystemEventHandler):
82
84
  def _handle_modification(self, event_path_str: str):
83
85
  """
84
86
  Orchestrates the complete update cycle for a modified or created file.
85
- This involves updating the graph and then re-linking the relationships.
87
+ This involves re-scanning the entire repo to update cross-file relationships.
86
88
  """
87
- logger.info(f"File change detected, starting full update for: {event_path_str}")
89
+ logger.info(f"File change detected, starting full repository refresh for: {event_path_str}")
88
90
  modified_path = Path(event_path_str)
89
91
 
90
- # 1. Update the specific file in the graph (delete old nodes, create new ones).
91
- new_file_data = self.graph_builder.update_file_in_graph(
92
+ # 1. Get all supported files in the repository.
93
+ supported_extensions = self.graph_builder.parsers.keys()
94
+ all_files = [f for f in self.repo_path.rglob("*") if f.is_file() and f.suffix in supported_extensions]
95
+
96
+ # 2. Re-scan all files to get a fresh, global map of all symbols.
97
+ self.imports_map = self.graph_builder._pre_scan_for_imports(all_files)
98
+ logger.info("Refreshed global imports map.")
99
+
100
+ # 3. Update the specific file that changed in the graph.
101
+ # This deletes old nodes and adds new ones for the single file.
102
+ self.graph_builder.update_file_in_graph(
92
103
  modified_path, self.repo_path, self.imports_map
93
104
  )
94
105
 
95
- if not new_file_data:
96
- logger.error(f"Update failed for {event_path_str}, skipping re-link.")
97
- return
98
-
99
- # 2. Update the in-memory cache of the repository's state.
100
- self.all_file_data = [d for d in self.all_file_data if d.get("file_path") != event_path_str]
101
- if not new_file_data.get("deleted"):
102
- self.all_file_data.append(new_file_data)
106
+ # 4. Re-parse all files to have a complete, in-memory representation for the linking pass.
107
+ # This is necessary because a change in one file can affect relationships in others.
108
+ self.all_file_data = []
109
+ for f in all_files:
110
+ parsed_data = self.graph_builder.parse_file(self.repo_path, f)
111
+ if "error" not in parsed_data:
112
+ self.all_file_data.append(parsed_data)
113
+ logger.info("Refreshed in-memory cache of all file data.")
103
114
 
104
- # 3. CRITICAL: Re-link the entire graph's call relationships using the updated cache.
105
- # This is necessary because a change in one file can affect its relationship with any other file.
106
- logger.info("Re-linking the call graph with updated information...")
115
+ # 5. CRITICAL: Re-link the entire graph using the fully updated cache and imports map.
116
+ logger.info("Re-linking the entire graph for calls and inheritance...")
107
117
  self.graph_builder._create_all_function_calls(self.all_file_data, self.imports_map)
108
- logger.info(f"Graph update for {event_path_str} complete! ✅")
118
+ self.graph_builder._create_all_inheritance_links(self.all_file_data, self.imports_map)
119
+ logger.info(f"Graph refresh for change in {event_path_str} complete! ✅")
109
120
 
110
121
  # The following methods are called by the watchdog observer when a file event occurs.
111
122
  def on_created(self, event):
@@ -136,8 +147,9 @@ class CodeWatcher:
136
147
  self.graph_builder = graph_builder
137
148
  self.observer = Observer()
138
149
  self.watched_paths = set() # Keep track of paths already being watched.
150
+ self.watches = {} # Store watch objects to allow unscheduling
139
151
 
140
- def watch_directory(self, path: str):
152
+ def watch_directory(self, path: str, perform_initial_scan: bool = True):
141
153
  """Schedules a directory to be watched for changes."""
142
154
  path_obj = Path(path).resolve()
143
155
  path_str = str(path_obj)
@@ -147,13 +159,34 @@ class CodeWatcher:
147
159
  return {"message": f"Path already being watched: {path_str}"}
148
160
 
149
161
  # Create a new, dedicated event handler for this specific repository path.
150
- event_handler = RepositoryEventHandler(self.graph_builder, path_obj)
162
+ event_handler = RepositoryEventHandler(self.graph_builder, path_obj, perform_initial_scan=perform_initial_scan)
151
163
 
152
- self.observer.schedule(event_handler, path_str, recursive=True)
164
+ watch = self.observer.schedule(event_handler, path_str, recursive=True)
165
+ self.watches[path_str] = watch
153
166
  self.watched_paths.add(path_str)
154
167
  logger.info(f"Started watching for code changes in: {path_str}")
155
168
 
156
- return {"message": f"Started watching {path_str}. Initial scan is in progress."}
169
+ return {"message": f"Started watching {path_str}."}
170
+ def unwatch_directory(self, path: str):
171
+ """Stops watching a directory for changes."""
172
+ path_obj = Path(path).resolve()
173
+ path_str = str(path_obj)
174
+
175
+ if path_str not in self.watched_paths:
176
+ logger.warning(f"Attempted to unwatch a path that is not being watched: {path_str}")
177
+ return {"error": f"Path not currently being watched: {path_str}"}
178
+
179
+ watch = self.watches.pop(path_str, None)
180
+ if watch:
181
+ self.observer.unschedule(watch)
182
+
183
+ self.watched_paths.discard(path_str)
184
+ logger.info(f"Stopped watching for code changes in: {path_str}")
185
+ return {"message": f"Stopped watching {path_str}."}
186
+
187
+ def list_watched_paths(self) -> list:
188
+ """Returns a list of all currently watched directory paths."""
189
+ return list(self.watched_paths)
157
190
 
158
191
  def start(self):
159
192
  """Starts the observer thread."""