codegraphcontext 0.4.17__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. codegraphcontext/api/app.py +9 -1
  2. codegraphcontext/api/mcp_sse.py +10 -5
  3. codegraphcontext/api/router.py +3 -6
  4. codegraphcontext/cli/cli_helpers.py +82 -12
  5. codegraphcontext/cli/config_manager.py +83 -39
  6. codegraphcontext/cli/hook_manager.py +220 -0
  7. codegraphcontext/cli/main.py +212 -45
  8. codegraphcontext/cli/registry_commands.py +11 -4
  9. codegraphcontext/cli/setup_macos.py +6 -1
  10. codegraphcontext/cli/setup_wizard.py +96 -10
  11. codegraphcontext/cli/visualizer.py +25 -1
  12. codegraphcontext/core/__init__.py +71 -10
  13. codegraphcontext/core/cgc_bundle.py +14 -4
  14. codegraphcontext/core/database_falkordb.py +41 -2
  15. codegraphcontext/core/database_kuzu.py +103 -53
  16. codegraphcontext/core/database_ladybug.py +55 -23
  17. codegraphcontext/core/jobs.py +7 -2
  18. codegraphcontext/core/watcher.py +155 -255
  19. codegraphcontext/prompts.py +1 -1
  20. codegraphcontext/server.py +54 -5
  21. codegraphcontext/stdlibs.py +18 -0
  22. codegraphcontext/tool_definitions.py +188 -79
  23. codegraphcontext/tools/code_finder.py +18 -1
  24. codegraphcontext/tools/graph_builder.py +297 -185
  25. codegraphcontext/tools/handlers/analysis_handlers.py +5 -0
  26. codegraphcontext/tools/handlers/indexing_handlers.py +4 -1
  27. codegraphcontext/tools/handlers/management_handlers.py +12 -9
  28. codegraphcontext/tools/handlers/query_handlers.py +21 -3
  29. codegraphcontext/tools/handlers/watcher_handlers.py +9 -4
  30. codegraphcontext/tools/indexing/discovery.py +1 -2
  31. codegraphcontext/tools/indexing/persistence/utils.py +46 -0
  32. codegraphcontext/tools/indexing/persistence/writer.py +630 -129
  33. codegraphcontext/tools/indexing/pipeline.py +49 -14
  34. codegraphcontext/tools/indexing/resolution/__init__.py +11 -1
  35. codegraphcontext/tools/indexing/resolution/calls.py +506 -63
  36. codegraphcontext/tools/indexing/resolution/inheritance.py +439 -3
  37. codegraphcontext/tools/indexing/resolution/post_resolution.py +56 -19
  38. codegraphcontext/tools/indexing/schema.py +5 -0
  39. codegraphcontext/tools/indexing/schema_contract.py +7 -0
  40. codegraphcontext/tools/indexing/scip_pipeline.py +38 -3
  41. codegraphcontext/tools/languages/c.py +154 -2
  42. codegraphcontext/tools/languages/cpp.py +13 -1
  43. codegraphcontext/tools/languages/csharp.py +1 -0
  44. codegraphcontext/tools/languages/css.py +13 -4
  45. codegraphcontext/tools/languages/dart.py +170 -41
  46. codegraphcontext/tools/languages/elixir.py +9 -6
  47. codegraphcontext/tools/languages/go.py +56 -16
  48. codegraphcontext/tools/languages/haskell.py +80 -8
  49. codegraphcontext/tools/languages/html.py +23 -8
  50. codegraphcontext/tools/languages/javascript.py +2 -2
  51. codegraphcontext/tools/languages/lua.py +61 -19
  52. codegraphcontext/tools/languages/perl.py +37 -4
  53. codegraphcontext/tools/languages/php.py +56 -4
  54. codegraphcontext/tools/languages/python.py +21 -14
  55. codegraphcontext/tools/languages/rust.py +70 -6
  56. codegraphcontext/tools/languages/swift.py +61 -11
  57. codegraphcontext/tools/languages/typescript.py +63 -6
  58. codegraphcontext/tools/package_resolver.py +159 -364
  59. codegraphcontext/tools/report_generator.py +11 -11
  60. codegraphcontext/tools/system.py +8 -1
  61. codegraphcontext/utils/path_sandbox.py +2 -1
  62. {codegraphcontext-0.4.17.dist-info → codegraphcontext-0.5.1.dist-info}/METADATA +15 -9
  63. {codegraphcontext-0.4.17.dist-info → codegraphcontext-0.5.1.dist-info}/RECORD +67 -64
  64. {codegraphcontext-0.4.17.dist-info → codegraphcontext-0.5.1.dist-info}/WHEEL +0 -0
  65. {codegraphcontext-0.4.17.dist-info → codegraphcontext-0.5.1.dist-info}/entry_points.txt +0 -0
  66. {codegraphcontext-0.4.17.dist-info → codegraphcontext-0.5.1.dist-info}/licenses/LICENSE +0 -0
  67. {codegraphcontext-0.4.17.dist-info → codegraphcontext-0.5.1.dist-info}/top_level.txt +0 -0
@@ -17,13 +17,21 @@ 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
  )
24
27
 
25
28
  app.include_router(router, prefix="/api/v1")
26
29
 
30
+ @app.get("/health")
31
+ async def health():
32
+ """Liveness probe for load balancers and k8s."""
33
+ return {"status": "ok"}
34
+
27
35
  # MCP-over-SSE Endpoints
28
36
  app.add_api_route("/api/v1/mcp/sse", handle_sse, methods=["GET"])
29
37
  app.add_api_route("/api/v1/mcp/messages", handle_messages, methods=["POST"])
@@ -8,16 +8,17 @@ from mcp.types import Tool, TextContent, ServerCapabilities, ToolsCapability
8
8
  from mcp.server.sse import SseServerTransport
9
9
 
10
10
  from codegraphcontext.api.router import get_server
11
- from codegraphcontext.tool_definitions import TOOLS
11
+ from codegraphcontext.server import _strip_workspace_prefix, _apply_response_token_limit
12
12
 
13
13
  # Create the MCP Server instance using the SDK
14
14
  mcp_server = Server("CodeGraphContext")
15
15
 
16
16
  @mcp_server.list_tools()
17
17
  async def handle_list_tools() -> list[Tool]:
18
- """List available tools."""
18
+ """List available tools (honors disabledTools from mcp.json)."""
19
+ server = get_server()
19
20
  tools = []
20
- for name, defn in TOOLS.items():
21
+ for name, defn in server.tools.items():
21
22
  tools.append(Tool(
22
23
  name=name,
23
24
  description=defn["description"],
@@ -33,12 +34,16 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten
33
34
 
34
35
  # Execute via the existing handler logic
35
36
  result = await server.handle_tool_call(name, args)
37
+ result = _strip_workspace_prefix(result)
36
38
 
37
39
  if "error" in result:
38
40
  return [TextContent(type="text", text=f"Error: {result['error']}")]
39
41
 
40
- # Format result as JSON string for the AI
41
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
42
+ # Format result as JSON string for the AI, with the same token budget
43
+ # the stdio transport applies.
44
+ response_text = json.dumps(result, indent=2)
45
+ response_text = _apply_response_token_limit(name, response_text)
46
+ return [TextContent(type="text", text=response_text)]
42
47
 
43
48
  # Create the SSE transport.
44
49
  # The messages_url is where the client will POST JSON-RPC messages.
@@ -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(
@@ -35,6 +35,7 @@ from .config_manager import (
35
35
  register_repo_in_context,
36
36
  ensure_first_run_bootstrap,
37
37
  ContextNotFoundError,
38
+ is_db_deletion_allowed,
38
39
  )
39
40
 
40
41
  console = Console()
@@ -260,6 +261,10 @@ def index_helper(path: str, context: Optional[str] = None):
260
261
  """Synchronously indexes a repository in a given context."""
261
262
  time_start = time.time()
262
263
  path_obj = Path(path).resolve()
264
+ # Normalize to forward slashes for cross-platform DB consistency.
265
+ # The graph DB always stores paths via Path.resolve().as_posix(),
266
+ # so Cypher queries must also use forward slashes on Windows.
267
+ repo_path_str = path_obj.as_posix()
263
268
  index_cwd = path_obj if path_obj.is_dir() else path_obj.parent
264
269
  services = _initialize_services(context, cwd=index_cwd)
265
270
  if not all(services[:3]):
@@ -283,7 +288,7 @@ def index_helper(path: str, context: Optional[str] = None):
283
288
  with db_manager.get_driver().session() as session:
284
289
  result = session.run(
285
290
  "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(DISTINCT f) as file_count",
286
- path=str(path_obj)
291
+ path=repo_path_str
287
292
  )
288
293
  record = result.single()
289
294
  file_count = record["file_count"] if record else 0
@@ -415,6 +420,49 @@ def delete_helper(repo_path: str, context: Optional[str] = None):
415
420
  finally:
416
421
  db_manager.close_driver()
417
422
 
423
+ def _print_query_exception(e: Exception, query: str) -> None:
424
+ """
425
+ Pretty-print a database query exception, surfacing the raw driver
426
+ error message so Cypher syntax problems are clearly visible.
427
+ """
428
+ import traceback
429
+
430
+ error_type = type(e).__name__
431
+ error_module = type(e).__module__ or ""
432
+
433
+ # Neo4j: CypherSyntaxError and other ClientError subclasses carry
434
+ # a .message and .code attribute with the full server-side detail.
435
+ if "neo4j" in error_module:
436
+ code = getattr(e, "code", None)
437
+ msg = getattr(e, "message", None) or str(e)
438
+ console.print(f"[bold red]Query Error ({error_type}):[/bold red]")
439
+ if code:
440
+ console.print(f" [yellow]Code:[/yellow] {code}")
441
+ console.print(f" [yellow]Message:[/yellow] {msg}")
442
+
443
+ # FalkorDB: ResponseError / exceptions in falkordb or redis packages
444
+ elif "falkordb" in error_module or "redis" in error_module:
445
+ console.print(f"[bold red]Query Error ({error_type}):[/bold red]")
446
+ console.print(f" [yellow]Database message:[/yellow] {e}")
447
+
448
+ # KuzuDB: RuntimeError from the kuzu extension
449
+ # KuzuDB: RuntimeError from the kuzu extension or database_kuzu wrapper
450
+ elif "kuzu" in error_module or (
451
+ error_type == "RuntimeError" and "Parser exception" in str(e)
452
+ ):
453
+ console.print(f"[bold red]Query Error ({error_type}):[/bold red]")
454
+ console.print(f" [yellow]Database message:[/yellow] {e}")
455
+
456
+ else:
457
+ # Fallback: unknown backend — print type + message + traceback
458
+ console.print(f"[bold red]An error occurred while executing query ({error_type}):[/bold red]")
459
+ console.print(f" [yellow]Message:[/yellow] {e}")
460
+ console.print("[dim]--- Traceback ---[/dim]")
461
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
462
+
463
+ console.print(f"\n[dim]Failed query:[/dim]")
464
+ console.print(f"[dim] {query}[/dim]")
465
+
418
466
 
419
467
  def cypher_helper(query: str, context: Optional[str] = None):
420
468
  """Executes a read-only Cypher query."""
@@ -440,7 +488,7 @@ def cypher_helper(query: str, context: Optional[str] = None):
440
488
  records = [record.data() for record in result]
441
489
  console.print(json.dumps(records, indent=2))
442
490
  except Exception as e:
443
- console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
491
+ _print_query_exception(e, query)
444
492
  db_manager.close_driver()
445
493
  raise typer.Exit(code=1)
446
494
  finally:
@@ -466,7 +514,7 @@ def cypher_helper_visual(query: str, context: Optional[str] = None):
466
514
  try:
467
515
  visualize_cypher_results(query)
468
516
  except Exception as e:
469
- console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
517
+ _print_query_exception(e, query)
470
518
  db_manager.close_driver()
471
519
  raise typer.Exit(code=1)
472
520
  finally:
@@ -626,14 +674,32 @@ def reindex_helper(path: str, context: Optional[str] = None):
626
674
  db_manager.close_driver()
627
675
 
628
676
 
629
- def update_helper(path: str, context: Optional[str] = None):
630
- """Update/refresh index for a path (alias for reindex)."""
677
+ def update_helper(path: str, context: Optional[str] = None, quiet: bool = False):
678
+ """Update/refresh index for a path (alias for reindex).
679
+
680
+ When *quiet* is True (e.g. when invoked from Git hooks with --quiet),
681
+ Rich console output, including progress rendering, is suppressed.
682
+ """
683
+ if quiet:
684
+ console.quiet = True
685
+ try:
686
+ reindex_helper(path, context)
687
+ finally:
688
+ console.quiet = False
689
+ return
631
690
  console.print("[cyan]Updating repository index...[/cyan]")
632
691
  reindex_helper(path, context)
633
692
 
634
693
 
635
694
  def clean_helper(context: Optional[str] = None):
636
695
  """Remove orphaned nodes and relationships from the database."""
696
+ if not is_db_deletion_allowed():
697
+ console.print(
698
+ "[bold red]Error:[/bold red] Database cleanup is disabled. "
699
+ "Set ALLOW_DB_DELETION=true in config to enable."
700
+ )
701
+ raise typer.Exit(code=1)
702
+
637
703
  services = _initialize_services(context)
638
704
  if not all(services[:3]):
639
705
  _fail_services_init()
@@ -691,6 +757,9 @@ def stats_helper(path: str = None, context: Optional[str] = None):
691
757
  if path:
692
758
  # Stats for specific repository
693
759
  path_obj = Path(path).resolve()
760
+ # Paths are stored with forward slashes (as_posix) in the graph DB,
761
+ # so lookups must use the same normalization on Windows too.
762
+ repo_path_str = path_obj.as_posix()
694
763
  console.print(f"[cyan]📊 Statistics for: {path_obj}[/cyan]\n")
695
764
 
696
765
  with db_manager.get_driver().session() as session:
@@ -699,7 +768,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
699
768
  MATCH (r:Repository {path: $path})
700
769
  RETURN r
701
770
  """
702
- result = session.run(repo_query, path=str(path_obj))
771
+ result = session.run(repo_query, path=repo_path_str)
703
772
  if not result.single():
704
773
  console.print(f"[red]Repository not found: {path_obj}[/red]")
705
774
  return
@@ -708,20 +777,20 @@ def stats_helper(path: str = None, context: Optional[str] = None):
708
777
  # Get stats using separate queries to handle depth and avoid Cartesian products
709
778
  # 1. Files
710
779
  file_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(f) as c"
711
- file_count = session.run(file_query, path=str(path_obj)).single()["c"]
780
+ file_count = session.run(file_query, path=repo_path_str).single()["c"]
712
781
 
713
782
  # 2. Functions (including methods in classes)
714
783
  func_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(func:Function) RETURN count(func) as c"
715
- func_count = session.run(func_query, path=str(path_obj)).single()["c"]
784
+ func_count = session.run(func_query, path=repo_path_str).single()["c"]
716
785
 
717
786
  # 3. Classes
718
787
  class_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(c:Class) RETURN count(c) as c"
719
- class_count = session.run(class_query, path=str(path_obj)).single()["c"]
788
+ class_count = session.run(class_query, path=repo_path_str).single()["c"]
720
789
 
721
790
  # 4. Modules (imported) - Note: Module nodes are outside the repo structure usually, connected via IMPORTS
722
791
  # We need to traverse from files to modules
723
792
  module_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File)-[:IMPORTS]->(m:Module) RETURN count(DISTINCT m) as c"
724
- module_count = session.run(module_query, path=str(path_obj)).single()["c"]
793
+ module_count = session.run(module_query, path=repo_path_str).single()["c"]
725
794
 
726
795
  table = Table(show_header=True, header_style="bold magenta")
727
796
  table.add_column("Metric", style="cyan")
@@ -822,7 +891,7 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
822
891
  with code_finder.driver.session() as _s:
823
892
  _r = _s.run(
824
893
  "MATCH (n:File) WHERE n.path STARTS WITH $p RETURN count(n) AS c",
825
- p=str(path_obj) + "/"
894
+ p=path_obj.as_posix() + "/"
826
895
  )
827
896
  _count = _r.single()["c"]
828
897
  if _count > 100:
@@ -844,10 +913,11 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
844
913
 
845
914
  # Add the directory to watch
846
915
  if is_indexed:
847
- console.print("[green]✓[/green] Already indexed (no initial scan needed)")
916
+ console.print("[green]✓[/green] Already indexed. Synchronizing current files...")
848
917
  watcher.watch_directory(
849
918
  str(path_obj),
850
919
  perform_initial_scan=False,
920
+ sync_on_start=True,
851
921
  cgcignore_path=ctx.cgcignore_path,
852
922
  )
853
923
  else:
@@ -15,6 +15,19 @@ import yaml
15
15
 
16
16
  console = Console()
17
17
 
18
+
19
+ def _atomic_write_text(path: Path, content: str, *, secure: bool = False) -> None:
20
+ """Write *content* to *path* atomically (temp file + replace)."""
21
+ path.parent.mkdir(parents=True, exist_ok=True)
22
+ tmp_path = path.with_suffix(path.suffix + ".tmp")
23
+ with open(tmp_path, "w", encoding="utf-8") as f:
24
+ f.write(content)
25
+ f.flush()
26
+ os.fsync(f.fileno())
27
+ os.replace(tmp_path, path)
28
+ if secure:
29
+ os.chmod(path, 0o600)
30
+
18
31
  # Configuration file location
19
32
  CONFIG_DIR = Path.home() / ".codegraphcontext"
20
33
  CONFIG_FILE = CONFIG_DIR / ".env"
@@ -160,6 +173,14 @@ CONFIG_VALIDATORS = {
160
173
  "CGC_EMBEDDING_MODEL": ["local", "openai"],
161
174
  "FUZZY_SEARCH": ["true", "false"],
162
175
  }
176
+
177
+ SUPPORTED_DATABASES: List[str] = CONFIG_VALIDATORS["DEFAULT_DATABASE"]
178
+ DATABASE_CLI_HELP = (
179
+ "Database backend ("
180
+ + "|".join(SUPPORTED_DATABASES)
181
+ + "). Defaults to DEFAULT_DATABASE from config."
182
+ )
183
+
163
184
  DEFAULT_CGCIGNORE_PATTERNS = """\
164
185
  # Default .cgcignore patterns
165
186
  # Lines starting with # are comments; blank lines are ignored.
@@ -279,18 +300,25 @@ def load_config() -> Dict[str, str]:
279
300
  def should_apply_project_dotenv() -> bool:
280
301
  """True when cwd-local ``.codegraphcontext/.env`` should merge with global config.
281
302
 
282
- Skips project env when ``HOME`` is isolated (e.g. E2E) but ``cwd`` is an unrelated
283
- checkout, unless ``CGC_LOAD_PROJECT_ENV=1``. Set ``CGC_IGNORE_PROJECT_ENV=1`` to force skip.
303
+ Project env is loaded only in **per-repo** context mode (or when
304
+ ``CGC_LOAD_PROJECT_ENV=1``). In **global** / **named** mode, ``~/.codegraphcontext/.env``
305
+ wins so clones with a checked-in ``.codegraphcontext/.env`` do not hijack config.
306
+
307
+ Set ``CGC_IGNORE_PROJECT_ENV=1`` to force skip; ``CGC_LOAD_PROJECT_ENV=1`` to force load.
284
308
  """
285
309
  if os.getenv("CGC_IGNORE_PROJECT_ENV", "").strip().lower() in ("1", "true", "yes"):
286
310
  return False
287
311
  if os.getenv("CGC_LOAD_PROJECT_ENV", "").strip().lower() in ("1", "true", "yes"):
288
312
  return True
313
+ cfg = load_context_config()
314
+ if cfg.mode != "per-repo":
315
+ return False
289
316
  try:
290
317
  Path.cwd().resolve().relative_to(Path.home().resolve())
291
318
  return True
292
319
  except ValueError:
293
- return False
320
+ # Per-repo indexing from /tmp with an isolated HOME (common in E2E/CI).
321
+ return True
294
322
 
295
323
 
296
324
  def find_local_env() -> Optional[Path]:
@@ -370,30 +398,27 @@ def save_config(config: Dict[str, str], preserve_db_credentials: bool = True):
370
398
  credentials_to_write[key] = config[key]
371
399
 
372
400
  try:
373
- with open(CONFIG_FILE, "w", encoding="utf-8") as f:
374
- f.write("# CodeGraphContext Configuration\n")
375
- f.write(f"# Location: {CONFIG_FILE}\n\n")
376
-
377
- # Write database credentials first if they exist
378
- if credentials_to_write:
379
- f.write("# ===== Database Credentials =====\n")
380
- for key in sorted(DATABASE_CREDENTIAL_KEYS):
381
- if key in credentials_to_write:
382
- f.write(f"{key}={credentials_to_write[key]}\n")
383
- f.write("\n")
384
-
385
- # Write configuration settings
386
- f.write("# ===== Configuration Settings =====\n")
387
- for key, value in sorted(config.items()):
388
- # Skip database credentials (already written above)
389
- if key in DATABASE_CREDENTIAL_KEYS:
390
- continue
391
-
392
- description = CONFIG_DESCRIPTIONS.get(key, "")
393
- if description:
394
- f.write(f"# {description}\n")
395
- f.write(f"{key}={value}\n\n")
396
-
401
+ lines = [
402
+ "# CodeGraphContext Configuration",
403
+ f"# Location: {CONFIG_FILE}",
404
+ "",
405
+ ]
406
+ if credentials_to_write:
407
+ lines.append("# ===== Database Credentials =====")
408
+ for key in sorted(DATABASE_CREDENTIAL_KEYS):
409
+ if key in credentials_to_write:
410
+ lines.append(f"{key}={credentials_to_write[key]}")
411
+ lines.append("")
412
+ lines.append("# ===== Configuration Settings =====")
413
+ for key, value in sorted(config.items()):
414
+ if key in DATABASE_CREDENTIAL_KEYS:
415
+ continue
416
+ description = CONFIG_DESCRIPTIONS.get(key, "")
417
+ if description:
418
+ lines.append(f"# {description}")
419
+ lines.append(f"{key}={value}")
420
+ lines.append("")
421
+ _atomic_write_text(CONFIG_FILE, "\n".join(lines), secure=True)
397
422
  console.print(f"[green]✅ Configuration saved to {CONFIG_FILE}[/green]")
398
423
  except Exception as e:
399
424
  console.print(f"[red]Error saving config: {e}[/red]")
@@ -789,8 +814,10 @@ def save_context_config(cfg: ContextConfig) -> None:
789
814
  }
790
815
 
791
816
  try:
792
- with open(CONTEXT_CONFIG_FILE, "w", encoding="utf-8") as f:
793
- yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
817
+ _atomic_write_text(
818
+ CONTEXT_CONFIG_FILE,
819
+ yaml.dump(raw, default_flow_style=False, sort_keys=False),
820
+ )
794
821
  except Exception as e:
795
822
  console.print(f"[red]Error saving config.yaml: {e}[/red]")
796
823
 
@@ -869,23 +896,38 @@ def resolve_context(
869
896
  local_cgc = cwd / ".codegraphcontext"
870
897
  local_cgc.mkdir(parents=True, exist_ok=True)
871
898
  (local_cgc / "db").mkdir(exist_ok=True)
872
-
873
- # Copy global .env into local context for easy per-repo tweaking
899
+
900
+ inherited_db = load_config().get("DEFAULT_DATABASE", "falkordb")
901
+
902
+ # Copy global .env into local context for easy per-repo tweaking.
903
+ # Guard against the self-copy case: when cwd is the home directory,
904
+ # local_cgc resolves to CONFIG_DIR itself, so `local_cgc / ".env"` is
905
+ # CONFIG_FILE. Copying a file onto itself raises shutil.SameFileError,
906
+ # which crashes resolve_context for any session started from home.
874
907
  import shutil
875
- if CONFIG_FILE.exists():
876
- shutil.copy2(CONFIG_FILE, local_cgc / ".env")
877
-
878
- console.print(f"[dim]Auto-initialized per-repo context at {local_cgc}[/dim]")
908
+ _target_env = local_cgc / ".env"
909
+ if CONFIG_FILE.exists() and _target_env.resolve() != CONFIG_FILE.resolve():
910
+ shutil.copy2(CONFIG_FILE, _target_env)
911
+
912
+ local_yaml = local_cgc / "config.yaml"
913
+ if not local_yaml.exists():
914
+ with open(local_yaml, "w", encoding="utf-8") as f:
915
+ yaml.safe_dump({"database": inherited_db}, f)
916
+
917
+ console.print(
918
+ f"[dim]Auto-initialized per-repo context at {local_cgc} "
919
+ f"(Database: {inherited_db})[/dim]"
920
+ )
879
921
 
880
922
  if local_cgc is not None:
881
923
  # Read local config.yaml if present
882
924
  local_yaml = local_cgc / "config.yaml"
883
- local_db = "falkordb"
925
+ local_db = load_config().get("DEFAULT_DATABASE", "falkordb")
884
926
  if local_yaml.exists():
885
927
  try:
886
928
  with open(local_yaml, encoding="utf-8") as f:
887
929
  local_raw = yaml.safe_load(f) or {}
888
- local_db = local_raw.get("database", "falkordb")
930
+ local_db = local_raw.get("database", local_db)
889
931
  except Exception:
890
932
  pass
891
933
  db_path = str(local_cgc / "db" / local_db)
@@ -1147,8 +1189,10 @@ def _save_workspace_mappings(mappings: Dict[str, Dict[str, str]]) -> None:
1147
1189
  raw = {}
1148
1190
  raw["workspace_mappings"] = mappings
1149
1191
  try:
1150
- with open(CONTEXT_CONFIG_FILE, "w", encoding="utf-8") as f:
1151
- yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
1192
+ _atomic_write_text(
1193
+ CONTEXT_CONFIG_FILE,
1194
+ yaml.dump(raw, default_flow_style=False, sort_keys=False),
1195
+ )
1152
1196
  except Exception as e:
1153
1197
  console.print(f"[red]Error saving workspace mappings: {e}[/red]")
1154
1198