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.
Files changed (126) hide show
  1. codegraphcontext/api/app.py +4 -1
  2. codegraphcontext/api/mcp_sse.py +63 -22
  3. codegraphcontext/api/router.py +3 -6
  4. codegraphcontext/cli/cli_helpers.py +87 -13
  5. codegraphcontext/cli/config_manager.py +152 -38
  6. codegraphcontext/cli/hook_manager.py +1 -1
  7. codegraphcontext/cli/main.py +172 -42
  8. codegraphcontext/cli/registry_commands.py +11 -4
  9. codegraphcontext/cli/setup_macos.py +6 -1
  10. codegraphcontext/cli/setup_wizard.py +61 -56
  11. codegraphcontext/cli/visualizer.py +25 -1
  12. codegraphcontext/core/__init__.py +71 -10
  13. codegraphcontext/core/cgc_bundle.py +1 -1
  14. codegraphcontext/core/cgcignore.py +4 -1
  15. codegraphcontext/core/database_embedded_kuzu.py +1265 -0
  16. codegraphcontext/core/database_falkordb.py +41 -2
  17. codegraphcontext/core/database_kuzu.py +37 -1163
  18. codegraphcontext/core/database_ladybug.py +37 -1145
  19. codegraphcontext/core/graph_query.py +45 -0
  20. codegraphcontext/core/jobs.py +7 -2
  21. codegraphcontext/core/watcher.py +114 -188
  22. codegraphcontext/prompts.py +1 -1
  23. codegraphcontext/server.py +111 -19
  24. codegraphcontext/stdlibs.py +18 -0
  25. codegraphcontext/tool_definitions.py +188 -79
  26. codegraphcontext/tools/code_finder.py +33 -19
  27. codegraphcontext/tools/graph_builder.py +266 -10
  28. codegraphcontext/tools/handlers/analysis_handlers.py +5 -0
  29. codegraphcontext/tools/handlers/indexing_handlers.py +4 -1
  30. codegraphcontext/tools/handlers/management_handlers.py +9 -10
  31. codegraphcontext/tools/handlers/query_handlers.py +21 -3
  32. codegraphcontext/tools/handlers/watcher_handlers.py +2 -2
  33. codegraphcontext/tools/indexing/embeddings.py +63 -49
  34. codegraphcontext/tools/indexing/persistence/writer.py +437 -49
  35. codegraphcontext/tools/indexing/pipeline.py +66 -1
  36. codegraphcontext/tools/indexing/resolution/__init__.py +11 -1
  37. codegraphcontext/tools/indexing/resolution/calls.py +527 -70
  38. codegraphcontext/tools/indexing/resolution/inheritance.py +448 -4
  39. codegraphcontext/tools/indexing/resolution/post_resolution.py +40 -15
  40. codegraphcontext/tools/indexing/schema.py +5 -0
  41. codegraphcontext/tools/indexing/schema_contract.py +7 -0
  42. codegraphcontext/tools/indexing/scip_pipeline.py +25 -10
  43. codegraphcontext/tools/indexing/vector_resolver.py +72 -57
  44. codegraphcontext/tools/languages/c.py +154 -2
  45. codegraphcontext/tools/languages/cpp.py +13 -1
  46. codegraphcontext/tools/languages/csharp.py +1 -0
  47. codegraphcontext/tools/languages/css.py +13 -4
  48. codegraphcontext/tools/languages/dart.py +170 -41
  49. codegraphcontext/tools/languages/elixir.py +9 -6
  50. codegraphcontext/tools/languages/go.py +56 -16
  51. codegraphcontext/tools/languages/haskell.py +80 -8
  52. codegraphcontext/tools/languages/html.py +23 -8
  53. codegraphcontext/tools/languages/javascript.py +2 -2
  54. codegraphcontext/tools/languages/lua.py +61 -19
  55. codegraphcontext/tools/languages/perl.py +37 -4
  56. codegraphcontext/tools/languages/php.py +56 -4
  57. codegraphcontext/tools/languages/python.py +13 -3
  58. codegraphcontext/tools/languages/rust.py +70 -6
  59. codegraphcontext/tools/languages/swift.py +61 -11
  60. codegraphcontext/tools/languages/typescript.py +63 -6
  61. codegraphcontext/tools/package_resolver.py +159 -364
  62. codegraphcontext/tools/query_tool_languages/c_toolkit.py +1 -1
  63. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +2 -2
  64. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +2 -2
  65. codegraphcontext/tools/query_tool_languages/go_toolkit.py +1 -1
  66. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +3 -3
  67. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +1 -1
  68. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +2 -2
  69. codegraphcontext/tools/query_tool_languages/python_toolkit.py +1 -1
  70. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +1 -1
  71. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +1 -1
  72. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +2 -2
  73. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +2 -2
  74. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +1 -1
  75. codegraphcontext/tools/scip_indexer.py +45 -3
  76. codegraphcontext/tools/system.py +8 -1
  77. codegraphcontext/tools/tree_sitter_parser.py +41 -96
  78. codegraphcontext/utils/debug_log.py +5 -0
  79. codegraphcontext/utils/gcf_encoder.py +58 -0
  80. codegraphcontext/utils/visualize_graph.py +461 -0
  81. codegraphcontext/viz/server.py +348 -32
  82. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/METADATA +201 -23
  83. codegraphcontext-0.5.2.dist-info/RECORD +132 -0
  84. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/WHEEL +1 -1
  85. codegraphcontext/viz/dist/assets/__vite-browser-external-9wXp6ZBx.js +0 -1
  86. codegraphcontext/viz/dist/assets/function-calls-BtRHrqa2.png +0 -0
  87. codegraphcontext/viz/dist/assets/graph-total-D1fBAugo.png +0 -0
  88. codegraphcontext/viz/dist/assets/hero-graph-2voMJp2a.jpg +0 -0
  89. codegraphcontext/viz/dist/assets/hierarchy-DGADo0YT.png +0 -0
  90. codegraphcontext/viz/dist/assets/index-C-187lf0.js +0 -5587
  91. codegraphcontext/viz/dist/assets/index-fNAa6jgv.css +0 -1
  92. codegraphcontext/viz/dist/assets/parser-pyodide.worker-BgsDfaad.js +0 -370
  93. codegraphcontext/viz/dist/assets/parser.worker-_nvrecvj.js +0 -233
  94. codegraphcontext/viz/dist/assets/tree-sitter-qKYAACSa.wasm +0 -0
  95. codegraphcontext/viz/dist/cgcIcon.png +0 -0
  96. codegraphcontext/viz/dist/favicon.ico +0 -0
  97. codegraphcontext/viz/dist/index.html +0 -32
  98. codegraphcontext/viz/dist/logo-icon.svg +0 -85
  99. codegraphcontext/viz/dist/logo.svg +0 -100
  100. codegraphcontext/viz/dist/placeholder.svg +0 -1
  101. codegraphcontext/viz/dist/preview-image.png +0 -0
  102. codegraphcontext/viz/dist/robots.txt +0 -14
  103. codegraphcontext/viz/dist/wasm/tree-sitter-c.wasm +0 -0
  104. codegraphcontext/viz/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  105. codegraphcontext/viz/dist/wasm/tree-sitter-core.js +0 -1
  106. codegraphcontext/viz/dist/wasm/tree-sitter-cpp.wasm +0 -0
  107. codegraphcontext/viz/dist/wasm/tree-sitter-dart.wasm +0 -0
  108. codegraphcontext/viz/dist/wasm/tree-sitter-go.wasm +0 -0
  109. codegraphcontext/viz/dist/wasm/tree-sitter-java.wasm +0 -0
  110. codegraphcontext/viz/dist/wasm/tree-sitter-javascript.wasm +0 -0
  111. codegraphcontext/viz/dist/wasm/tree-sitter-kotlin.wasm +0 -0
  112. codegraphcontext/viz/dist/wasm/tree-sitter-perl.wasm +0 -1
  113. codegraphcontext/viz/dist/wasm/tree-sitter-php.wasm +0 -0
  114. codegraphcontext/viz/dist/wasm/tree-sitter-python.wasm +0 -0
  115. codegraphcontext/viz/dist/wasm/tree-sitter-ruby.wasm +0 -0
  116. codegraphcontext/viz/dist/wasm/tree-sitter-rust.wasm +0 -0
  117. codegraphcontext/viz/dist/wasm/tree-sitter-swift.wasm +0 -0
  118. codegraphcontext/viz/dist/wasm/tree-sitter-tsx.wasm +0 -0
  119. codegraphcontext/viz/dist/wasm/tree-sitter-typescript.wasm +0 -0
  120. codegraphcontext/viz/dist/wasm/tree-sitter.wasm +0 -0
  121. codegraphcontext/viz/dist/wasm/web-tree-sitter.js +0 -4007
  122. codegraphcontext/viz/dist/wasm/web-tree-sitter.wasm +0 -0
  123. codegraphcontext-0.4.18.dist-info/RECORD +0 -166
  124. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/entry_points.txt +0 -0
  125. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/licenses/LICENSE +0 -0
  126. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/top_level.txt +0 -0
@@ -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
- allow_credentials=True,
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
  )
@@ -1,23 +1,27 @@
1
1
  # src/codegraphcontext/api/mcp_sse.py
2
2
  import json
3
3
  import asyncio
4
- from fastapi import Request
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.tool_definitions import TOOLS
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 TOOLS.items():
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
- # Format result as JSON string for the AI
41
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
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
- async with sse.connect_sse(request.scope, request.receive, request._send) as (read_stream, write_stream):
50
- await mcp_server.run(
51
- read_stream,
52
- write_stream,
53
- InitializationOptions(
54
- server_name="CodeGraphContext",
55
- server_version="0.1.0",
56
- capabilities=ServerCapabilities(
57
- tools=ToolsCapability(listChanged=False)
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
- await sse.handle_post_message(request.scope, request.receive, request._send)
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__)
@@ -91,12 +91,9 @@ async def index_repository(
91
91
  background_tasks: BackgroundTasks,
92
92
  server: MCPServer = Depends(get_server)
93
93
  ):
94
- args = {
95
- "path": request.path,
96
- "repo_name": request.repo_name,
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="127.0.0.1", port=port, static_dir=str(static_dir))
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=str(path_obj))
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=str(path_obj)).single()["c"]
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=str(path_obj)).single()["c"]
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=str(path_obj)).single()["c"]
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=str(path_obj)).single()["c"]
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(path: str, context: Optional[str] = None, use_polling: Optional[bool] = None):
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=str(path_obj) + "/"
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
- console.print("[green]✓[/green] Already indexed. Synchronizing current files...")
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=True,
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. Graph is up to date.")
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
- from rich.console import Console
12
- from rich.table import Table
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
- console = Console()
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
- with open(CONFIG_FILE, "w", encoding="utf-8") as f:
389
- f.write("# CodeGraphContext Configuration\n")
390
- f.write(f"# Location: {CONFIG_FILE}\n\n")
391
-
392
- # Write database credentials first if they exist
393
- if credentials_to_write:
394
- f.write("# ===== Database Credentials =====\n")
395
- for key in sorted(DATABASE_CREDENTIAL_KEYS):
396
- if key in credentials_to_write:
397
- f.write(f"{key}={credentials_to_write[key]}\n")
398
- f.write("\n")
399
-
400
- # Write configuration settings
401
- f.write("# ===== Configuration Settings =====\n")
402
- for key, value in sorted(config.items()):
403
- # Skip database credentials (already written above)
404
- if key in DATABASE_CREDENTIAL_KEYS:
405
- continue
406
-
407
- description = CONFIG_DESCRIPTIONS.get(key, "")
408
- if description:
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. FALKORDB_PATH in config (if database is falkordb)
721
- 2. Legacy flat path
722
- 3. New layout default
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
- with open(CONTEXT_CONFIG_FILE, "w", encoding="utf-8") as f:
808
- yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
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
- if CONFIG_FILE.exists():
893
- shutil.copy2(CONFIG_FILE, local_cgc / ".env")
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 = load_config().get("DEFAULT_DATABASE", "falkordb")
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
- with open(CONTEXT_CONFIG_FILE, "w", encoding="utf-8") as f:
1176
- yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
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