codegraphcontext 0.5.1__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 (85) hide show
  1. codegraphcontext/api/mcp_sse.py +55 -19
  2. codegraphcontext/cli/cli_helpers.py +57 -5
  3. codegraphcontext/cli/config_manager.py +103 -8
  4. codegraphcontext/cli/main.py +80 -11
  5. codegraphcontext/cli/setup_wizard.py +61 -56
  6. codegraphcontext/core/cgcignore.py +4 -1
  7. codegraphcontext/core/database_embedded_kuzu.py +1265 -0
  8. codegraphcontext/core/database_kuzu.py +37 -1203
  9. codegraphcontext/core/database_ladybug.py +37 -1177
  10. codegraphcontext/core/graph_query.py +45 -0
  11. codegraphcontext/core/watcher.py +100 -31
  12. codegraphcontext/server.py +57 -14
  13. codegraphcontext/tools/code_finder.py +16 -19
  14. codegraphcontext/tools/graph_builder.py +6 -2
  15. codegraphcontext/tools/indexing/embeddings.py +63 -49
  16. codegraphcontext/tools/indexing/persistence/writer.py +116 -58
  17. codegraphcontext/tools/indexing/pipeline.py +44 -0
  18. codegraphcontext/tools/indexing/resolution/calls.py +54 -29
  19. codegraphcontext/tools/indexing/resolution/inheritance.py +9 -1
  20. codegraphcontext/tools/indexing/scip_pipeline.py +25 -10
  21. codegraphcontext/tools/indexing/vector_resolver.py +72 -57
  22. codegraphcontext/tools/languages/python.py +2 -0
  23. codegraphcontext/tools/query_tool_languages/c_toolkit.py +1 -1
  24. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +2 -2
  25. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +2 -2
  26. codegraphcontext/tools/query_tool_languages/go_toolkit.py +1 -1
  27. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +3 -3
  28. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +1 -1
  29. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +2 -2
  30. codegraphcontext/tools/query_tool_languages/python_toolkit.py +1 -1
  31. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +1 -1
  32. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +1 -1
  33. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +2 -2
  34. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +2 -2
  35. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +1 -1
  36. codegraphcontext/tools/scip_indexer.py +45 -3
  37. codegraphcontext/tools/tree_sitter_parser.py +41 -96
  38. codegraphcontext/utils/debug_log.py +5 -0
  39. codegraphcontext/utils/gcf_encoder.py +58 -0
  40. codegraphcontext/utils/visualize_graph.py +461 -0
  41. codegraphcontext/viz/server.py +348 -32
  42. {codegraphcontext-0.5.1.dist-info → codegraphcontext-0.5.2.dist-info}/METADATA +190 -18
  43. {codegraphcontext-0.5.1.dist-info → codegraphcontext-0.5.2.dist-info}/RECORD +47 -82
  44. {codegraphcontext-0.5.1.dist-info → codegraphcontext-0.5.2.dist-info}/WHEEL +1 -1
  45. codegraphcontext/viz/dist/assets/__vite-browser-external-9wXp6ZBx.js +0 -1
  46. codegraphcontext/viz/dist/assets/function-calls-BtRHrqa2.png +0 -0
  47. codegraphcontext/viz/dist/assets/graph-total-D1fBAugo.png +0 -0
  48. codegraphcontext/viz/dist/assets/hero-graph-2voMJp2a.jpg +0 -0
  49. codegraphcontext/viz/dist/assets/hierarchy-DGADo0YT.png +0 -0
  50. codegraphcontext/viz/dist/assets/index-C-187lf0.js +0 -5587
  51. codegraphcontext/viz/dist/assets/index-fNAa6jgv.css +0 -1
  52. codegraphcontext/viz/dist/assets/parser-pyodide.worker-BgsDfaad.js +0 -370
  53. codegraphcontext/viz/dist/assets/parser.worker-_nvrecvj.js +0 -233
  54. codegraphcontext/viz/dist/assets/tree-sitter-qKYAACSa.wasm +0 -0
  55. codegraphcontext/viz/dist/cgcIcon.png +0 -0
  56. codegraphcontext/viz/dist/favicon.ico +0 -0
  57. codegraphcontext/viz/dist/index.html +0 -32
  58. codegraphcontext/viz/dist/logo-icon.svg +0 -85
  59. codegraphcontext/viz/dist/logo.svg +0 -100
  60. codegraphcontext/viz/dist/placeholder.svg +0 -1
  61. codegraphcontext/viz/dist/preview-image.png +0 -0
  62. codegraphcontext/viz/dist/robots.txt +0 -14
  63. codegraphcontext/viz/dist/wasm/tree-sitter-c.wasm +0 -0
  64. codegraphcontext/viz/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  65. codegraphcontext/viz/dist/wasm/tree-sitter-core.js +0 -1
  66. codegraphcontext/viz/dist/wasm/tree-sitter-cpp.wasm +0 -0
  67. codegraphcontext/viz/dist/wasm/tree-sitter-dart.wasm +0 -0
  68. codegraphcontext/viz/dist/wasm/tree-sitter-go.wasm +0 -0
  69. codegraphcontext/viz/dist/wasm/tree-sitter-java.wasm +0 -0
  70. codegraphcontext/viz/dist/wasm/tree-sitter-javascript.wasm +0 -0
  71. codegraphcontext/viz/dist/wasm/tree-sitter-kotlin.wasm +0 -0
  72. codegraphcontext/viz/dist/wasm/tree-sitter-perl.wasm +0 -1
  73. codegraphcontext/viz/dist/wasm/tree-sitter-php.wasm +0 -0
  74. codegraphcontext/viz/dist/wasm/tree-sitter-python.wasm +0 -0
  75. codegraphcontext/viz/dist/wasm/tree-sitter-ruby.wasm +0 -0
  76. codegraphcontext/viz/dist/wasm/tree-sitter-rust.wasm +0 -0
  77. codegraphcontext/viz/dist/wasm/tree-sitter-swift.wasm +0 -0
  78. codegraphcontext/viz/dist/wasm/tree-sitter-tsx.wasm +0 -0
  79. codegraphcontext/viz/dist/wasm/tree-sitter-typescript.wasm +0 -0
  80. codegraphcontext/viz/dist/wasm/tree-sitter.wasm +0 -0
  81. codegraphcontext/viz/dist/wasm/web-tree-sitter.js +0 -4007
  82. codegraphcontext/viz/dist/wasm/web-tree-sitter.wasm +0 -0
  83. {codegraphcontext-0.5.1.dist-info → codegraphcontext-0.5.2.dist-info}/entry_points.txt +0 -0
  84. {codegraphcontext-0.5.1.dist-info → codegraphcontext-0.5.2.dist-info}/licenses/LICENSE +0 -0
  85. {codegraphcontext-0.5.1.dist-info → codegraphcontext-0.5.2.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,18 @@
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
12
  from codegraphcontext.server import _strip_workspace_prefix, _apply_response_token_limit
12
13
 
14
+ logger = logging.getLogger(__name__)
15
+
13
16
  # Create the MCP Server instance using the SDK
14
17
  mcp_server = Server("CodeGraphContext")
15
18
 
@@ -32,38 +35,71 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten
32
35
  server = get_server()
33
36
  args = arguments or {}
34
37
 
35
- # Execute via the existing handler logic
36
38
  result = await server.handle_tool_call(name, args)
37
39
  result = _strip_workspace_prefix(result)
38
40
 
39
41
  if "error" in result:
40
42
  return [TextContent(type="text", text=f"Error: {result['error']}")]
41
43
 
42
- # Format result as JSON string for the AI, with the same token budget
43
- # the stdio transport applies.
44
44
  response_text = json.dumps(result, indent=2)
45
45
  response_text = _apply_response_token_limit(name, response_text)
46
46
  return [TextContent(type="text", text=response_text)]
47
47
 
48
- # Create the SSE transport.
49
- # The messages_url is where the client will POST JSON-RPC messages.
48
+ # Create the SSE transport.
50
49
  sse = SseServerTransport("/api/v1/mcp/messages")
51
50
 
52
51
  async def handle_sse(request: Request):
53
52
  """Entry point for the SSE connection."""
54
- async with sse.connect_sse(request.scope, request.receive, request._send) as (read_stream, write_stream):
55
- await mcp_server.run(
56
- read_stream,
57
- write_stream,
58
- InitializationOptions(
59
- server_name="CodeGraphContext",
60
- server_version="0.1.0",
61
- capabilities=ServerCapabilities(
62
- 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
+ )
63
65
  )
64
66
  )
65
- )
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
+
66
74
 
67
75
  async def handle_messages(request: Request):
68
- """Endpoint for receiving messages from the client."""
69
- 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__)
@@ -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 (
@@ -85,6 +86,42 @@ def _print_call_resolution_diagnostics(graph_builder: GraphBuilder, limit: int =
85
86
  console.print(table)
86
87
 
87
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
+
88
125
  def _initialize_services(
89
126
  cli_context_flag: Optional[str] = None,
90
127
  cwd: Optional[Path] = None,
@@ -315,6 +352,7 @@ def index_helper(path: str, context: Optional[str] = None):
315
352
  time_end = time.time()
316
353
  elapsed = time_end - time_start
317
354
  _print_call_resolution_diagnostics(graph_builder)
355
+ _print_index_execution_summary(graph_builder)
318
356
  console.print(f"[green]Successfully finished indexing: {path} in {elapsed:.2f} seconds[/green]")
319
357
 
320
358
  # Check if auto-watch is enabled
@@ -363,6 +401,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
363
401
  try:
364
402
  asyncio.run(_run_index_with_progress(graph_builder, package_path, is_dependency=True, cgcignore_path=ctx.cgcignore_path))
365
403
  _print_call_resolution_diagnostics(graph_builder)
404
+ _print_index_execution_summary(graph_builder)
366
405
  console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
367
406
  except Exception as e:
368
407
  console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
@@ -527,6 +566,7 @@ from ..viz.server import run_server, set_db_manager
527
566
 
528
567
  def visualize_helper(
529
568
  repo_path: Optional[str] = None,
569
+ host: str = "127.0.0.1",
530
570
  port: int = 8000,
531
571
  context: Optional[str] = None,
532
572
  cypher_query: Optional[str] = None,
@@ -621,7 +661,7 @@ def visualize_helper(
621
661
  threading.Thread(target=open_browser, daemon=True).start()
622
662
 
623
663
  try:
624
- 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))
625
665
  except Exception as e:
626
666
  console.print(f"[bold red]An error occurred while running the server:[/bold red] {e}")
627
667
  raise typer.Exit(code=1)
@@ -666,6 +706,7 @@ def reindex_helper(path: str, context: Optional[str] = None):
666
706
  time_end = time.time()
667
707
  elapsed = time_end - time_start
668
708
  _print_call_resolution_diagnostics(graph_builder)
709
+ _print_index_execution_summary(graph_builder)
669
710
  console.print(f"[green]Successfully re-indexed: {path} in {elapsed:.2f} seconds[/green]")
670
711
  except Exception as e:
671
712
  console.print(f"[bold red]An error occurred during re-indexing:[/bold red] {e}")
@@ -850,7 +891,12 @@ def stats_helper(path: str = None, context: Optional[str] = None):
850
891
  db_manager.close_driver()
851
892
 
852
893
 
853
- 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
+ ):
854
900
  """Watch a directory for changes and auto-update the graph (blocking mode)."""
855
901
  import logging
856
902
  from ..core.watcher import CodeWatcher
@@ -913,11 +959,17 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
913
959
 
914
960
  # Add the directory to watch
915
961
  if is_indexed:
916
- 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
+ )
917
969
  watcher.watch_directory(
918
970
  str(path_obj),
919
971
  perform_initial_scan=False,
920
- sync_on_start=True,
972
+ sync_on_start=sync_on_start,
921
973
  cgcignore_path=ctx.cgcignore_path,
922
974
  )
923
975
  else:
@@ -960,7 +1012,7 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
960
1012
  finally:
961
1013
  watcher.stop()
962
1014
  db_manager.close_driver()
963
- console.print("[green]✓[/green] Watcher stopped. Graph is up to date.")
1015
+ console.print("[green]✓[/green] Watcher stopped.")
964
1016
 
965
1017
 
966
1018
 
@@ -8,13 +8,50 @@ 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()
17
-
18
55
 
19
56
  def _atomic_write_text(path: Path, content: str, *, secure: bool = False) -> None:
20
57
  """Write *content* to *path* atomically (temp file + replace)."""
@@ -72,9 +109,14 @@ DEFAULT_CONFIG = {
72
109
  # SCIP indexer feature flag (default off — existing Tree-sitter behaviour unchanged)
73
110
  "SCIP_INDEXER": "false",
74
111
  "SCIP_LANGUAGES": "python,typescript,javascript,go,rust,java,dart,cpp,c,csharp",
112
+ "SCIP_LOCAL_INDEXER_TIMEOUT_SECONDS": "300",
75
113
  "SKIP_EXTERNAL_RESOLUTION": "false",
76
114
  # 0 = unlimited; any positive integer caps MCP tool response size.
77
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",
78
120
  # JSON object mapping tool names to integer result-count limits.
79
121
  # Example: {"find_code": 20, "analyze_code_relationships": 10, "find_dead_code": 30}
80
122
  "TOOL_RESULT_LIMITS": "{}",
@@ -85,6 +127,9 @@ DEFAULT_CONFIG = {
85
127
  "CGC_EMBEDDING_BATCH_SIZE": "256",
86
128
  # Default fuzzy matching behavior for `cgc find name` (overridable per-command with --fuzzy/--no-fuzzy)
87
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",
88
133
  }
89
134
 
90
135
  # Configuration key descriptions
@@ -113,8 +158,10 @@ CONFIG_DESCRIPTIONS = {
113
158
  "INDEX_SOURCE": "Store full source code in graph database (for faster indexing use false, for better performance use true)",
114
159
  "SCIP_INDEXER": "Use SCIP-based indexing for higher accuracy call/inheritance resolution (requires scip-<lang> tools installed)",
115
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.",
116
162
  "SKIP_EXTERNAL_RESOLUTION": "Skip resolution attempts for external library method calls (recommended for enterprise large Java/Spring codebases)",
117
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.",
118
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.",
119
166
  # Post-indexing resolution phases
120
167
  "ENABLE_INHERIT_RESOLVE": (
@@ -151,6 +198,16 @@ CONFIG_DESCRIPTIONS = {
151
198
  "Enable fuzzy matching by default for `cgc find name` (true|false). "
152
199
  "Per-invocation overrides are available via --fuzzy / --no-fuzzy."
153
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
+ ),
154
211
  }
155
212
 
156
213
  # Valid values for each config key
@@ -210,6 +267,25 @@ coverage/
210
267
  """
211
268
 
212
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
+
213
289
  def normalize_config_path(value: str, *, absolute: bool = False, base_dir: Optional[Path] = None) -> str:
214
290
  """Normalize config path values.
215
291
 
@@ -480,6 +556,14 @@ def validate_config_value(key: str, value: str) -> tuple[bool, Optional[str]]:
480
556
  except ValueError:
481
557
  return False, "MAX_TOOL_RESPONSE_TOKENS must be an integer (0 = unlimited)"
482
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
+
483
567
  if key == "TOOL_RESULT_LIMITS":
484
568
  import json as _json
485
569
  try:
@@ -727,10 +811,17 @@ def _default_global_db_path(database: str) -> str:
727
811
 
728
812
  New layout: ``~/.codegraphcontext/global/db/<backend>/``
729
813
  For backward-compat, we check:
730
- 1. FALKORDB_PATH in config (if database is falkordb)
731
- 2. Legacy flat path
732
- 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
733
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
734
825
  if database == "falkordb":
735
826
  custom_path = load_config().get("FALKORDB_PATH")
736
827
  if custom_path:
@@ -922,7 +1013,11 @@ def resolve_context(
922
1013
  if local_cgc is not None:
923
1014
  # Read local config.yaml if present
924
1015
  local_yaml = local_cgc / "config.yaml"
925
- 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
+ )
926
1021
  if local_yaml.exists():
927
1022
  try:
928
1023
  with open(local_yaml, encoding="utf-8") as f:
@@ -1295,27 +1295,61 @@ def doctor():
1295
1295
 
1296
1296
 
1297
1297
 
1298
+ @app.command()
1298
1299
  @app.command()
1299
1300
  def index(
1300
1301
  path: Optional[str] = typer.Argument(None, help="Path to the directory or file to index. Defaults to the current directory."),
1301
1302
  force: bool = typer.Option(False, "--force", "-f", help="Force re-index (delete existing and rebuild)"),
1302
1303
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use (overrides mode/default)"),
1304
+ summarize: bool = typer.Option(False, "--summarize", "-s", help="Show a summary of the indexed codebase after indexing"),
1303
1305
  ):
1304
1306
  """
1305
1307
  Indexes a directory or file by adding it to the code graph.
1306
1308
  If no path is provided, it indexes the current directory.
1307
-
1309
+
1308
1310
  Use --force to delete the existing index and rebuild from scratch.
1311
+ Use --summarize to display a summary after indexing.
1309
1312
  """
1310
1313
  _load_credentials()
1311
1314
  if path is None:
1312
1315
  path = str(Path.cwd())
1313
-
1314
- if force:
1315
- console.print("[yellow]Force re-indexing (--force flag detected)[/yellow]")
1316
- reindex_helper(path, context)
1317
- else:
1318
- index_helper(path, context)
1316
+
1317
+ try:
1318
+ if force:
1319
+ console.print("[yellow]Force re-indexing (--force flag detected)[/yellow]")
1320
+ reindex_helper(path, context)
1321
+ else:
1322
+ index_helper(path, context)
1323
+ except Exception as e:
1324
+ if str(e):
1325
+ console.print(f"[red]An error occurred during indexing: {e}[/red]")
1326
+
1327
+ if summarize:
1328
+ import os
1329
+
1330
+ py_files = []
1331
+ for root, dirs, files in os.walk(path):
1332
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['venv', '__pycache__', 'node_modules']]
1333
+ for file in files:
1334
+ if file.endswith('.py'):
1335
+ py_files.append(os.path.join(root, file))
1336
+
1337
+ total_lines = 0
1338
+ for f in py_files:
1339
+ try:
1340
+ with open(f, 'r', encoding='utf-8', errors='ignore') as fp:
1341
+ total_lines += len(fp.readlines())
1342
+ except:
1343
+ pass
1344
+
1345
+ console.print("\n[bold cyan]📊 Codebase Summary:[/bold cyan]")
1346
+ console.print(f" • Path indexed : [green]{path}[/green]")
1347
+ console.print(f" • Python files : [yellow]{len(py_files)}[/yellow]")
1348
+ console.print(f" • Total lines : [yellow]{total_lines}[/yellow]")
1349
+ console.print(f"\n • Run [bold]cgc analyze complexity[/bold] to find complex functions")
1350
+ console.print(f" • Run [bold]cgc analyze dead-code[/bold] to find unused code")
1351
+ console.print(f" • Run [bold]cgc list[/bold] to see all indexed repositories")
1352
+ console.print("\n[dim]Tip: Use --summarize anytime after indexing to see this.[/dim]")
1319
1353
 
1320
1354
  @app.command()
1321
1355
  def update(
@@ -1519,6 +1553,7 @@ def report(
1519
1553
  @app.command()
1520
1554
  def visualize(
1521
1555
  repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Path to the repository to visualize."),
1556
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host interface to bind to."),
1522
1557
  port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on."),
1523
1558
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
1524
1559
  ):
@@ -1526,7 +1561,7 @@ def visualize(
1526
1561
  Launches the interactive UI to visualize the code graph.
1527
1562
  """
1528
1563
  _load_credentials()
1529
- visualize_helper(repo, port, context)
1564
+ visualize_helper(repo, host, port, context)
1530
1565
 
1531
1566
  @app.command("list")
1532
1567
  def list_repositories(
@@ -1565,6 +1600,14 @@ def watch(
1565
1600
  "--poll",
1566
1601
  help="Use watchdog's polling observer for Docker bind mounts and network filesystems.",
1567
1602
  ),
1603
+ sync_on_start: bool = typer.Option(
1604
+ False,
1605
+ "--sync-on-start",
1606
+ help=(
1607
+ "Synchronize already-indexed files before watching. "
1608
+ "Defaults off; use 'cgc index --force' for a full re-index."
1609
+ ),
1610
+ ),
1568
1611
  ):
1569
1612
  """
1570
1613
  Watch a directory for file changes and automatically update the code graph.
@@ -1575,6 +1618,7 @@ def watch(
1575
1618
 
1576
1619
  The watcher will:
1577
1620
  - Perform an initial scan if the directory is not yet indexed
1621
+ - Attach immediately for already-indexed directories unless --sync-on-start is passed
1578
1622
  - Monitor for file creation, modification, deletion, and moves
1579
1623
  - Automatically re-index affected files and update relationships
1580
1624
 
@@ -1584,12 +1628,13 @@ def watch(
1584
1628
  cgc watch . # Watch current directory
1585
1629
  cgc watch /path/to/project # Watch specific directory
1586
1630
  cgc watch --poll . # Use polling for Docker/NFS/SMB mounts
1631
+ cgc watch --sync-on-start . # Reconcile current files before watching
1587
1632
  cgc w . # Using shortcut alias
1588
1633
 
1589
1634
  Set CGC_WATCH_POLLING=1 to use polling without passing --poll.
1590
1635
  """
1591
1636
  _load_credentials()
1592
- watch_helper(path, context, use_polling=poll or None)
1637
+ watch_helper(path, context, use_polling=poll or None, sync_on_start=sync_on_start)
1593
1638
 
1594
1639
  @app.command()
1595
1640
  def unwatch(
@@ -2508,7 +2553,7 @@ def analyze_inheritance_tree(
2508
2553
  @analyze_app.command("complexity")
2509
2554
  def analyze_complexity(
2510
2555
  path: Optional[str] = typer.Argument(None, help="Function name or file path to analyze"),
2511
- threshold: int = typer.Option(10, "--threshold", "-t", help="Complexity threshold for warnings"),
2556
+ threshold: Optional[int] = typer.Option(None, "--threshold", "-t", help="Complexity threshold for warnings (default: from config or 10)"),
2512
2557
  limit: int = typer.Option(20, "--limit", "-l", help="Maximum results to show"),
2513
2558
  file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path to scope analysis"),
2514
2559
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
@@ -2531,6 +2576,17 @@ def analyze_complexity(
2531
2576
  raise typer.Exit(code=1)
2532
2577
  db_manager, graph_builder, code_finder = services[:3]
2533
2578
 
2579
+ # Read threshold from config if not explicitly provided via CLI
2580
+ if threshold is None:
2581
+ configured = config_manager.get_config_value("COMPLEXITY_THRESHOLD")
2582
+ if configured is not None:
2583
+ try:
2584
+ threshold = int(configured)
2585
+ except (ValueError, TypeError):
2586
+ threshold = 10
2587
+ else:
2588
+ threshold = 10
2589
+
2534
2590
  _FILE_EXTENSIONS = ('.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.rb',
2535
2591
  '.java', '.cpp', '.c', '.cs', '.swift', '.kt', '.scala',
2536
2592
  '.php', '.lua', '.zig', '.ex', '.exs', '.r', '.m', '.sh')
@@ -2846,9 +2902,22 @@ def visualize_abbrev(
2846
2902
  def watch_abbrev(
2847
2903
  path: str = typer.Argument(".", help="Path to watch"),
2848
2904
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2905
+ poll: bool = typer.Option(
2906
+ False,
2907
+ "--poll",
2908
+ help="Use watchdog's polling observer for Docker bind mounts and network filesystems.",
2909
+ ),
2910
+ sync_on_start: bool = typer.Option(
2911
+ False,
2912
+ "--sync-on-start",
2913
+ help=(
2914
+ "Synchronize already-indexed files before watching. "
2915
+ "Defaults off; use 'cgc index --force' for a full re-index."
2916
+ ),
2917
+ ),
2849
2918
  ):
2850
2919
  """Shortcut for 'cgc watch'"""
2851
- watch(path, context=context)
2920
+ watch(path, context=context, poll=poll, sync_on_start=sync_on_start)
2852
2921
 
2853
2922
 
2854
2923
  # ============================================================================