codegraphcontext-rust 0.5.5__cp310-cp310-win_amd64.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 (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp310-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,1051 @@
1
+ """
2
+ Configuration management for CodeGraphContext.
3
+ Handles reading, writing, and validating configuration settings.
4
+ Also manages the context system (config.yaml) alongside the existing .env file.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Optional, Dict, Any, List
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ import os
13
+ import yaml
14
+
15
+ console = Console()
16
+
17
+ # Configuration file location
18
+ CONFIG_DIR = Path.home() / ".codegraphcontext"
19
+ CONFIG_FILE = CONFIG_DIR / ".env"
20
+
21
+ # Database credential keys (stored in same .env file but not managed as config)
22
+ DATABASE_CREDENTIAL_KEYS = {"NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE"}
23
+
24
+ # Default configuration values
25
+ DEFAULT_CONFIG = {
26
+ "DEFAULT_DATABASE": "falkordb",
27
+ "FALKORDB_PATH": str(CONFIG_DIR / "global" / "db" / "falkordb"),
28
+ "FALKORDB_SOCKET_PATH": str(CONFIG_DIR / "global" / "db" / "falkordb.sock"),
29
+ "INDEX_VARIABLES": "true",
30
+ "ALLOW_DB_DELETION": "false",
31
+ "DEBUG_LOGS": "false",
32
+ "DEBUG_LOG_PATH": str(Path.home() / "mcp_debug.log"),
33
+ "ENABLE_APP_LOGS": "CRITICAL",
34
+ "LIBRARY_LOG_LEVEL": "WARNING",
35
+ "LOG_FILE_PATH": str(CONFIG_DIR / "logs" / "cgc.log"),
36
+ "MAX_FILE_SIZE_MB": "10",
37
+ "IGNORE_TEST_FILES": "false",
38
+ "IGNORE_HIDDEN_FILES": "true",
39
+ "ENABLE_AUTO_WATCH": "false",
40
+ "COMPLEXITY_THRESHOLD": "10",
41
+ "MAX_DEPTH": "unlimited",
42
+ "PARALLEL_WORKERS": "4",
43
+ "CACHE_ENABLED": "true",
44
+ "IGNORE_DIRS": "node_modules,venv,.venv,env,.env,dist,build,target,out,.git,.idea,.vscode,__pycache__",
45
+ "INDEX_SOURCE": "true",
46
+ # SCIP indexer feature flag (default off — existing Tree-sitter behaviour unchanged)
47
+ "SCIP_INDEXER": "false",
48
+ "SCIP_LANGUAGES": "python,typescript,go,rust,java",
49
+ "SKIP_EXTERNAL_RESOLUTION": "false",
50
+ }
51
+
52
+ # Configuration key descriptions
53
+ CONFIG_DESCRIPTIONS = {
54
+ "DEFAULT_DATABASE": "Default database backend (neo4j|falkordb|kuzudb)",
55
+ "FALKORDB_PATH": "Path to FalkorDB database file",
56
+ "FALKORDB_SOCKET_PATH": "Path to FalkorDB Unix socket",
57
+ "INDEX_VARIABLES": "Index variable nodes in the graph (lighter graph if false)",
58
+ "ALLOW_DB_DELETION": "Allow full database deletion commands",
59
+ "DEBUG_LOGS": "Enable debug logging (for development/troubleshooting)",
60
+ "DEBUG_LOG_PATH": "Path to debug log file",
61
+ "ENABLE_APP_LOGS": "Application log level (DEBUG|INFO|WARNING|ERROR|CRITICAL|DISABLED)",
62
+ "LIBRARY_LOG_LEVEL": "Log level for third-party libraries (neo4j, asyncio, urllib3) (DEBUG|INFO|WARNING|ERROR|CRITICAL)",
63
+ "LOG_FILE_PATH": "Path to application log file",
64
+ "MAX_FILE_SIZE_MB": "Maximum file size to index (in MB)",
65
+ "IGNORE_TEST_FILES": "Skip test files during indexing",
66
+ "IGNORE_HIDDEN_FILES": "Skip hidden files/directories",
67
+ "ENABLE_AUTO_WATCH": "Automatically watch directory after indexing",
68
+ "COMPLEXITY_THRESHOLD": "Cyclomatic complexity warning threshold",
69
+ "MAX_DEPTH": "Maximum directory depth for indexing (unlimited or number)",
70
+ "PARALLEL_WORKERS": "Number of parallel indexing workers",
71
+ "CACHE_ENABLED": "Enable caching for faster re-indexing",
72
+ "IGNORE_DIRS": "Comma-separated list of directory names to ignore during indexing",
73
+ "INDEX_SOURCE": "Store full source code in graph database (for faster indexing use false, for better performance use true)",
74
+ "SCIP_INDEXER": "Use SCIP-based indexing for higher accuracy call/inheritance resolution (requires scip-<lang> tools installed)",
75
+ "SCIP_LANGUAGES": "Comma-separated languages to index via SCIP when SCIP_INDEXER=true (python,typescript,go,rust,java)",
76
+ "SKIP_EXTERNAL_RESOLUTION": "Skip resolution attempts for external library method calls (recommended for enterprise large Java/Spring codebases)",
77
+ }
78
+
79
+ # Valid values for each config key
80
+ CONFIG_VALIDATORS = {
81
+ "DEFAULT_DATABASE": ["neo4j", "falkordb", "falkordb-remote", "kuzudb"],
82
+ "INDEX_VARIABLES": ["true", "false"],
83
+ "ALLOW_DB_DELETION": ["true", "false"],
84
+ "DEBUG_LOGS": ["true", "false"],
85
+ "ENABLE_APP_LOGS": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "DISABLED"],
86
+ "LIBRARY_LOG_LEVEL": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
87
+ "IGNORE_TEST_FILES": ["true", "false"],
88
+ "IGNORE_HIDDEN_FILES": ["true", "false"],
89
+ "ENABLE_AUTO_WATCH": ["true", "false"],
90
+ "CACHE_ENABLED": ["true", "false"],
91
+ "INDEX_SOURCE": ["true", "false"],
92
+ "SCIP_INDEXER": ["true", "false"],
93
+ "SKIP_EXTERNAL_RESOLUTION": ["true", "false"],
94
+ }
95
+ DEFAULT_CGCIGNORE_PATTERNS = """\
96
+ # Default .cgcignore patterns
97
+ # Lines starting with # are comments; blank lines are ignored.
98
+ # Patterns follow .gitignore syntax.
99
+
100
+ node_modules/
101
+ venv/
102
+ .venv/
103
+ env/
104
+ .env/
105
+ dist/
106
+ build/
107
+ target/
108
+ out/
109
+ .git/
110
+ .idea/
111
+ .vscode/
112
+ __pycache__/
113
+ *.pyc
114
+ *.pyo
115
+ *.egg-info/
116
+ .tox/
117
+ .mypy_cache/
118
+ .pytest_cache/
119
+ coverage/
120
+ .next/
121
+ """
122
+
123
+
124
+ def ensure_config_dir(path: Path = CONFIG_DIR):
125
+ """
126
+ Ensure that the configuration directory exists.
127
+ Creates the directory and a logs subdirectory if they do not already exist.
128
+ """
129
+ path.mkdir(parents=True, exist_ok=True)
130
+ (path / "logs").mkdir(parents=True, exist_ok=True)
131
+
132
+
133
+ def ensure_global_cgcignore() -> bool:
134
+ """Create ``~/.codegraphcontext/global/.cgcignore`` with sensible defaults
135
+ if it does not already exist. Returns True when a new file was created."""
136
+ cgcignore_path = CONFIG_DIR / "global" / ".cgcignore"
137
+ if cgcignore_path.exists():
138
+ return False
139
+ cgcignore_path.parent.mkdir(parents=True, exist_ok=True)
140
+ cgcignore_path.write_text(DEFAULT_CGCIGNORE_PATTERNS)
141
+ return True
142
+
143
+
144
+
145
+ def load_config() -> Dict[str, str]:
146
+ """
147
+ Load configuration with priority support.
148
+ Priority order (highest to lowest):
149
+ 1. Environment variables
150
+ 2. Local .env file (in current or parent directories)
151
+ 3. Global ~/.codegraphcontext/.env
152
+
153
+ Note: Does NOT create config directory - caller must call ensure_config_dir() first if needed.
154
+ """
155
+ # Start with defaults
156
+ config = DEFAULT_CONFIG.copy()
157
+
158
+ # Load global config
159
+ if CONFIG_FILE.exists():
160
+ try:
161
+ with open(CONFIG_FILE, "r") as f:
162
+ for line in f:
163
+ line = line.strip()
164
+ if line and not line.startswith("#") and "=" in line:
165
+ key, value = line.split("=", 1)
166
+ config[key.strip()] = value.strip()
167
+ except Exception as e:
168
+ console.print(f"[red]Error loading global config: {e}[/red]")
169
+
170
+ # Load local .env file if it exists (overrides global)
171
+ local_env = find_local_env()
172
+ if local_env and local_env.exists():
173
+ try:
174
+ with open(local_env, "r") as f:
175
+ for line in f:
176
+ line = line.strip()
177
+ if line and not line.startswith("#") and "=" in line:
178
+ key, value = line.split("=", 1)
179
+ key = key.strip()
180
+ # Only override if it's a config key (not database credentials in local file)
181
+ if key in DEFAULT_CONFIG or key in DATABASE_CREDENTIAL_KEYS:
182
+ config[key] = value.strip()
183
+ except Exception as e:
184
+ console.print(f"[yellow]Warning: Error loading local .env: {e}[/yellow]")
185
+
186
+ # Environment variables have highest priority
187
+ for key in DEFAULT_CONFIG.keys():
188
+ env_value = os.getenv(key)
189
+ if env_value is not None:
190
+ config[key] = env_value
191
+
192
+ return config
193
+
194
+
195
+ def find_local_env() -> Optional[Path]:
196
+ """
197
+ Find a local .env file by searching current directory and parents.
198
+ Returns the first .env file found, or None.
199
+ """
200
+ current = Path.cwd()
201
+
202
+ # Search up to 5 levels up
203
+ for _ in range(5):
204
+ # 1. Prefer .codegraphcontext/.env if it exists
205
+ cgc_env = current / ".codegraphcontext" / ".env"
206
+ if cgc_env.exists() and cgc_env != CONFIG_FILE:
207
+ return cgc_env
208
+
209
+ # 2. Fall back to root project .env
210
+ env_file = current / ".env"
211
+ if env_file.exists() and env_file != CONFIG_FILE:
212
+ return env_file
213
+
214
+ # Stop at root
215
+ if current.parent == current:
216
+ break
217
+ current = current.parent
218
+
219
+ return None
220
+
221
+
222
+ def codegraphcontext_dotenv_at_cwd(cwd: Optional[Path] = None) -> Optional[Path]:
223
+ """
224
+ Return ``<cwd>/.codegraphcontext/.env`` if that file exists, else None.
225
+
226
+ *cwd* defaults to ``Path.cwd()``. Parent directories are **not** searched—same rule as
227
+ local context resolution (``find_local_cgc_dir``).
228
+ """
229
+ root = (cwd or Path.cwd()).resolve()
230
+ candidate = root / ".codegraphcontext" / ".env"
231
+ return candidate if candidate.exists() else None
232
+
233
+
234
+ def save_config(config: Dict[str, str], preserve_db_credentials: bool = True):
235
+ """
236
+ Save configuration to file.
237
+ If preserve_db_credentials is True, existing database credentials will be preserved.
238
+ If preserve_db_credentials is False, credentials from config dict will be written.
239
+ """
240
+ ensure_config_dir()
241
+
242
+ # Determine which credentials to write
243
+ credentials_to_write = {}
244
+
245
+ if preserve_db_credentials and CONFIG_FILE.exists():
246
+ # Load existing credentials from file to preserve them
247
+ try:
248
+ with open(CONFIG_FILE, "r") as f:
249
+ for line in f:
250
+ line = line.strip()
251
+ if line and not line.startswith("#") and "=" in line:
252
+ key, value = line.split("=", 1)
253
+ key = key.strip()
254
+ if key in DATABASE_CREDENTIAL_KEYS:
255
+ credentials_to_write[key] = value.strip()
256
+ except Exception:
257
+ pass
258
+ # Merge credentials from the config dict (handles both new and updated values)
259
+ for key in DATABASE_CREDENTIAL_KEYS:
260
+ if key in config:
261
+ credentials_to_write[key] = config[key]
262
+ else:
263
+ # Use credentials from the config dict being passed in
264
+ for key in DATABASE_CREDENTIAL_KEYS:
265
+ if key in config:
266
+ credentials_to_write[key] = config[key]
267
+
268
+ try:
269
+ with open(CONFIG_FILE, "w") as f:
270
+ f.write("# CodeGraphContext Configuration\n")
271
+ f.write(f"# Location: {CONFIG_FILE}\n\n")
272
+
273
+ # Write database credentials first if they exist
274
+ if credentials_to_write:
275
+ f.write("# ===== Database Credentials =====\n")
276
+ for key in sorted(DATABASE_CREDENTIAL_KEYS):
277
+ if key in credentials_to_write:
278
+ f.write(f"{key}={credentials_to_write[key]}\n")
279
+ f.write("\n")
280
+
281
+ # Write configuration settings
282
+ f.write("# ===== Configuration Settings =====\n")
283
+ for key, value in sorted(config.items()):
284
+ # Skip database credentials (already written above)
285
+ if key in DATABASE_CREDENTIAL_KEYS:
286
+ continue
287
+
288
+ description = CONFIG_DESCRIPTIONS.get(key, "")
289
+ if description:
290
+ f.write(f"# {description}\n")
291
+ f.write(f"{key}={value}\n\n")
292
+
293
+ console.print(f"[green]✅ Configuration saved to {CONFIG_FILE}[/green]")
294
+ except Exception as e:
295
+ console.print(f"[red]Error saving config: {e}[/red]")
296
+
297
+
298
+ def validate_config_value(key: str, value: str) -> tuple[bool, Optional[str]]:
299
+ """
300
+ Validate a configuration value.
301
+ Returns (is_valid, error_message)
302
+ """
303
+ # Skip validation for database credentials (they have their own validation elsewhere)
304
+ if key in DATABASE_CREDENTIAL_KEYS:
305
+ return True, None
306
+
307
+ # Strip quotes that might be in the value
308
+ value = value.strip().strip("'\"")
309
+
310
+ # Check if key exists
311
+ if key not in DEFAULT_CONFIG:
312
+ available_keys = ", ".join(sorted(DEFAULT_CONFIG.keys()))
313
+ return False, f"Unknown config key: {key}. Available keys: {available_keys}"
314
+
315
+ # Validate against specific validators if they exist
316
+ if key in CONFIG_VALIDATORS:
317
+ valid_values = CONFIG_VALIDATORS[key]
318
+ if value.lower() not in [v.lower() for v in valid_values]:
319
+ return False, f"Invalid value for {key}. Must be one of: {', '.join(valid_values)}"
320
+
321
+ # Specific validation for numeric values
322
+ if key == "MAX_FILE_SIZE_MB":
323
+ try:
324
+ size = int(value)
325
+ if size <= 0:
326
+ return False, "MAX_FILE_SIZE_MB must be a positive number"
327
+ except ValueError:
328
+ return False, "MAX_FILE_SIZE_MB must be a number"
329
+
330
+ if key == "COMPLEXITY_THRESHOLD":
331
+ try:
332
+ threshold = int(value)
333
+ if threshold <= 0:
334
+ return False, "COMPLEXITY_THRESHOLD must be a positive number"
335
+ except ValueError:
336
+ return False, "COMPLEXITY_THRESHOLD must be a number"
337
+
338
+ if key == "PARALLEL_WORKERS":
339
+ try:
340
+ workers = int(value)
341
+ if workers <= 0 or workers > 32:
342
+ return False, "PARALLEL_WORKERS must be between 1 and 32"
343
+ except ValueError:
344
+ return False, "PARALLEL_WORKERS must be a number"
345
+
346
+ if key == "MAX_DEPTH":
347
+ if value.lower() != "unlimited":
348
+ try:
349
+ depth = int(value)
350
+ if depth <= 0:
351
+ return False, "MAX_DEPTH must be 'unlimited' or a positive number"
352
+ except ValueError:
353
+ return False, "MAX_DEPTH must be 'unlimited' or a number"
354
+
355
+ if key in ("LOG_FILE_PATH", "DEBUG_LOG_PATH"):
356
+ # Validate path is writable
357
+ log_path = Path(value)
358
+ try:
359
+ log_path.parent.mkdir(parents=True, exist_ok=True)
360
+ except Exception as e:
361
+ return False, f"Cannot create log directory: {e}"
362
+
363
+ if key in ("FALKORDB_PATH", "FALKORDB_SOCKET_PATH"):
364
+ # Validate path is writable
365
+ db_path = Path(value)
366
+ try:
367
+ db_path.parent.mkdir(parents=True, exist_ok=True)
368
+ except Exception as e:
369
+ return False, f"Cannot create directory for {key}: {e}"
370
+
371
+ # Check if parent directory is writable
372
+ if not os.access(db_path.parent, os.W_OK):
373
+ return False, f"Directory {db_path.parent} is not writable"
374
+
375
+ return True, None
376
+
377
+
378
+ def get_config_value(key: str) -> Optional[str]:
379
+ """Get a specific configuration value."""
380
+ config = load_config()
381
+ return config.get(key)
382
+
383
+
384
+ def set_config_value(key: str, value: str) -> bool:
385
+ """Set a configuration value. Returns True if successful.
386
+
387
+ The special key ``mode`` is delegated to :func:`set_context_mode` so
388
+ that ``cgc config set mode named`` works as expected.
389
+ """
390
+ if key.lower() == "mode":
391
+ return set_context_mode(value.lower())
392
+
393
+ # Ensure config directory exists
394
+ ensure_config_dir()
395
+
396
+ # Validate
397
+ is_valid, error_msg = validate_config_value(key, value)
398
+ if not is_valid:
399
+ console.print(f"[red]❌ {error_msg}[/red]")
400
+ return False
401
+
402
+ # Load, update, and save
403
+ config = load_config()
404
+ config[key] = value
405
+ save_config(config)
406
+
407
+ console.print(f"[green]✅ Set {key} = {value}[/green]")
408
+ return True
409
+
410
+
411
+ def reset_config():
412
+ """Reset configuration to defaults (preserves database credentials)."""
413
+ ensure_config_dir()
414
+ save_config(DEFAULT_CONFIG.copy(), preserve_db_credentials=True)
415
+ console.print("[green]✅ Configuration reset to defaults[/green]")
416
+ console.print("[cyan]Note: Database credentials were preserved[/cyan]")
417
+
418
+
419
+
420
+ _FIRST_RUN_MARKER = CONFIG_DIR / ".first_run_done"
421
+
422
+
423
+ def _print_welcome_banner() -> None:
424
+ """Print a one-time welcome message explaining the context system."""
425
+ console.print()
426
+ console.print("[bold green]Welcome to CodeGraphContext![/bold green]")
427
+ console.print()
428
+ console.print("CGC organises your code graphs using [bold]contexts[/bold]:")
429
+ console.print(" [cyan]global[/cyan] - One shared graph for all projects (default)")
430
+ console.print(" [cyan]per-repo[/cyan] - Each repo gets its own .codegraphcontext/ folder")
431
+ console.print(" [cyan]named[/cyan] - Create named workspaces (e.g. cgc index . --context MyProject)")
432
+ console.print()
433
+ console.print("Switch modes anytime: [dim]cgc context mode <global|per-repo|named>[/dim]")
434
+ console.print("Or: [dim]cgc config set mode <global|per-repo|named>[/dim]")
435
+ console.print()
436
+
437
+
438
+ def ensure_first_run_bootstrap() -> bool:
439
+ """Run one-time setup for brand-new installs.
440
+
441
+ Creates default config files, the global .cgcignore, and prints a
442
+ welcome banner. Returns True when bootstrap was performed.
443
+ """
444
+ if _FIRST_RUN_MARKER.exists():
445
+ return False
446
+
447
+ ensure_config_dir()
448
+ ensure_global_cgcignore()
449
+ # Ensure config.yaml exists (triggers creation with defaults)
450
+ load_context_config()
451
+ _print_welcome_banner()
452
+
453
+ # Stamp so we don't repeat
454
+ _FIRST_RUN_MARKER.parent.mkdir(parents=True, exist_ok=True)
455
+ _FIRST_RUN_MARKER.write_text("1")
456
+ return True
457
+
458
+
459
+ def ensure_config_file():
460
+ """
461
+ Create default .env config file on first run if it does not exist.
462
+ """
463
+ ensure_config_dir()
464
+
465
+ if CONFIG_FILE.exists():
466
+ return False # file already exists
467
+
468
+ save_config(DEFAULT_CONFIG.copy(), preserve_db_credentials=False)
469
+ return True # file was created
470
+
471
+
472
+
473
+
474
+ def show_config():
475
+ """Display current configuration in a nice table."""
476
+ created = ensure_config_file()
477
+ if created:
478
+ console.print(
479
+ f"[green]🆕 Created default configuration at {CONFIG_FILE}[/green]\n"
480
+ )
481
+ config = load_config()
482
+
483
+ # Separate database credentials from configuration
484
+ db_creds = {k: v for k, v in config.items() if k in DATABASE_CREDENTIAL_KEYS}
485
+ config_settings = {k: v for k, v in config.items() if k not in DATABASE_CREDENTIAL_KEYS}
486
+
487
+ # Show database credentials if they exist
488
+ if db_creds:
489
+ console.print("\n[bold cyan]Database Credentials[/bold cyan]")
490
+ db_table = Table(show_header=True, header_style="bold magenta")
491
+ db_table.add_column("Credential", style="cyan", width=20)
492
+ db_table.add_column("Value", style="green", width=30)
493
+
494
+ for key in sorted(db_creds.keys()):
495
+ value = db_creds[key]
496
+ # Mask password
497
+ if "PASSWORD" in key:
498
+ value = "********" if value else "(not set)"
499
+ db_table.add_row(key, value)
500
+
501
+ console.print(db_table)
502
+
503
+ # Show configuration settings
504
+ console.print("\n[bold cyan]Configuration Settings[/bold cyan]")
505
+ table = Table(show_header=True, header_style="bold magenta")
506
+ table.add_column("Setting", style="cyan", width=25)
507
+ table.add_column("Value", style="green", width=20)
508
+ table.add_column("Description", style="dim", width=50)
509
+
510
+ for key in sorted(config_settings.keys()):
511
+ value = config_settings[key]
512
+ description = CONFIG_DESCRIPTIONS.get(key, "")
513
+
514
+ # Highlight non-default values
515
+ if value != DEFAULT_CONFIG.get(key):
516
+ value_style = "[bold yellow]" + value + "[/bold yellow]"
517
+ else:
518
+ value_style = value
519
+
520
+ table.add_row(key, value_style, description)
521
+
522
+ console.print(table)
523
+ console.print(f"\n[cyan]Config file: {CONFIG_FILE}[/cyan]")
524
+
525
+
526
+ # =============================================================================
527
+ # CONTEXT SYSTEM (config.yaml)
528
+ # =============================================================================
529
+
530
+ CONTEXT_CONFIG_FILE = CONFIG_DIR / "config.yaml"
531
+ _LEGACY_CONTEXT_CONFIG_FILE = CONFIG_DIR / "cgc_config.yaml"
532
+
533
+ # Valid mode values
534
+ VALID_MODES = ["global", "per-repo", "named"]
535
+
536
+
537
+ @dataclass
538
+ class ContextInfo:
539
+ """Metadata for a single named context."""
540
+ name: str
541
+ database: str = "falkordb" # neo4j | falkordb | kuzudb
542
+ db_path: str = "" # resolved at init if empty
543
+ repos: List[str] = field(default_factory=list)
544
+ cgcignore_path: str = "" # resolved at init if empty
545
+
546
+
547
+ @dataclass
548
+ class ContextConfig:
549
+ """Top-level structure stored in ~/.codegraphcontext/config.yaml."""
550
+ version: int = 1
551
+ mode: str = "global" # global | per-repo | named
552
+ default_context: str = "" # used when mode=named and no --context flag
553
+ contexts: Dict[str, ContextInfo] = field(default_factory=dict)
554
+
555
+
556
+ def _default_db_path(context_name: str, database: str) -> str:
557
+ """Return the canonical DB path for a named context."""
558
+ return str(CONFIG_DIR / "contexts" / context_name / "db" / database)
559
+
560
+
561
+ _LEGACY_FALKORDB_PATH = CONFIG_DIR / "global" / "falkordb.db"
562
+
563
+
564
+ def _default_global_db_path(database: str) -> str:
565
+ """Return the canonical DB path for the global context.
566
+
567
+ New layout: ``~/.codegraphcontext/global/db/<backend>/``
568
+ For backward-compat, if the legacy flat path exists we keep using it.
569
+ """
570
+ if database == "falkordb" and _LEGACY_FALKORDB_PATH.exists():
571
+ return str(_LEGACY_FALKORDB_PATH)
572
+ return str(CONFIG_DIR / "global" / "db" / database)
573
+
574
+
575
+ def _migrate_legacy_config_yaml() -> None:
576
+ """Rename cgc_config.yaml -> config.yaml if the old name exists and the new one does not."""
577
+ if _LEGACY_CONTEXT_CONFIG_FILE.exists() and not CONTEXT_CONFIG_FILE.exists():
578
+ import shutil
579
+ shutil.copy2(_LEGACY_CONTEXT_CONFIG_FILE, CONTEXT_CONFIG_FILE)
580
+ console.print(f"[dim]Migrated {_LEGACY_CONTEXT_CONFIG_FILE.name} -> {CONTEXT_CONFIG_FILE.name}[/dim]")
581
+
582
+
583
+ def load_context_config() -> ContextConfig:
584
+ """
585
+ Load ~/.codegraphcontext/config.yaml.
586
+ Returns a ContextConfig with defaults if the file does not exist.
587
+ """
588
+ _migrate_legacy_config_yaml()
589
+
590
+ if not CONTEXT_CONFIG_FILE.exists():
591
+ cfg = ContextConfig()
592
+ save_context_config(cfg)
593
+ return cfg
594
+
595
+ try:
596
+ with open(CONTEXT_CONFIG_FILE, "r") as f:
597
+ raw = yaml.safe_load(f) or {}
598
+
599
+ contexts: Dict[str, ContextInfo] = {}
600
+ for name, meta in raw.get("contexts", {}).items():
601
+ meta = meta or {}
602
+ db = meta.get("database", "falkordb")
603
+ ctx = ContextInfo(
604
+ name=name,
605
+ database=db,
606
+ db_path=meta.get("db_path") or _default_db_path(name, db),
607
+ repos=[str(r) for r in meta.get("repos", [])],
608
+ cgcignore_path=meta.get("cgcignore_path") or str(
609
+ CONFIG_DIR / "contexts" / name / ".cgcignore"
610
+ ),
611
+ )
612
+ contexts[name] = ctx
613
+
614
+ return ContextConfig(
615
+ version=raw.get("version", 1),
616
+ mode=raw.get("mode", "global"),
617
+ default_context=raw.get("default_context", ""),
618
+ contexts=contexts,
619
+ )
620
+ except Exception as e:
621
+ console.print(f"[yellow]Warning: Could not load config.yaml: {e}. Using defaults.[/yellow]")
622
+ return ContextConfig()
623
+
624
+
625
+ def save_context_config(cfg: ContextConfig) -> None:
626
+ """Persist ContextConfig to ~/.codegraphcontext/config.yaml."""
627
+ ensure_config_dir()
628
+
629
+ contexts_raw: Dict[str, Any] = {}
630
+ for name, ctx in cfg.contexts.items():
631
+ contexts_raw[name] = {
632
+ "database": ctx.database,
633
+ "db_path": ctx.db_path,
634
+ "repos": ctx.repos,
635
+ "cgcignore_path": ctx.cgcignore_path,
636
+ }
637
+
638
+ raw = {
639
+ "version": cfg.version,
640
+ "mode": cfg.mode,
641
+ "default_context": cfg.default_context,
642
+ "contexts": contexts_raw,
643
+ }
644
+
645
+ try:
646
+ with open(CONTEXT_CONFIG_FILE, "w") as f:
647
+ yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
648
+ except Exception as e:
649
+ console.print(f"[red]Error saving config.yaml: {e}[/red]")
650
+
651
+
652
+ # ---------------------------------------------------------------------------
653
+ # Context Resolution
654
+ # ---------------------------------------------------------------------------
655
+
656
+ @dataclass
657
+ class ResolvedContext:
658
+ """Result of resolve_context() — everything needed to instantiate the DB."""
659
+ mode: str # global | per-repo | named
660
+ context_name: str # empty for global / per-repo
661
+ database: str # neo4j | falkordb | kuzudb
662
+ db_path: str # absolute path to the DB directory
663
+ cgcignore_path: str # path to the applicable .cgcignore
664
+ is_local: bool = False # True when a local .codegraphcontext/ was found
665
+
666
+
667
+ def find_local_cgc_dir(start: Optional[Path] = None) -> Optional[Path]:
668
+ """
669
+ Check *start* (default: cwd) for a ``.codegraphcontext/`` directory that
670
+ belongs to a repo (i.e. is NOT the global ``~/.codegraphcontext``).
671
+ Returns the directory path or None.
672
+ """
673
+ current = start or Path.cwd()
674
+ global_dir = CONFIG_DIR.resolve()
675
+
676
+ candidate = current / ".codegraphcontext"
677
+ if candidate.exists() and candidate.resolve() != global_dir:
678
+ return candidate
679
+ return None
680
+
681
+
682
+ def resolve_context(
683
+ cli_context: Optional[str] = None,
684
+ cwd: Optional[Path] = None,
685
+ ) -> ResolvedContext:
686
+ """
687
+ Determine which context / DB to use.
688
+
689
+ Resolution order (highest priority first):
690
+ 1. --context <name> CLI flag
691
+ 2. Local .codegraphcontext/ directory (per-repo)
692
+ 3. Global config.yaml: mode + default_context
693
+ 4. Ultimate fallback: global mode, default DB
694
+ """
695
+ cfg = load_context_config()
696
+ cwd = cwd or Path.cwd()
697
+
698
+ # --- 1. Explicit CLI flag ---
699
+ if cli_context:
700
+ ctx = cfg.contexts.get(cli_context)
701
+ db = ctx.database if ctx else "falkordb"
702
+ db_path = ctx.db_path if ctx else _default_db_path(cli_context, db)
703
+ cgcignore = (
704
+ ctx.cgcignore_path
705
+ if ctx
706
+ else str(CONFIG_DIR / "contexts" / cli_context / ".cgcignore")
707
+ )
708
+ return ResolvedContext(
709
+ mode="named",
710
+ context_name=cli_context,
711
+ database=db,
712
+ db_path=db_path,
713
+ cgcignore_path=cgcignore,
714
+ )
715
+
716
+ # --- 2. Local .codegraphcontext/ in repo ---
717
+ local_cgc = find_local_cgc_dir(cwd)
718
+
719
+ # If we are in per-repo mode and no local folder was found, create it in CWD
720
+ if local_cgc is None and cfg.mode == "per-repo":
721
+ local_cgc = cwd / ".codegraphcontext"
722
+ local_cgc.mkdir(parents=True, exist_ok=True)
723
+ (local_cgc / "db").mkdir(exist_ok=True)
724
+
725
+ # Copy global .env into local context for easy per-repo tweaking
726
+ import shutil
727
+ if CONFIG_FILE.exists():
728
+ shutil.copy2(CONFIG_FILE, local_cgc / ".env")
729
+
730
+ console.print(f"[dim]Auto-initialized per-repo context at {local_cgc}[/dim]")
731
+
732
+ if local_cgc is not None:
733
+ # Read local config.yaml if present
734
+ local_yaml = local_cgc / "config.yaml"
735
+ local_db = "falkordb"
736
+ if local_yaml.exists():
737
+ try:
738
+ with open(local_yaml) as f:
739
+ local_raw = yaml.safe_load(f) or {}
740
+ local_db = local_raw.get("database", "falkordb")
741
+ except Exception:
742
+ pass
743
+ db_path = str(local_cgc / "db" / local_db)
744
+ cgcignore = str(local_cgc / ".cgcignore")
745
+ return ResolvedContext(
746
+ mode="per-repo",
747
+ context_name="",
748
+ database=local_db,
749
+ db_path=db_path,
750
+ cgcignore_path=cgcignore,
751
+ is_local=True,
752
+ )
753
+
754
+ # --- 2b. Saved workspace mapping (CWD -> child .codegraphcontext/) ---
755
+ mapping = get_workspace_mapping(cwd)
756
+ if mapping:
757
+ mapped_ctx_path = Path(mapping["context_path"])
758
+ if mapped_ctx_path.exists() and mapped_ctx_path.is_dir():
759
+ mapped_db = mapping.get("database", "falkordb")
760
+ return ResolvedContext(
761
+ mode="per-repo",
762
+ context_name="",
763
+ database=mapped_db,
764
+ db_path=str(mapped_ctx_path / "db" / mapped_db),
765
+ cgcignore_path=str(mapped_ctx_path / ".cgcignore"),
766
+ is_local=True,
767
+ )
768
+
769
+ # --- 3. Global config.yaml ---
770
+ if cfg.mode == "named":
771
+ ctx_name = cfg.default_context
772
+ ctx = cfg.contexts.get(ctx_name) if ctx_name else None
773
+ db = ctx.database if ctx else "falkordb"
774
+ db_path = ctx.db_path if ctx else _default_db_path(ctx_name, db) if ctx_name else ""
775
+ if not db_path:
776
+ # No default context set — fall through to global
777
+ pass
778
+ else:
779
+ cgcignore = (
780
+ ctx.cgcignore_path
781
+ if ctx
782
+ else str(CONFIG_DIR / "contexts" / ctx_name / ".cgcignore")
783
+ )
784
+ return ResolvedContext(
785
+ mode="named",
786
+ context_name=ctx_name,
787
+ database=db,
788
+ db_path=db_path,
789
+ cgcignore_path=cgcignore,
790
+ )
791
+
792
+ # --- 4. Global fallback ---
793
+ db = load_config().get("DEFAULT_DATABASE", "falkordb")
794
+ return ResolvedContext(
795
+ mode="global",
796
+ context_name="",
797
+ database=db,
798
+ db_path=_default_global_db_path(db),
799
+ cgcignore_path=str(CONFIG_DIR / "global" / ".cgcignore"),
800
+ )
801
+
802
+
803
+ # ---------------------------------------------------------------------------
804
+ # Context CRUD helpers
805
+ # ---------------------------------------------------------------------------
806
+
807
+ def create_context(
808
+ name: str,
809
+ database: str = "falkordb",
810
+ db_path: Optional[str] = None,
811
+ ) -> bool:
812
+ """Create a new named context. Returns True on success."""
813
+ cfg = load_context_config()
814
+ if name in cfg.contexts:
815
+ console.print(f"[yellow]Context '{name}' already exists.[/yellow]")
816
+ return False
817
+
818
+ resolved_db_path = db_path or _default_db_path(name, database)
819
+ cgcignore = str(CONFIG_DIR / "contexts" / name / ".cgcignore")
820
+
821
+ # Ensure the context directories exist (create parent of db_path so DBs can create their files/dirs)
822
+ Path(resolved_db_path).parent.mkdir(parents=True, exist_ok=True)
823
+ Path(cgcignore).parent.mkdir(parents=True, exist_ok=True)
824
+
825
+ cfg.contexts[name] = ContextInfo(
826
+ name=name,
827
+ database=database,
828
+ db_path=resolved_db_path,
829
+ repos=[],
830
+ cgcignore_path=cgcignore,
831
+ )
832
+ save_context_config(cfg)
833
+ console.print(f"[green]✅ Created context '{name}' (DB: {database})[/green]")
834
+ console.print(f" [dim]DB path: {resolved_db_path}[/dim]")
835
+ return True
836
+
837
+
838
+ def delete_context(name: str) -> bool:
839
+ """Delete a named context from the registry. Returns True on success."""
840
+ cfg = load_context_config()
841
+ if name not in cfg.contexts:
842
+ console.print(f"[red]Context '{name}' not found.[/red]")
843
+ return False
844
+ del cfg.contexts[name]
845
+ if cfg.default_context == name:
846
+ cfg.default_context = ""
847
+ save_context_config(cfg)
848
+ console.print(f"[green]✅ Deleted context '{name}'[/green]")
849
+ console.print("[dim]Note: DB files were NOT deleted. Remove manually if needed.[/dim]")
850
+ return True
851
+
852
+
853
+ def register_repo_in_context(context_name: str, repo_path: str, auto_create: bool = False) -> bool:
854
+ """Add a repo path to a named context (idempotent).
855
+
856
+ When *auto_create* is True the context is silently created with default
857
+ settings if it does not yet exist, matching the UX of
858
+ ``cgc index ./frontend --context ProjectAB`` "just working".
859
+ """
860
+ cfg = load_context_config()
861
+ ctx = cfg.contexts.get(context_name)
862
+ if ctx is None:
863
+ if not auto_create:
864
+ console.print(f"[red]Context '{context_name}' not found. Create it first with 'cgc context create {context_name}'.[/red]")
865
+ return False
866
+ create_context(context_name)
867
+ cfg = load_context_config()
868
+ ctx = cfg.contexts.get(context_name)
869
+ if ctx is None:
870
+ return False
871
+ resolved = str(Path(repo_path).resolve())
872
+ if resolved not in ctx.repos:
873
+ ctx.repos.append(resolved)
874
+ save_context_config(cfg)
875
+ return True
876
+
877
+
878
+ def set_context_mode(mode: str) -> bool:
879
+ """Set the global CGC mode. Returns True on success."""
880
+ if mode not in VALID_MODES:
881
+ console.print(f"[red]Invalid mode '{mode}'. Must be one of: {', '.join(VALID_MODES)}[/red]")
882
+ return False
883
+ cfg = load_context_config()
884
+ cfg.mode = mode
885
+ save_context_config(cfg)
886
+ console.print(f"[green]✅ Mode set to '{mode}'[/green]")
887
+ return True
888
+
889
+
890
+ def set_default_context(name: str) -> bool:
891
+ """Set the default named context used when no --context flag is given."""
892
+ cfg = load_context_config()
893
+ if name and name not in cfg.contexts:
894
+ console.print(f"[red]Context '{name}' not found. Create it first.[/red]")
895
+ return False
896
+ cfg.default_context = name
897
+ save_context_config(cfg)
898
+ console.print(f"[green]✅ Default context set to '{name}'[/green]")
899
+ return True
900
+
901
+
902
+ def list_contexts() -> List[ContextInfo]:
903
+ """Return all named contexts."""
904
+ cfg = load_context_config()
905
+ return list(cfg.contexts.values())
906
+
907
+
908
+ # =============================================================================
909
+ # CHILD CONTEXT DISCOVERY
910
+ # =============================================================================
911
+
912
+ @dataclass
913
+ class DiscoveredContext:
914
+ """A .codegraphcontext folder found in a child directory."""
915
+ path: str # absolute path to the parent repo directory
916
+ cgc_path: str # absolute path to the .codegraphcontext directory
917
+ repo_name: str # name of the parent directory
918
+ database: str # backend from local config.yaml, or default
919
+ db_path: str # resolved db path
920
+ cgcignore_path: str # path to .cgcignore if present
921
+
922
+
923
+ def discover_child_contexts(
924
+ start: Optional[Path] = None,
925
+ max_depth: int = 1,
926
+ ) -> List[DiscoveredContext]:
927
+ """Walk child directories of *start* up to *max_depth* levels looking for
928
+ ``.codegraphcontext/`` folders that represent per-repo databases.
929
+
930
+ Returns a list of :class:`DiscoveredContext` for each match found.
931
+ The global ``~/.codegraphcontext`` is always excluded.
932
+ """
933
+ start = (start or Path.cwd()).resolve()
934
+ global_dir = CONFIG_DIR.resolve()
935
+ results: List[DiscoveredContext] = []
936
+
937
+ def _scan(directory: Path, depth: int) -> None:
938
+ if depth > max_depth:
939
+ return
940
+ try:
941
+ entries = sorted(directory.iterdir())
942
+ except PermissionError:
943
+ return
944
+ for entry in entries:
945
+ if not entry.is_dir() or entry.name.startswith("."):
946
+ continue
947
+ candidate = entry / ".codegraphcontext"
948
+ if candidate.exists() and candidate.is_dir() and candidate.resolve() != global_dir:
949
+ local_db = "falkordb"
950
+ local_yaml = candidate / "config.yaml"
951
+ if local_yaml.exists():
952
+ try:
953
+ with open(local_yaml) as f:
954
+ raw = yaml.safe_load(f) or {}
955
+ local_db = raw.get("database", "falkordb")
956
+ except Exception:
957
+ pass
958
+ results.append(DiscoveredContext(
959
+ path=str(entry),
960
+ cgc_path=str(candidate),
961
+ repo_name=entry.name,
962
+ database=local_db,
963
+ db_path=str(candidate / "db" / local_db),
964
+ cgcignore_path=str(candidate / ".cgcignore"),
965
+ ))
966
+ if depth < max_depth:
967
+ _scan(entry, depth + 1)
968
+
969
+ _scan(start, 1)
970
+ return results
971
+
972
+
973
+ # =============================================================================
974
+ # WORKSPACE MAPPINGS (global persistence of CWD -> context path)
975
+ # =============================================================================
976
+
977
+ def _load_workspace_mappings() -> Dict[str, Dict[str, str]]:
978
+ """Load the ``workspace_mappings`` section from config.yaml."""
979
+ if not CONTEXT_CONFIG_FILE.exists():
980
+ return {}
981
+ try:
982
+ with open(CONTEXT_CONFIG_FILE, "r") as f:
983
+ raw = yaml.safe_load(f) or {}
984
+ return raw.get("workspace_mappings", {}) or {}
985
+ except Exception:
986
+ return {}
987
+
988
+
989
+ def _save_workspace_mappings(mappings: Dict[str, Dict[str, str]]) -> None:
990
+ """Write *mappings* back into the ``workspace_mappings`` key of config.yaml,
991
+ preserving all other keys."""
992
+ ensure_config_dir()
993
+ raw: Dict[str, Any] = {}
994
+ if CONTEXT_CONFIG_FILE.exists():
995
+ try:
996
+ with open(CONTEXT_CONFIG_FILE, "r") as f:
997
+ raw = yaml.safe_load(f) or {}
998
+ except Exception:
999
+ raw = {}
1000
+ raw["workspace_mappings"] = mappings
1001
+ try:
1002
+ with open(CONTEXT_CONFIG_FILE, "w") as f:
1003
+ yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
1004
+ except Exception as e:
1005
+ console.print(f"[red]Error saving workspace mappings: {e}[/red]")
1006
+
1007
+
1008
+ def get_workspace_mapping(cwd: Path) -> Optional[Dict[str, str]]:
1009
+ """Look up a saved workspace mapping for *cwd*.
1010
+
1011
+ Returns a dict with ``context_path`` and ``database`` keys, or None.
1012
+ """
1013
+ mappings = _load_workspace_mappings()
1014
+ return mappings.get(str(cwd.resolve()))
1015
+
1016
+
1017
+ def save_workspace_mapping(cwd: Path, context_path: Path) -> None:
1018
+ """Persist an association from *cwd* to a ``.codegraphcontext`` directory."""
1019
+ context_path = context_path.resolve()
1020
+ local_db = "falkordb"
1021
+ local_yaml = context_path / "config.yaml"
1022
+ if local_yaml.exists():
1023
+ try:
1024
+ with open(local_yaml) as f:
1025
+ raw = yaml.safe_load(f) or {}
1026
+ local_db = raw.get("database", "falkordb")
1027
+ except Exception:
1028
+ pass
1029
+
1030
+ mappings = _load_workspace_mappings()
1031
+ mappings[str(cwd.resolve())] = {
1032
+ "context_path": str(context_path),
1033
+ "database": local_db,
1034
+ }
1035
+ _save_workspace_mappings(mappings)
1036
+
1037
+
1038
+ def remove_workspace_mapping(cwd: Path) -> bool:
1039
+ """Delete a saved workspace mapping. Returns True if one was removed."""
1040
+ mappings = _load_workspace_mappings()
1041
+ key = str(cwd.resolve())
1042
+ if key in mappings:
1043
+ del mappings[key]
1044
+ _save_workspace_mappings(mappings)
1045
+ return True
1046
+ return False
1047
+
1048
+
1049
+ def list_workspace_mappings() -> Dict[str, Dict[str, str]]:
1050
+ """Return all saved workspace mappings."""
1051
+ return _load_workspace_mappings()