code-graph-builder 0.2.0__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 (93) hide show
  1. code_graph_builder/__init__.py +82 -0
  2. code_graph_builder/builder.py +366 -0
  3. code_graph_builder/cgb_cli.py +32 -0
  4. code_graph_builder/cli.py +564 -0
  5. code_graph_builder/commands_cli.py +1288 -0
  6. code_graph_builder/config.py +340 -0
  7. code_graph_builder/constants.py +708 -0
  8. code_graph_builder/embeddings/__init__.py +40 -0
  9. code_graph_builder/embeddings/qwen3_embedder.py +573 -0
  10. code_graph_builder/embeddings/vector_store.py +584 -0
  11. code_graph_builder/examples/__init__.py +0 -0
  12. code_graph_builder/examples/example_configuration.py +276 -0
  13. code_graph_builder/examples/example_kuzu_usage.py +109 -0
  14. code_graph_builder/examples/example_semantic_search_full.py +347 -0
  15. code_graph_builder/examples/generate_wiki.py +915 -0
  16. code_graph_builder/examples/graph_export_example.py +100 -0
  17. code_graph_builder/examples/rag_example.py +206 -0
  18. code_graph_builder/examples/test_cli_demo.py +129 -0
  19. code_graph_builder/examples/test_embedding_api.py +153 -0
  20. code_graph_builder/examples/test_kuzu_local.py +190 -0
  21. code_graph_builder/examples/test_rag_redis.py +390 -0
  22. code_graph_builder/graph_updater.py +605 -0
  23. code_graph_builder/guidance/__init__.py +1 -0
  24. code_graph_builder/guidance/agent.py +123 -0
  25. code_graph_builder/guidance/prompts.py +74 -0
  26. code_graph_builder/guidance/toolset.py +264 -0
  27. code_graph_builder/language_spec.py +536 -0
  28. code_graph_builder/mcp/__init__.py +21 -0
  29. code_graph_builder/mcp/api_doc_generator.py +764 -0
  30. code_graph_builder/mcp/file_editor.py +207 -0
  31. code_graph_builder/mcp/pipeline.py +777 -0
  32. code_graph_builder/mcp/server.py +161 -0
  33. code_graph_builder/mcp/tools.py +1800 -0
  34. code_graph_builder/models.py +115 -0
  35. code_graph_builder/parser_loader.py +344 -0
  36. code_graph_builder/parsers/__init__.py +7 -0
  37. code_graph_builder/parsers/call_processor.py +306 -0
  38. code_graph_builder/parsers/call_resolver.py +139 -0
  39. code_graph_builder/parsers/definition_processor.py +796 -0
  40. code_graph_builder/parsers/factory.py +119 -0
  41. code_graph_builder/parsers/import_processor.py +293 -0
  42. code_graph_builder/parsers/structure_processor.py +145 -0
  43. code_graph_builder/parsers/type_inference.py +143 -0
  44. code_graph_builder/parsers/utils.py +134 -0
  45. code_graph_builder/rag/__init__.py +68 -0
  46. code_graph_builder/rag/camel_agent.py +429 -0
  47. code_graph_builder/rag/client.py +298 -0
  48. code_graph_builder/rag/config.py +239 -0
  49. code_graph_builder/rag/cypher_generator.py +67 -0
  50. code_graph_builder/rag/llm_backend.py +210 -0
  51. code_graph_builder/rag/markdown_generator.py +352 -0
  52. code_graph_builder/rag/prompt_templates.py +440 -0
  53. code_graph_builder/rag/rag_engine.py +640 -0
  54. code_graph_builder/rag/review_report.md +172 -0
  55. code_graph_builder/rag/tests/__init__.py +3 -0
  56. code_graph_builder/rag/tests/test_camel_agent.py +313 -0
  57. code_graph_builder/rag/tests/test_client.py +221 -0
  58. code_graph_builder/rag/tests/test_config.py +177 -0
  59. code_graph_builder/rag/tests/test_markdown_generator.py +240 -0
  60. code_graph_builder/rag/tests/test_prompt_templates.py +160 -0
  61. code_graph_builder/services/__init__.py +39 -0
  62. code_graph_builder/services/graph_service.py +465 -0
  63. code_graph_builder/services/kuzu_service.py +665 -0
  64. code_graph_builder/services/memory_service.py +171 -0
  65. code_graph_builder/settings.py +75 -0
  66. code_graph_builder/tests/ACCEPTANCE_CRITERIA_PHASE2.md +401 -0
  67. code_graph_builder/tests/__init__.py +1 -0
  68. code_graph_builder/tests/run_acceptance_check.py +378 -0
  69. code_graph_builder/tests/test_api_find.py +231 -0
  70. code_graph_builder/tests/test_api_find_integration.py +226 -0
  71. code_graph_builder/tests/test_basic.py +78 -0
  72. code_graph_builder/tests/test_c_api_extraction.py +388 -0
  73. code_graph_builder/tests/test_call_resolution_scenarios.py +504 -0
  74. code_graph_builder/tests/test_embedder.py +411 -0
  75. code_graph_builder/tests/test_integration_semantic.py +434 -0
  76. code_graph_builder/tests/test_mcp_protocol.py +298 -0
  77. code_graph_builder/tests/test_mcp_user_flow.py +190 -0
  78. code_graph_builder/tests/test_rag.py +404 -0
  79. code_graph_builder/tests/test_settings.py +135 -0
  80. code_graph_builder/tests/test_step1_graph_build.py +264 -0
  81. code_graph_builder/tests/test_step2_api_docs.py +323 -0
  82. code_graph_builder/tests/test_step3_embedding.py +278 -0
  83. code_graph_builder/tests/test_vector_store.py +552 -0
  84. code_graph_builder/tools/__init__.py +40 -0
  85. code_graph_builder/tools/graph_query.py +495 -0
  86. code_graph_builder/tools/semantic_search.py +387 -0
  87. code_graph_builder/types.py +333 -0
  88. code_graph_builder/utils/__init__.py +0 -0
  89. code_graph_builder/utils/path_utils.py +30 -0
  90. code_graph_builder-0.2.0.dist-info/METADATA +321 -0
  91. code_graph_builder-0.2.0.dist-info/RECORD +93 -0
  92. code_graph_builder-0.2.0.dist-info/WHEEL +4 -0
  93. code_graph_builder-0.2.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,82 @@
1
+ """Code Graph Builder - 代码知识图谱构建库.
2
+
3
+ This library provides functionality to build knowledge graphs from source code,
4
+ supporting multiple programming languages and multiple storage backends.
5
+
6
+ Backends:
7
+ - Kùzu (default): Embedded graph database, no Docker required
8
+ - Memgraph: Full-featured graph database (requires Docker)
9
+ - Memory: In-memory storage, no persistence
10
+
11
+ Example:
12
+ >>> from code_graph_builder import CodeGraphBuilder
13
+ >>>
14
+ >>> # Using Kùzu (recommended, no Docker)
15
+ >>> builder = CodeGraphBuilder("/path/to/repo", backend="kuzu")
16
+ >>> result = builder.build_graph()
17
+ >>>
18
+ >>> # Using Memory (for testing)
19
+ >>> builder = CodeGraphBuilder("/path/to/repo", backend="memory")
20
+ >>> data = builder.export_graph()
21
+ """
22
+
23
+ from .builder import CodeGraphBuilder
24
+ from .config import (
25
+ ConfigValidator,
26
+ EmbeddingConfig,
27
+ KuzuConfig,
28
+ MemgraphConfig,
29
+ MemoryConfig,
30
+ OutputConfig,
31
+ ScanConfig,
32
+ )
33
+ from .embeddings import (
34
+ BaseEmbedder,
35
+ DummyEmbedder,
36
+ MemoryVectorStore,
37
+ QdrantVectorStore,
38
+ Qwen3Embedder,
39
+ SearchResult,
40
+ VectorRecord,
41
+ VectorStore,
42
+ cosine_similarity,
43
+ create_embedder,
44
+ create_vector_store,
45
+ last_token_pool,
46
+ )
47
+ from .services.kuzu_service import KuzuIngestor
48
+ from .services.memory_service import MemoryIngestor
49
+ from .types import BuildResult, GraphData, GraphSummary
50
+
51
+ __version__ = "0.1.0"
52
+ __all__ = [
53
+ # Main API
54
+ "CodeGraphBuilder",
55
+ "BuildResult",
56
+ "GraphData",
57
+ "GraphSummary",
58
+ # Backend implementations
59
+ "KuzuIngestor",
60
+ "MemoryIngestor",
61
+ # Configuration classes
62
+ "ConfigValidator",
63
+ "EmbeddingConfig",
64
+ "KuzuConfig",
65
+ "MemgraphConfig",
66
+ "MemoryConfig",
67
+ "OutputConfig",
68
+ "ScanConfig",
69
+ # Embeddings
70
+ "BaseEmbedder",
71
+ "DummyEmbedder",
72
+ "Qwen3Embedder",
73
+ "create_embedder",
74
+ "last_token_pool",
75
+ "VectorStore",
76
+ "MemoryVectorStore",
77
+ "QdrantVectorStore",
78
+ "VectorRecord",
79
+ "SearchResult",
80
+ "create_vector_store",
81
+ "cosine_similarity",
82
+ ]
@@ -0,0 +1,366 @@
1
+ """Code Graph Builder - Main API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from loguru import logger
10
+
11
+ from .config import ConfigValidator, EmbeddingConfig, KuzuConfig, MemgraphConfig, MemoryConfig, ScanConfig
12
+ from .constants import SupportedLanguage
13
+ from .graph_updater import GraphUpdater
14
+ from .parser_loader import load_parsers
15
+ from .services.graph_service import MemgraphIngestor
16
+ from .services.kuzu_service import KuzuIngestor
17
+ from .types import BuildResult, GraphData, GraphSummary, PropertyDict, ResultRow
18
+
19
+ if TYPE_CHECKING:
20
+ from tree_sitter import Parser
21
+
22
+ from .types import LanguageQueries
23
+
24
+
25
+ class CodeGraphBuilder:
26
+ """Main API for building code knowledge graphs.
27
+
28
+ Supports multiple backends:
29
+ - Memgraph: Full-featured graph database (requires Docker)
30
+ - Kùzu: Embedded graph database (no Docker, recommended for local use)
31
+ - Memory: In-memory storage only (no persistence)
32
+
33
+ Examples:
34
+ >>> # Method 1: Simple dict-based config (recommended for quick start)
35
+ >>> builder = CodeGraphBuilder(
36
+ ... repo_path="/path/to/repo",
37
+ ... backend="kuzu",
38
+ ... backend_config={"db_path": "./graph.db"}
39
+ ... )
40
+ >>> result = builder.build_graph()
41
+
42
+ >>> # Method 2: Using config dataclasses (type-safe)
43
+ >>> from code_graph_builder.config import KuzuConfig, ScanConfig
44
+ >>> builder = CodeGraphBuilder(
45
+ ... repo_path="/path/to/repo",
46
+ ... backend="kuzu",
47
+ ... backend_config=KuzuConfig(db_path="./graph.db"),
48
+ ... scan_config=ScanConfig(exclude_patterns={"tests", "docs"})
49
+ ... )
50
+
51
+ >>> # Method 3: Memgraph backend
52
+ >>> builder = CodeGraphBuilder(
53
+ ... repo_path="/path/to/repo",
54
+ ... backend="memgraph",
55
+ ... backend_config={"host": "localhost", "port": 7687}
56
+ ... )
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ repo_path: str | Path,
62
+ backend: str = "kuzu",
63
+ backend_config: dict[str, Any] | KuzuConfig | MemgraphConfig | MemoryConfig | None = None,
64
+ scan_config: dict[str, Any] | ScanConfig | None = None,
65
+ embedding_config: dict[str, Any] | EmbeddingConfig | None = None,
66
+ # Backward compatibility
67
+ db_config: dict | None = None,
68
+ exclude_paths: frozenset[str] | None = None,
69
+ unignore_paths: frozenset[str] | None = None,
70
+ ) -> None:
71
+ """Initialize the code graph builder.
72
+
73
+ Args:
74
+ repo_path: Path to the repository to analyze
75
+ backend: Database backend to use ("kuzu", "memgraph", or "memory")
76
+ backend_config: Backend configuration. Can be:
77
+ - dict: {"db_path": "...", "batch_size": 1000}
78
+ - KuzuConfig: Type-safe configuration for Kùzu
79
+ - MemgraphConfig: Type-safe configuration for Memgraph
80
+ - MemoryConfig: Type-safe configuration for Memory
81
+ scan_config: Scan configuration. Can be:
82
+ - dict: {"exclude_patterns": {"tests"}, "max_file_size": 1000000}
83
+ - ScanConfig: Type-safe scan configuration
84
+ # Deprecated (use backend_config and scan_config instead):
85
+ db_config: Deprecated, use backend_config
86
+ exclude_paths: Deprecated, use scan_config
87
+ unignore_paths: Deprecated, use scan_config
88
+ """
89
+ self.repo_path = Path(repo_path).resolve()
90
+ self.backend = backend.lower()
91
+
92
+ # Handle backward compatibility
93
+ if db_config is not None:
94
+ logger.warning("db_config is deprecated, use backend_config instead")
95
+ if backend_config is None:
96
+ backend_config = db_config
97
+
98
+ # Validate and normalize backend config
99
+ self.backend_config = ConfigValidator.validate_backend_config(self.backend, backend_config)
100
+
101
+ # Validate and normalize scan config
102
+ if scan_config is None:
103
+ scan_config = ScanConfig()
104
+ elif isinstance(scan_config, dict):
105
+ scan_config = ScanConfig(**scan_config)
106
+
107
+ # Handle backward compatibility for exclude_paths/unignore_paths
108
+ if exclude_paths is not None:
109
+ logger.warning("exclude_paths is deprecated, use scan_config instead")
110
+ scan_config.exclude_patterns.update(exclude_paths)
111
+ if unignore_paths is not None:
112
+ logger.warning("unignore_paths is deprecated, use scan_config instead")
113
+ scan_config.unignore_paths.update(unignore_paths)
114
+
115
+ self.scan_config = scan_config
116
+
117
+ # Validate and normalize embedding config
118
+ if embedding_config is None:
119
+ embedding_config = EmbeddingConfig(enabled=False)
120
+ elif isinstance(embedding_config, dict):
121
+ embedding_config = EmbeddingConfig(**embedding_config)
122
+ self.embedding_config = embedding_config
123
+
124
+ self._parsers: dict[SupportedLanguage, Parser] | None = None
125
+ self._queries: dict[SupportedLanguage, LanguageQueries] | None = None
126
+ self._ingestor: MemgraphIngestor | KuzuIngestor | None = None
127
+ self._embedder: Any | None = None
128
+ self._vector_store: Any | None = None
129
+
130
+ def _load_parsers(self) -> None:
131
+ """Load Tree-sitter parsers for supported languages."""
132
+ if self._parsers is None or self._queries is None:
133
+ self._parsers, self._queries = load_parsers()
134
+ logger.info(f"Loaded parsers for {len(self._parsers)} languages")
135
+
136
+ def _get_ingestor(self) -> MemgraphIngestor | KuzuIngestor | Any:
137
+ """Get or create the graph ingestor based on backend."""
138
+ if self._ingestor is None:
139
+ if self.backend == "memgraph":
140
+ host = self.backend_config.get("host", "localhost")
141
+ port = self.backend_config.get("port", 7687)
142
+ batch_size = self.backend_config.get("batch_size", 1000)
143
+ self._ingestor = MemgraphIngestor(host, port, batch_size)
144
+ elif self.backend == "kuzu":
145
+ db_path = self.backend_config.get("db_path", f"./{self.repo_path.name}_graph.db")
146
+ batch_size = self.backend_config.get("batch_size", 1000)
147
+ self._ingestor = KuzuIngestor(db_path, batch_size)
148
+ elif self.backend == "memory":
149
+ from .services.memory_service import MemoryIngestor
150
+
151
+ self._ingestor = MemoryIngestor()
152
+ else:
153
+ raise ValueError(f"Unknown backend: {self.backend}. Use 'memgraph', 'kuzu', or 'memory'")
154
+ return self._ingestor
155
+
156
+ def _get_embedder_and_store(self) -> tuple[Any | None, Any | None]:
157
+ """Get or create the embedder and vector store if embedding is enabled."""
158
+ if not self.embedding_config.enabled:
159
+ return None, None
160
+
161
+ if self._embedder is None or self._vector_store is None:
162
+ from .embeddings.qwen3_embedder import create_embedder
163
+ from .embeddings.vector_store import create_vector_store
164
+
165
+ # Create embedder
166
+ self._embedder = create_embedder(
167
+ model_name=self.embedding_config.model_name,
168
+ device=self.embedding_config.device,
169
+ )
170
+
171
+ # Get embedding dimension
172
+ dimension = self.embedding_config.vector_dimension
173
+ if dimension is None:
174
+ dimension = self._embedder.get_embedding_dimension()
175
+
176
+ # Create vector store
177
+ self._vector_store = create_vector_store(
178
+ backend=self.embedding_config.vector_store_backend,
179
+ dimension=dimension,
180
+ db_path=self.embedding_config.vector_store_path,
181
+ )
182
+
183
+ return self._embedder, self._vector_store
184
+
185
+ def build_graph(self, clean: bool = False) -> BuildResult:
186
+ """Build the code knowledge graph.
187
+
188
+ Args:
189
+ clean: If True, clean the database before building
190
+
191
+ Returns:
192
+ BuildResult with statistics about the build
193
+ """
194
+ self._load_parsers()
195
+ ingestor = self._get_ingestor()
196
+
197
+ with ingestor:
198
+ if clean:
199
+ ingestor.clean_database()
200
+
201
+ # Get embedder and vector store if embedding is enabled
202
+ embedder, vector_store = self._get_embedder_and_store()
203
+
204
+ updater = GraphUpdater(
205
+ ingestor=ingestor,
206
+ repo_path=self.repo_path,
207
+ parsers=self._parsers,
208
+ queries=self._queries,
209
+ unignore_paths=frozenset(self.scan_config.unignore_paths),
210
+ exclude_paths=frozenset(self.scan_config.exclude_patterns),
211
+ embedder=embedder,
212
+ vector_store=vector_store,
213
+ embedding_config=self.embedding_config.to_dict(),
214
+ )
215
+
216
+ updater.run()
217
+
218
+ # Get statistics
219
+ if hasattr(ingestor, 'get_statistics'):
220
+ stats = ingestor.get_statistics()
221
+ total_nodes = stats.get("node_count", 0)
222
+ total_rels = stats.get("relationship_count", 0)
223
+ else:
224
+ # Fallback for ingestors without get_statistics
225
+ total_nodes = 0
226
+ total_rels = 0
227
+
228
+ return BuildResult(
229
+ project_name=self.repo_path.name,
230
+ nodes_created=total_nodes,
231
+ relationships_created=total_rels,
232
+ functions_found=0, # Will be updated from stats
233
+ classes_found=0,
234
+ files_processed=0,
235
+ errors=[],
236
+ )
237
+
238
+ def export_graph(self) -> GraphData:
239
+ """Export the graph data as a dictionary.
240
+
241
+ Returns:
242
+ GraphData containing nodes, relationships, and metadata
243
+ """
244
+ ingestor = self._get_ingestor()
245
+ with ingestor:
246
+ if hasattr(ingestor, 'export_graph'):
247
+ return ingestor.export_graph()
248
+ elif hasattr(ingestor, 'export_graph_to_dict'):
249
+ return ingestor.export_graph_to_dict()
250
+ else:
251
+ return {"nodes": [], "relationships": [], "metadata": {}}
252
+
253
+ def get_statistics(self) -> dict:
254
+ """Get statistics about the graph.
255
+
256
+ Returns:
257
+ Dictionary with node and relationship counts
258
+ """
259
+ ingestor = self._get_ingestor()
260
+ with ingestor:
261
+ if hasattr(ingestor, 'get_statistics'):
262
+ stats = ingestor.get_statistics()
263
+ if isinstance(stats, dict):
264
+ return {
265
+ "total_nodes": stats.get("node_count", 0),
266
+ "total_relationships": stats.get("relationship_count", 0),
267
+ "node_labels": stats.get("node_labels", {}),
268
+ "relationship_types": stats.get("relationship_types", {}),
269
+ }
270
+
271
+ # Fallback to export
272
+ if hasattr(ingestor, 'export_graph_to_dict'):
273
+ data = ingestor.export_graph_to_dict()
274
+ elif hasattr(ingestor, 'export_graph'):
275
+ data = ingestor.export_graph()
276
+ else:
277
+ data = {"nodes": [], "relationships": []}
278
+
279
+ # Count node labels
280
+ node_labels: dict[str, int] = {}
281
+ for node in data.get("nodes", []):
282
+ labels = node.get("labels", [])
283
+ for label in labels:
284
+ node_labels[label] = node_labels.get(label, 0) + 1
285
+
286
+ # Count relationship types
287
+ rel_types: dict[str, int] = {}
288
+ for rel in data.get("relationships", []):
289
+ rel_type = rel.get("type", "UNKNOWN")
290
+ rel_types[rel_type] = rel_types.get(rel_type, 0) + 1
291
+
292
+ return {
293
+ "total_nodes": len(data.get("nodes", [])),
294
+ "total_relationships": len(data.get("relationships", [])),
295
+ "node_labels": node_labels,
296
+ "relationship_types": rel_types,
297
+ "metadata": data.get("metadata", {}),
298
+ }
299
+
300
+ def query(self, cypher_query: str, params: PropertyDict | None = None) -> list[ResultRow]:
301
+ """Execute a Cypher query against the graph.
302
+
303
+ Args:
304
+ cypher_query: The Cypher query to execute
305
+ params: Optional query parameters
306
+
307
+ Returns:
308
+ List of result rows as dictionaries
309
+ """
310
+ ingestor = self._get_ingestor()
311
+ with ingestor:
312
+ if hasattr(ingestor, 'query'):
313
+ return ingestor.query(cypher_query, params)
314
+ elif hasattr(ingestor, 'fetch_all'):
315
+ return ingestor.fetch_all(cypher_query, params)
316
+ else:
317
+ return []
318
+
319
+ def get_function_source(self, qualified_name: str) -> str | None:
320
+ """Get the source code of a function by its qualified name.
321
+
322
+ Args:
323
+ qualified_name: The fully qualified name of the function
324
+
325
+ Returns:
326
+ The source code as a string, or None if not found
327
+ """
328
+ results = self.query(
329
+ """
330
+ MATCH (n)
331
+ WHERE n.qualified_name = $qn
332
+ RETURN n.name AS name, n.start_line AS start, n.end_line AS end
333
+ LIMIT 1
334
+ """,
335
+ {"qn": qualified_name},
336
+ )
337
+
338
+ if not results:
339
+ return None
340
+
341
+ result = results[0]
342
+ return f"Function: {result.get('name')} (lines {result.get('start')}-{result.get('end')})"
343
+
344
+ def list_projects(self) -> list[str]:
345
+ """List all projects in the database.
346
+
347
+ Returns:
348
+ List of project names
349
+ """
350
+ ingestor = self._get_ingestor()
351
+ with ingestor:
352
+ if hasattr(ingestor, 'list_projects'):
353
+ return ingestor.list_projects()
354
+ return []
355
+
356
+ def delete_project(self, project_name: str | None = None) -> None:
357
+ """Delete a project from the database.
358
+
359
+ Args:
360
+ project_name: Name of the project to delete (default: current project)
361
+ """
362
+ name = project_name or self.repo_path.name
363
+ ingestor = self._get_ingestor()
364
+ with ingestor:
365
+ if hasattr(ingestor, 'delete_project'):
366
+ ingestor.delete_project(name)
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ """Global CLI entry point for CodeGraphWiki custom commands.
3
+
4
+ This file lives at ~/.claude/commands/code-graph/cgb_cli.py and acts as
5
+ a thin wrapper that delegates to the installed code_graph_builder package.
6
+
7
+ Usage (called by the .md command files in this directory):
8
+ python3 ~/.claude/commands/code-graph/cgb_cli.py <subcommand> [args...]
9
+ """
10
+
11
+ import sys
12
+
13
+
14
+ def main():
15
+ try:
16
+ from code_graph_builder.commands_cli import main as cli_main
17
+ except ImportError:
18
+ print(
19
+ "ERROR: code_graph_builder package is not installed.\n"
20
+ "Run the following to install:\n"
21
+ " pip install /path/to/CodeGraphWiki\n"
22
+ "Or:\n"
23
+ " pip install -e /path/to/CodeGraphWiki",
24
+ file=sys.stderr,
25
+ )
26
+ sys.exit(1)
27
+
28
+ cli_main()
29
+
30
+
31
+ if __name__ == "__main__":
32
+ main()