codegraphcontext 0.4.18__py3-none-any.whl → 0.5.2__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 +63 -22
- codegraphcontext/api/router.py +3 -6
- codegraphcontext/cli/cli_helpers.py +87 -13
- codegraphcontext/cli/config_manager.py +152 -38
- codegraphcontext/cli/hook_manager.py +1 -1
- codegraphcontext/cli/main.py +172 -42
- codegraphcontext/cli/registry_commands.py +11 -4
- codegraphcontext/cli/setup_macos.py +6 -1
- codegraphcontext/cli/setup_wizard.py +61 -56
- codegraphcontext/cli/visualizer.py +25 -1
- codegraphcontext/core/__init__.py +71 -10
- codegraphcontext/core/cgc_bundle.py +1 -1
- codegraphcontext/core/cgcignore.py +4 -1
- codegraphcontext/core/database_embedded_kuzu.py +1265 -0
- codegraphcontext/core/database_falkordb.py +41 -2
- codegraphcontext/core/database_kuzu.py +37 -1163
- codegraphcontext/core/database_ladybug.py +37 -1145
- codegraphcontext/core/graph_query.py +45 -0
- codegraphcontext/core/jobs.py +7 -2
- codegraphcontext/core/watcher.py +114 -188
- codegraphcontext/prompts.py +1 -1
- codegraphcontext/server.py +111 -19
- codegraphcontext/stdlibs.py +18 -0
- codegraphcontext/tool_definitions.py +188 -79
- codegraphcontext/tools/code_finder.py +33 -19
- codegraphcontext/tools/graph_builder.py +266 -10
- 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/embeddings.py +63 -49
- codegraphcontext/tools/indexing/persistence/writer.py +437 -49
- codegraphcontext/tools/indexing/pipeline.py +66 -1
- codegraphcontext/tools/indexing/resolution/__init__.py +11 -1
- codegraphcontext/tools/indexing/resolution/calls.py +527 -70
- codegraphcontext/tools/indexing/resolution/inheritance.py +448 -4
- 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/indexing/scip_pipeline.py +25 -10
- codegraphcontext/tools/indexing/vector_resolver.py +72 -57
- 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 +13 -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/query_tool_languages/c_toolkit.py +1 -1
- codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +2 -2
- codegraphcontext/tools/query_tool_languages/dart_toolkit.py +2 -2
- codegraphcontext/tools/query_tool_languages/go_toolkit.py +1 -1
- codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +3 -3
- codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +1 -1
- codegraphcontext/tools/query_tool_languages/perl_toolkit.py +2 -2
- codegraphcontext/tools/query_tool_languages/python_toolkit.py +1 -1
- codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +1 -1
- codegraphcontext/tools/query_tool_languages/rust_toolkit.py +1 -1
- codegraphcontext/tools/query_tool_languages/scala_toolkit.py +2 -2
- codegraphcontext/tools/query_tool_languages/swift_toolkit.py +2 -2
- codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +1 -1
- codegraphcontext/tools/scip_indexer.py +45 -3
- codegraphcontext/tools/system.py +8 -1
- codegraphcontext/tools/tree_sitter_parser.py +41 -96
- codegraphcontext/utils/debug_log.py +5 -0
- codegraphcontext/utils/gcf_encoder.py +58 -0
- codegraphcontext/utils/visualize_graph.py +461 -0
- codegraphcontext/viz/server.py +348 -32
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/METADATA +201 -23
- codegraphcontext-0.5.2.dist-info/RECORD +132 -0
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/WHEEL +1 -1
- codegraphcontext/viz/dist/assets/__vite-browser-external-9wXp6ZBx.js +0 -1
- codegraphcontext/viz/dist/assets/function-calls-BtRHrqa2.png +0 -0
- codegraphcontext/viz/dist/assets/graph-total-D1fBAugo.png +0 -0
- codegraphcontext/viz/dist/assets/hero-graph-2voMJp2a.jpg +0 -0
- codegraphcontext/viz/dist/assets/hierarchy-DGADo0YT.png +0 -0
- codegraphcontext/viz/dist/assets/index-C-187lf0.js +0 -5587
- codegraphcontext/viz/dist/assets/index-fNAa6jgv.css +0 -1
- codegraphcontext/viz/dist/assets/parser-pyodide.worker-BgsDfaad.js +0 -370
- codegraphcontext/viz/dist/assets/parser.worker-_nvrecvj.js +0 -233
- codegraphcontext/viz/dist/assets/tree-sitter-qKYAACSa.wasm +0 -0
- codegraphcontext/viz/dist/cgcIcon.png +0 -0
- codegraphcontext/viz/dist/favicon.ico +0 -0
- codegraphcontext/viz/dist/index.html +0 -32
- codegraphcontext/viz/dist/logo-icon.svg +0 -85
- codegraphcontext/viz/dist/logo.svg +0 -100
- codegraphcontext/viz/dist/placeholder.svg +0 -1
- codegraphcontext/viz/dist/preview-image.png +0 -0
- codegraphcontext/viz/dist/robots.txt +0 -14
- codegraphcontext/viz/dist/wasm/tree-sitter-c.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-core.js +0 -1
- codegraphcontext/viz/dist/wasm/tree-sitter-cpp.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-dart.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-go.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-java.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-javascript.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-kotlin.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-perl.wasm +0 -1
- codegraphcontext/viz/dist/wasm/tree-sitter-php.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-python.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-ruby.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-rust.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-swift.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-tsx.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter-typescript.wasm +0 -0
- codegraphcontext/viz/dist/wasm/tree-sitter.wasm +0 -0
- codegraphcontext/viz/dist/wasm/web-tree-sitter.js +0 -4007
- codegraphcontext/viz/dist/wasm/web-tree-sitter.wasm +0 -0
- codegraphcontext-0.4.18.dist-info/RECORD +0 -166
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.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
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
# src/codegraphcontext/api/mcp_sse.py
|
|
2
2
|
import json
|
|
3
3
|
import asyncio
|
|
4
|
-
|
|
4
|
+
import logging
|
|
5
|
+
import anyio
|
|
6
|
+
from fastapi import Request, Response
|
|
5
7
|
from mcp.server import Server
|
|
6
8
|
from mcp.server.models import InitializationOptions
|
|
7
9
|
from mcp.types import Tool, TextContent, ServerCapabilities, ToolsCapability
|
|
8
10
|
from mcp.server.sse import SseServerTransport
|
|
9
|
-
|
|
10
11
|
from codegraphcontext.api.router import get_server
|
|
11
|
-
from codegraphcontext.
|
|
12
|
+
from codegraphcontext.server import _strip_workspace_prefix, _apply_response_token_limit
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
12
15
|
|
|
13
16
|
# Create the MCP Server instance using the SDK
|
|
14
17
|
mcp_server = Server("CodeGraphContext")
|
|
15
18
|
|
|
16
19
|
@mcp_server.list_tools()
|
|
17
20
|
async def handle_list_tools() -> list[Tool]:
|
|
18
|
-
"""List available tools."""
|
|
21
|
+
"""List available tools (honors disabledTools from mcp.json)."""
|
|
22
|
+
server = get_server()
|
|
19
23
|
tools = []
|
|
20
|
-
for name, defn in
|
|
24
|
+
for name, defn in server.tools.items():
|
|
21
25
|
tools.append(Tool(
|
|
22
26
|
name=name,
|
|
23
27
|
description=defn["description"],
|
|
@@ -31,34 +35,71 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten
|
|
|
31
35
|
server = get_server()
|
|
32
36
|
args = arguments or {}
|
|
33
37
|
|
|
34
|
-
# Execute via the existing handler logic
|
|
35
38
|
result = await server.handle_tool_call(name, args)
|
|
39
|
+
result = _strip_workspace_prefix(result)
|
|
36
40
|
|
|
37
41
|
if "error" in result:
|
|
38
42
|
return [TextContent(type="text", text=f"Error: {result['error']}")]
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
# Create the SSE transport.
|
|
44
|
-
# The messages_url is where the client will POST JSON-RPC messages.
|
|
48
|
+
# Create the SSE transport.
|
|
45
49
|
sse = SseServerTransport("/api/v1/mcp/messages")
|
|
46
50
|
|
|
47
51
|
async def handle_sse(request: Request):
|
|
48
52
|
"""Entry point for the SSE connection."""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
logger.info("SSE client connected")
|
|
54
|
+
try:
|
|
55
|
+
async with sse.connect_sse(request.scope, request.receive, request._send) as (read_stream, write_stream):
|
|
56
|
+
await mcp_server.run(
|
|
57
|
+
read_stream,
|
|
58
|
+
write_stream,
|
|
59
|
+
InitializationOptions(
|
|
60
|
+
server_name="CodeGraphContext",
|
|
61
|
+
server_version="0.1.0",
|
|
62
|
+
capabilities=ServerCapabilities(
|
|
63
|
+
tools=ToolsCapability(listChanged=False)
|
|
64
|
+
)
|
|
58
65
|
)
|
|
59
66
|
)
|
|
60
|
-
|
|
67
|
+
except anyio.EndOfStream:
|
|
68
|
+
logger.debug("SSE client disconnected cleanly (stream ended)")
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
logger.debug("SSE connection closed: %s", type(exc).__name__)
|
|
71
|
+
finally:
|
|
72
|
+
logger.info("SSE client disconnected — handler exited, resources freed")
|
|
73
|
+
|
|
61
74
|
|
|
62
75
|
async def handle_messages(request: Request):
|
|
63
|
-
"""Endpoint for receiving messages from the client.
|
|
64
|
-
|
|
76
|
+
"""Endpoint for receiving messages from the client.
|
|
77
|
+
|
|
78
|
+
Uses a buffer framing collector to ensure the full JSON-RPC payload
|
|
79
|
+
is received before processing. This prevents crashes caused by large
|
|
80
|
+
responses being split across SSE line boundaries.
|
|
81
|
+
"""
|
|
82
|
+
# Buffer the complete request body before any parsing occurs.
|
|
83
|
+
# request.body() accumulates all chunks, preventing partial JSON reads.
|
|
84
|
+
raw_body = await request.body()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
json.loads(raw_body)
|
|
88
|
+
except json.JSONDecodeError as e:
|
|
89
|
+
return Response(
|
|
90
|
+
content=json.dumps({
|
|
91
|
+
"jsonrpc": "2.0",
|
|
92
|
+
"error": {
|
|
93
|
+
"code": -32700,
|
|
94
|
+
"message": f"Parse error: incomplete or malformed JSON-RPC message: {e}"
|
|
95
|
+
},
|
|
96
|
+
"id": None
|
|
97
|
+
}),
|
|
98
|
+
status_code=400,
|
|
99
|
+
media_type="application/json"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
await sse.handle_post_message(request.scope, request.receive, request._send)
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
logger.debug("Message handler closed: %s", type(exc).__name__)
|
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(
|
|
@@ -9,6 +9,7 @@ import time
|
|
|
9
9
|
import os
|
|
10
10
|
from typing import Optional, List, Dict, Any
|
|
11
11
|
import typer
|
|
12
|
+
from rich import box
|
|
12
13
|
from rich.console import Console
|
|
13
14
|
from rich.table import Table
|
|
14
15
|
from rich.progress import (
|
|
@@ -35,6 +36,7 @@ from .config_manager import (
|
|
|
35
36
|
register_repo_in_context,
|
|
36
37
|
ensure_first_run_bootstrap,
|
|
37
38
|
ContextNotFoundError,
|
|
39
|
+
is_db_deletion_allowed,
|
|
38
40
|
)
|
|
39
41
|
|
|
40
42
|
console = Console()
|
|
@@ -84,6 +86,42 @@ def _print_call_resolution_diagnostics(graph_builder: GraphBuilder, limit: int =
|
|
|
84
86
|
console.print(table)
|
|
85
87
|
|
|
86
88
|
|
|
89
|
+
def _format_extension_counts(files_by_extension: Dict[str, int]) -> str:
|
|
90
|
+
if not files_by_extension:
|
|
91
|
+
return "None"
|
|
92
|
+
return ", ".join(
|
|
93
|
+
f"{extension}: {count}" for extension, count in files_by_extension.items()
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _print_index_execution_summary(graph_builder: GraphBuilder) -> None:
|
|
98
|
+
summary = getattr(graph_builder, "last_index_summary", None)
|
|
99
|
+
if not summary:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
table = Table(
|
|
103
|
+
title="CGC Index Execution Summary",
|
|
104
|
+
show_header=True,
|
|
105
|
+
header_style="bold cyan",
|
|
106
|
+
box=box.ASCII,
|
|
107
|
+
)
|
|
108
|
+
table.add_column("Metric", style="cyan", no_wrap=True)
|
|
109
|
+
table.add_column("Value", style="green", overflow="fold")
|
|
110
|
+
table.add_row("Total scanned files", str(summary.get("total_scanned_files", 0)))
|
|
111
|
+
table.add_row(
|
|
112
|
+
"Files by extension",
|
|
113
|
+
_format_extension_counts(summary.get("files_by_extension", {})),
|
|
114
|
+
)
|
|
115
|
+
table.add_row("Function nodes", str(summary.get("function_nodes", 0)))
|
|
116
|
+
table.add_row("Class nodes", str(summary.get("class_nodes", 0)))
|
|
117
|
+
table.add_row("CALLS edges", str(summary.get("call_edges", 0)))
|
|
118
|
+
table.add_row(
|
|
119
|
+
"Serialization seconds",
|
|
120
|
+
f"{summary.get('serialization_seconds', 0.0):.2f}",
|
|
121
|
+
)
|
|
122
|
+
console.print(table)
|
|
123
|
+
|
|
124
|
+
|
|
87
125
|
def _initialize_services(
|
|
88
126
|
cli_context_flag: Optional[str] = None,
|
|
89
127
|
cwd: Optional[Path] = None,
|
|
@@ -314,6 +352,7 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
314
352
|
time_end = time.time()
|
|
315
353
|
elapsed = time_end - time_start
|
|
316
354
|
_print_call_resolution_diagnostics(graph_builder)
|
|
355
|
+
_print_index_execution_summary(graph_builder)
|
|
317
356
|
console.print(f"[green]Successfully finished indexing: {path} in {elapsed:.2f} seconds[/green]")
|
|
318
357
|
|
|
319
358
|
# Check if auto-watch is enabled
|
|
@@ -362,6 +401,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
362
401
|
try:
|
|
363
402
|
asyncio.run(_run_index_with_progress(graph_builder, package_path, is_dependency=True, cgcignore_path=ctx.cgcignore_path))
|
|
364
403
|
_print_call_resolution_diagnostics(graph_builder)
|
|
404
|
+
_print_index_execution_summary(graph_builder)
|
|
365
405
|
console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
|
|
366
406
|
except Exception as e:
|
|
367
407
|
console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
|
|
@@ -526,6 +566,7 @@ from ..viz.server import run_server, set_db_manager
|
|
|
526
566
|
|
|
527
567
|
def visualize_helper(
|
|
528
568
|
repo_path: Optional[str] = None,
|
|
569
|
+
host: str = "127.0.0.1",
|
|
529
570
|
port: int = 8000,
|
|
530
571
|
context: Optional[str] = None,
|
|
531
572
|
cypher_query: Optional[str] = None,
|
|
@@ -620,7 +661,7 @@ def visualize_helper(
|
|
|
620
661
|
threading.Thread(target=open_browser, daemon=True).start()
|
|
621
662
|
|
|
622
663
|
try:
|
|
623
|
-
run_server(host=
|
|
664
|
+
run_server(host=host, port=port, static_dir=str(static_dir))
|
|
624
665
|
except Exception as e:
|
|
625
666
|
console.print(f"[bold red]An error occurred while running the server:[/bold red] {e}")
|
|
626
667
|
raise typer.Exit(code=1)
|
|
@@ -665,6 +706,7 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
665
706
|
time_end = time.time()
|
|
666
707
|
elapsed = time_end - time_start
|
|
667
708
|
_print_call_resolution_diagnostics(graph_builder)
|
|
709
|
+
_print_index_execution_summary(graph_builder)
|
|
668
710
|
console.print(f"[green]Successfully re-indexed: {path} in {elapsed:.2f} seconds[/green]")
|
|
669
711
|
except Exception as e:
|
|
670
712
|
console.print(f"[bold red]An error occurred during re-indexing:[/bold red] {e}")
|
|
@@ -673,14 +715,32 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
673
715
|
db_manager.close_driver()
|
|
674
716
|
|
|
675
717
|
|
|
676
|
-
def update_helper(path: str, context: Optional[str] = None):
|
|
677
|
-
"""Update/refresh index for a path (alias for reindex).
|
|
718
|
+
def update_helper(path: str, context: Optional[str] = None, quiet: bool = False):
|
|
719
|
+
"""Update/refresh index for a path (alias for reindex).
|
|
720
|
+
|
|
721
|
+
When *quiet* is True (e.g. when invoked from Git hooks with --quiet),
|
|
722
|
+
Rich console output, including progress rendering, is suppressed.
|
|
723
|
+
"""
|
|
724
|
+
if quiet:
|
|
725
|
+
console.quiet = True
|
|
726
|
+
try:
|
|
727
|
+
reindex_helper(path, context)
|
|
728
|
+
finally:
|
|
729
|
+
console.quiet = False
|
|
730
|
+
return
|
|
678
731
|
console.print("[cyan]Updating repository index...[/cyan]")
|
|
679
732
|
reindex_helper(path, context)
|
|
680
733
|
|
|
681
734
|
|
|
682
735
|
def clean_helper(context: Optional[str] = None):
|
|
683
736
|
"""Remove orphaned nodes and relationships from the database."""
|
|
737
|
+
if not is_db_deletion_allowed():
|
|
738
|
+
console.print(
|
|
739
|
+
"[bold red]Error:[/bold red] Database cleanup is disabled. "
|
|
740
|
+
"Set ALLOW_DB_DELETION=true in config to enable."
|
|
741
|
+
)
|
|
742
|
+
raise typer.Exit(code=1)
|
|
743
|
+
|
|
684
744
|
services = _initialize_services(context)
|
|
685
745
|
if not all(services[:3]):
|
|
686
746
|
_fail_services_init()
|
|
@@ -738,6 +798,9 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
738
798
|
if path:
|
|
739
799
|
# Stats for specific repository
|
|
740
800
|
path_obj = Path(path).resolve()
|
|
801
|
+
# Paths are stored with forward slashes (as_posix) in the graph DB,
|
|
802
|
+
# so lookups must use the same normalization on Windows too.
|
|
803
|
+
repo_path_str = path_obj.as_posix()
|
|
741
804
|
console.print(f"[cyan]📊 Statistics for: {path_obj}[/cyan]\n")
|
|
742
805
|
|
|
743
806
|
with db_manager.get_driver().session() as session:
|
|
@@ -746,7 +809,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
746
809
|
MATCH (r:Repository {path: $path})
|
|
747
810
|
RETURN r
|
|
748
811
|
"""
|
|
749
|
-
result = session.run(repo_query, path=
|
|
812
|
+
result = session.run(repo_query, path=repo_path_str)
|
|
750
813
|
if not result.single():
|
|
751
814
|
console.print(f"[red]Repository not found: {path_obj}[/red]")
|
|
752
815
|
return
|
|
@@ -755,20 +818,20 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
755
818
|
# Get stats using separate queries to handle depth and avoid Cartesian products
|
|
756
819
|
# 1. Files
|
|
757
820
|
file_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(f) as c"
|
|
758
|
-
file_count = session.run(file_query, path=
|
|
821
|
+
file_count = session.run(file_query, path=repo_path_str).single()["c"]
|
|
759
822
|
|
|
760
823
|
# 2. Functions (including methods in classes)
|
|
761
824
|
func_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(func:Function) RETURN count(func) as c"
|
|
762
|
-
func_count = session.run(func_query, path=
|
|
825
|
+
func_count = session.run(func_query, path=repo_path_str).single()["c"]
|
|
763
826
|
|
|
764
827
|
# 3. Classes
|
|
765
828
|
class_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(c:Class) RETURN count(c) as c"
|
|
766
|
-
class_count = session.run(class_query, path=
|
|
829
|
+
class_count = session.run(class_query, path=repo_path_str).single()["c"]
|
|
767
830
|
|
|
768
831
|
# 4. Modules (imported) - Note: Module nodes are outside the repo structure usually, connected via IMPORTS
|
|
769
832
|
# We need to traverse from files to modules
|
|
770
833
|
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=
|
|
834
|
+
module_count = session.run(module_query, path=repo_path_str).single()["c"]
|
|
772
835
|
|
|
773
836
|
table = Table(show_header=True, header_style="bold magenta")
|
|
774
837
|
table.add_column("Metric", style="cyan")
|
|
@@ -828,7 +891,12 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
828
891
|
db_manager.close_driver()
|
|
829
892
|
|
|
830
893
|
|
|
831
|
-
def watch_helper(
|
|
894
|
+
def watch_helper(
|
|
895
|
+
path: str,
|
|
896
|
+
context: Optional[str] = None,
|
|
897
|
+
use_polling: Optional[bool] = None,
|
|
898
|
+
sync_on_start: bool = False,
|
|
899
|
+
):
|
|
832
900
|
"""Watch a directory for changes and auto-update the graph (blocking mode)."""
|
|
833
901
|
import logging
|
|
834
902
|
from ..core.watcher import CodeWatcher
|
|
@@ -869,7 +937,7 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
|
|
|
869
937
|
with code_finder.driver.session() as _s:
|
|
870
938
|
_r = _s.run(
|
|
871
939
|
"MATCH (n:File) WHERE n.path STARTS WITH $p RETURN count(n) AS c",
|
|
872
|
-
p=
|
|
940
|
+
p=path_obj.as_posix() + "/"
|
|
873
941
|
)
|
|
874
942
|
_count = _r.single()["c"]
|
|
875
943
|
if _count > 100:
|
|
@@ -891,11 +959,17 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
|
|
|
891
959
|
|
|
892
960
|
# Add the directory to watch
|
|
893
961
|
if is_indexed:
|
|
894
|
-
|
|
962
|
+
if sync_on_start:
|
|
963
|
+
console.print("[green]✓[/green] Already indexed. Synchronizing current files...")
|
|
964
|
+
else:
|
|
965
|
+
console.print("[green]✓[/green] Already indexed. Watching for future changes only.")
|
|
966
|
+
console.print(
|
|
967
|
+
"[dim]Use 'cgc index --force' or 'cgc watch --sync-on-start' to reconcile existing changes.[/dim]"
|
|
968
|
+
)
|
|
895
969
|
watcher.watch_directory(
|
|
896
970
|
str(path_obj),
|
|
897
971
|
perform_initial_scan=False,
|
|
898
|
-
sync_on_start=
|
|
972
|
+
sync_on_start=sync_on_start,
|
|
899
973
|
cgcignore_path=ctx.cgcignore_path,
|
|
900
974
|
)
|
|
901
975
|
else:
|
|
@@ -938,7 +1012,7 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
|
|
|
938
1012
|
finally:
|
|
939
1013
|
watcher.stop()
|
|
940
1014
|
db_manager.close_driver()
|
|
941
|
-
console.print("[green]✓[/green] Watcher stopped.
|
|
1015
|
+
console.print("[green]✓[/green] Watcher stopped.")
|
|
942
1016
|
|
|
943
1017
|
|
|
944
1018
|
|
|
@@ -8,12 +8,62 @@ Also manages the context system (config.yaml) alongside the existing .env file.
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Optional, Dict, Any, List
|
|
11
|
-
|
|
12
|
-
from rich.
|
|
11
|
+
try:
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
console = Console()
|
|
15
|
+
except Exception:
|
|
16
|
+
# Lightweight fallback when 'rich' is not installed (tests or minimal envs)
|
|
17
|
+
class _TableFallback:
|
|
18
|
+
def __init__(self, show_header=True, header_style=None):
|
|
19
|
+
self._cols = []
|
|
20
|
+
self._rows = []
|
|
21
|
+
|
|
22
|
+
def add_column(self, name, **_kwargs):
|
|
23
|
+
self._cols.append(name)
|
|
24
|
+
|
|
25
|
+
def add_row(self, *cells):
|
|
26
|
+
self._rows.append([str(c) for c in cells])
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
# Simple text table rendering
|
|
30
|
+
out = []
|
|
31
|
+
if self._cols:
|
|
32
|
+
out.append(" | ".join(self._cols))
|
|
33
|
+
out.append("-" * max(10, len(out[0])))
|
|
34
|
+
for r in self._rows:
|
|
35
|
+
out.append(" | ".join(r))
|
|
36
|
+
return "\n".join(out)
|
|
37
|
+
|
|
38
|
+
class _ConsoleFallback:
|
|
39
|
+
def print(self, *args, **kwargs):
|
|
40
|
+
# Mimic rich.console.Console.print by delegating to built-in print
|
|
41
|
+
end = kwargs.get("end", "\n")
|
|
42
|
+
sep = kwargs.get("sep", " ")
|
|
43
|
+
for a in args:
|
|
44
|
+
if isinstance(a, _TableFallback):
|
|
45
|
+
built = str(a)
|
|
46
|
+
print(built, end=end)
|
|
47
|
+
else:
|
|
48
|
+
print(a, end=end)
|
|
49
|
+
|
|
50
|
+
Table = _TableFallback
|
|
51
|
+
console = _ConsoleFallback()
|
|
13
52
|
import os
|
|
14
53
|
import yaml
|
|
15
54
|
|
|
16
|
-
|
|
55
|
+
|
|
56
|
+
def _atomic_write_text(path: Path, content: str, *, secure: bool = False) -> None:
|
|
57
|
+
"""Write *content* to *path* atomically (temp file + replace)."""
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
60
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
61
|
+
f.write(content)
|
|
62
|
+
f.flush()
|
|
63
|
+
os.fsync(f.fileno())
|
|
64
|
+
os.replace(tmp_path, path)
|
|
65
|
+
if secure:
|
|
66
|
+
os.chmod(path, 0o600)
|
|
17
67
|
|
|
18
68
|
# Configuration file location
|
|
19
69
|
CONFIG_DIR = Path.home() / ".codegraphcontext"
|
|
@@ -59,9 +109,14 @@ DEFAULT_CONFIG = {
|
|
|
59
109
|
# SCIP indexer feature flag (default off — existing Tree-sitter behaviour unchanged)
|
|
60
110
|
"SCIP_INDEXER": "false",
|
|
61
111
|
"SCIP_LANGUAGES": "python,typescript,javascript,go,rust,java,dart,cpp,c,csharp",
|
|
112
|
+
"SCIP_LOCAL_INDEXER_TIMEOUT_SECONDS": "300",
|
|
62
113
|
"SKIP_EXTERNAL_RESOLUTION": "false",
|
|
63
114
|
# 0 = unlimited; any positive integer caps MCP tool response size.
|
|
64
115
|
"MAX_TOOL_RESPONSE_TOKENS": "0",
|
|
116
|
+
# 0 = unlimited; any positive integer caps response size in raw characters.
|
|
117
|
+
# When both MAX_TOOL_RESPONSE_TOKENS and MAX_PROMPT_CHARS are set, the
|
|
118
|
+
# stricter (smaller) effective character budget wins.
|
|
119
|
+
"MAX_PROMPT_CHARS": "0",
|
|
65
120
|
# JSON object mapping tool names to integer result-count limits.
|
|
66
121
|
# Example: {"find_code": 20, "analyze_code_relationships": 10, "find_dead_code": 30}
|
|
67
122
|
"TOOL_RESULT_LIMITS": "{}",
|
|
@@ -72,6 +127,9 @@ DEFAULT_CONFIG = {
|
|
|
72
127
|
"CGC_EMBEDDING_BATCH_SIZE": "256",
|
|
73
128
|
# Default fuzzy matching behavior for `cgc find name` (overridable per-command with --fuzzy/--no-fuzzy)
|
|
74
129
|
"FUZZY_SEARCH": "true",
|
|
130
|
+
# Default LLM model names used for graph queries when no value is explicitly configured
|
|
131
|
+
"OPENAI_MODEL": "gpt-4o",
|
|
132
|
+
"ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022",
|
|
75
133
|
}
|
|
76
134
|
|
|
77
135
|
# Configuration key descriptions
|
|
@@ -100,8 +158,10 @@ CONFIG_DESCRIPTIONS = {
|
|
|
100
158
|
"INDEX_SOURCE": "Store full source code in graph database (for faster indexing use false, for better performance use true)",
|
|
101
159
|
"SCIP_INDEXER": "Use SCIP-based indexing for higher accuracy call/inheritance resolution (requires scip-<lang> tools installed)",
|
|
102
160
|
"SCIP_LANGUAGES": "Comma-separated languages to index via SCIP when SCIP_INDEXER=true (python,typescript,javascript,go,rust,java,dart,cpp,c,csharp)",
|
|
161
|
+
"SCIP_LOCAL_INDEXER_TIMEOUT_SECONDS": "Timeout in seconds for the local SCIP indexer subprocess (default 300). Raise it for large repositories whose indexer runs longer than 5 minutes; a value <= 0 or non-numeric falls back to 300.",
|
|
103
162
|
"SKIP_EXTERNAL_RESOLUTION": "Skip resolution attempts for external library method calls (recommended for enterprise large Java/Spring codebases)",
|
|
104
163
|
"MAX_TOOL_RESPONSE_TOKENS": "Maximum tokens per MCP tool response (0 = unlimited). Truncates oversized payloads and appends a notice.",
|
|
164
|
+
"MAX_PROMPT_CHARS": "Maximum characters per MCP tool response (0 = unlimited). When set alongside MAX_TOOL_RESPONSE_TOKENS the stricter limit wins. Truncated payloads receive a visible [CGC] notice and a stdout warning is emitted.",
|
|
105
165
|
"TOOL_RESULT_LIMITS": "JSON object mapping tool names to max result counts, e.g. {\"find_code\": 20, \"analyze_code_relationships\": 10}. Missing keys use built-in defaults.",
|
|
106
166
|
# Post-indexing resolution phases
|
|
107
167
|
"ENABLE_INHERIT_RESOLVE": (
|
|
@@ -138,6 +198,16 @@ CONFIG_DESCRIPTIONS = {
|
|
|
138
198
|
"Enable fuzzy matching by default for `cgc find name` (true|false). "
|
|
139
199
|
"Per-invocation overrides are available via --fuzzy / --no-fuzzy."
|
|
140
200
|
),
|
|
201
|
+
"OPENAI_MODEL": (
|
|
202
|
+
"Default OpenAI model used for graph queries. "
|
|
203
|
+
"Requires OPENAI_API_KEY environment variable. "
|
|
204
|
+
"Default: gpt-4o"
|
|
205
|
+
),
|
|
206
|
+
"ANTHROPIC_MODEL": (
|
|
207
|
+
"Default Anthropic model used for graph queries. "
|
|
208
|
+
"Requires ANTHROPIC_API_KEY environment variable. "
|
|
209
|
+
"Default: claude-3-5-sonnet-20241022"
|
|
210
|
+
),
|
|
141
211
|
}
|
|
142
212
|
|
|
143
213
|
# Valid values for each config key
|
|
@@ -197,6 +267,25 @@ coverage/
|
|
|
197
267
|
"""
|
|
198
268
|
|
|
199
269
|
|
|
270
|
+
def resolve_model_name(provider: str, configured_value: Optional[str] = None) -> str:
|
|
271
|
+
"""Get the model it should use for an LLM provider.
|
|
272
|
+
|
|
273
|
+
If the user configured a model, it uses that. Otherwise, it falls back to the default
|
|
274
|
+
model for the provider (like OPENAI_MODEL or ANTHROPIC_MODEL).
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
provider: The name of the provider, like "openai" or "anthropic" (case-insensitive).
|
|
278
|
+
configured_value: The model name from the user's config, if they set one.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
The model name it should use, or an empty string if it doesn't know the provider.
|
|
282
|
+
"""
|
|
283
|
+
if configured_value and configured_value.strip():
|
|
284
|
+
return configured_value.strip()
|
|
285
|
+
key = f"{provider.upper()}_MODEL"
|
|
286
|
+
return DEFAULT_CONFIG.get(key, "")
|
|
287
|
+
|
|
288
|
+
|
|
200
289
|
def normalize_config_path(value: str, *, absolute: bool = False, base_dir: Optional[Path] = None) -> str:
|
|
201
290
|
"""Normalize config path values.
|
|
202
291
|
|
|
@@ -385,30 +474,27 @@ def save_config(config: Dict[str, str], preserve_db_credentials: bool = True):
|
|
|
385
474
|
credentials_to_write[key] = config[key]
|
|
386
475
|
|
|
387
476
|
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
|
-
|
|
477
|
+
lines = [
|
|
478
|
+
"# CodeGraphContext Configuration",
|
|
479
|
+
f"# Location: {CONFIG_FILE}",
|
|
480
|
+
"",
|
|
481
|
+
]
|
|
482
|
+
if credentials_to_write:
|
|
483
|
+
lines.append("# ===== Database Credentials =====")
|
|
484
|
+
for key in sorted(DATABASE_CREDENTIAL_KEYS):
|
|
485
|
+
if key in credentials_to_write:
|
|
486
|
+
lines.append(f"{key}={credentials_to_write[key]}")
|
|
487
|
+
lines.append("")
|
|
488
|
+
lines.append("# ===== Configuration Settings =====")
|
|
489
|
+
for key, value in sorted(config.items()):
|
|
490
|
+
if key in DATABASE_CREDENTIAL_KEYS:
|
|
491
|
+
continue
|
|
492
|
+
description = CONFIG_DESCRIPTIONS.get(key, "")
|
|
493
|
+
if description:
|
|
494
|
+
lines.append(f"# {description}")
|
|
495
|
+
lines.append(f"{key}={value}")
|
|
496
|
+
lines.append("")
|
|
497
|
+
_atomic_write_text(CONFIG_FILE, "\n".join(lines), secure=True)
|
|
412
498
|
console.print(f"[green]✅ Configuration saved to {CONFIG_FILE}[/green]")
|
|
413
499
|
except Exception as e:
|
|
414
500
|
console.print(f"[red]Error saving config: {e}[/red]")
|
|
@@ -470,6 +556,14 @@ def validate_config_value(key: str, value: str) -> tuple[bool, Optional[str]]:
|
|
|
470
556
|
except ValueError:
|
|
471
557
|
return False, "MAX_TOOL_RESPONSE_TOKENS must be an integer (0 = unlimited)"
|
|
472
558
|
|
|
559
|
+
if key == "MAX_PROMPT_CHARS":
|
|
560
|
+
try:
|
|
561
|
+
limit = int(value)
|
|
562
|
+
if limit < 0:
|
|
563
|
+
return False, "MAX_PROMPT_CHARS must be 0 (unlimited) or a positive integer"
|
|
564
|
+
except ValueError:
|
|
565
|
+
return False, "MAX_PROMPT_CHARS must be an integer (0 = unlimited)"
|
|
566
|
+
|
|
473
567
|
if key == "TOOL_RESULT_LIMITS":
|
|
474
568
|
import json as _json
|
|
475
569
|
try:
|
|
@@ -717,10 +811,17 @@ def _default_global_db_path(database: str) -> str:
|
|
|
717
811
|
|
|
718
812
|
New layout: ``~/.codegraphcontext/global/db/<backend>/``
|
|
719
813
|
For backward-compat, we check:
|
|
720
|
-
1.
|
|
721
|
-
2.
|
|
722
|
-
3.
|
|
814
|
+
1. CGC_RUNTIME_DB_PATH environment variable (highest priority — works for all DBs)
|
|
815
|
+
2. FALKORDB_PATH in config (if database is falkordb)
|
|
816
|
+
3. Legacy flat path
|
|
817
|
+
4. New layout default
|
|
723
818
|
"""
|
|
819
|
+
# Generic env var override — highest priority for any backend.
|
|
820
|
+
# Useful for relocating the global DB to a path that avoids platform-specific
|
|
821
|
+
# encoding issues (e.g. non-ASCII characters in the Windows user profile path).
|
|
822
|
+
env_override = os.environ.get("CGC_RUNTIME_DB_PATH")
|
|
823
|
+
if env_override:
|
|
824
|
+
return env_override
|
|
724
825
|
if database == "falkordb":
|
|
725
826
|
custom_path = load_config().get("FALKORDB_PATH")
|
|
726
827
|
if custom_path:
|
|
@@ -804,8 +905,10 @@ def save_context_config(cfg: ContextConfig) -> None:
|
|
|
804
905
|
}
|
|
805
906
|
|
|
806
907
|
try:
|
|
807
|
-
|
|
808
|
-
|
|
908
|
+
_atomic_write_text(
|
|
909
|
+
CONTEXT_CONFIG_FILE,
|
|
910
|
+
yaml.dump(raw, default_flow_style=False, sort_keys=False),
|
|
911
|
+
)
|
|
809
912
|
except Exception as e:
|
|
810
913
|
console.print(f"[red]Error saving config.yaml: {e}[/red]")
|
|
811
914
|
|
|
@@ -887,10 +990,15 @@ def resolve_context(
|
|
|
887
990
|
|
|
888
991
|
inherited_db = load_config().get("DEFAULT_DATABASE", "falkordb")
|
|
889
992
|
|
|
890
|
-
# Copy global .env into local context for easy per-repo tweaking
|
|
993
|
+
# Copy global .env into local context for easy per-repo tweaking.
|
|
994
|
+
# Guard against the self-copy case: when cwd is the home directory,
|
|
995
|
+
# local_cgc resolves to CONFIG_DIR itself, so `local_cgc / ".env"` is
|
|
996
|
+
# CONFIG_FILE. Copying a file onto itself raises shutil.SameFileError,
|
|
997
|
+
# which crashes resolve_context for any session started from home.
|
|
891
998
|
import shutil
|
|
892
|
-
|
|
893
|
-
|
|
999
|
+
_target_env = local_cgc / ".env"
|
|
1000
|
+
if CONFIG_FILE.exists() and _target_env.resolve() != CONFIG_FILE.resolve():
|
|
1001
|
+
shutil.copy2(CONFIG_FILE, _target_env)
|
|
894
1002
|
|
|
895
1003
|
local_yaml = local_cgc / "config.yaml"
|
|
896
1004
|
if not local_yaml.exists():
|
|
@@ -905,7 +1013,11 @@ def resolve_context(
|
|
|
905
1013
|
if local_cgc is not None:
|
|
906
1014
|
# Read local config.yaml if present
|
|
907
1015
|
local_yaml = local_cgc / "config.yaml"
|
|
908
|
-
local_db =
|
|
1016
|
+
local_db = (
|
|
1017
|
+
os.environ.get("CGC_RUNTIME_DB_TYPE")
|
|
1018
|
+
or os.environ.get("DEFAULT_DATABASE")
|
|
1019
|
+
or load_config().get("DEFAULT_DATABASE", "falkordb")
|
|
1020
|
+
)
|
|
909
1021
|
if local_yaml.exists():
|
|
910
1022
|
try:
|
|
911
1023
|
with open(local_yaml, encoding="utf-8") as f:
|
|
@@ -1172,8 +1284,10 @@ def _save_workspace_mappings(mappings: Dict[str, Dict[str, str]]) -> None:
|
|
|
1172
1284
|
raw = {}
|
|
1173
1285
|
raw["workspace_mappings"] = mappings
|
|
1174
1286
|
try:
|
|
1175
|
-
|
|
1176
|
-
|
|
1287
|
+
_atomic_write_text(
|
|
1288
|
+
CONTEXT_CONFIG_FILE,
|
|
1289
|
+
yaml.dump(raw, default_flow_style=False, sort_keys=False),
|
|
1290
|
+
)
|
|
1177
1291
|
except Exception as e:
|
|
1178
1292
|
console.print(f"[red]Error saving workspace mappings: {e}[/red]")
|
|
1179
1293
|
|