codegraph-py 1.0.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.
- codegraph/__init__.py +12 -0
- codegraph/__main__.py +6 -0
- codegraph/cli.py +911 -0
- codegraph/codegraph.py +618 -0
- codegraph/context/__init__.py +216 -0
- codegraph/db/__init__.py +11 -0
- codegraph/db/connection.py +163 -0
- codegraph/db/queries.py +752 -0
- codegraph/db/schema.sql +151 -0
- codegraph/directory.py +160 -0
- codegraph/errors.py +101 -0
- codegraph/extraction/__init__.py +956 -0
- codegraph/extraction/languages/__init__.py +64 -0
- codegraph/extraction/languages/base.py +132 -0
- codegraph/extraction/languages/c_cfg.py +53 -0
- codegraph/extraction/languages/cpp_cfg.py +60 -0
- codegraph/extraction/languages/dart_cfg.py +53 -0
- codegraph/extraction/languages/go_cfg.py +70 -0
- codegraph/extraction/languages/java_cfg.py +62 -0
- codegraph/extraction/languages/javascript_cfg.py +84 -0
- codegraph/extraction/languages/kotlin_cfg.py +52 -0
- codegraph/extraction/languages/lua_cfg.py +65 -0
- codegraph/extraction/languages/python_cfg.py +75 -0
- codegraph/extraction/languages/ruby_cfg.py +48 -0
- codegraph/extraction/languages/rust_cfg.py +68 -0
- codegraph/extraction/languages/scala_cfg.py +59 -0
- codegraph/extraction/languages/swift_cfg.py +64 -0
- codegraph/extraction/languages/typescript_cfg.py +89 -0
- codegraph/extraction/tree_sitter_extractor.py +689 -0
- codegraph/graph/__init__.py +685 -0
- codegraph/installer/__init__.py +6 -0
- codegraph/mcp/__init__.py +668 -0
- codegraph/project_config.py +191 -0
- codegraph/resolution/__init__.py +337 -0
- codegraph/search/__init__.py +653 -0
- codegraph/sync/__init__.py +204 -0
- codegraph/types.py +334 -0
- codegraph/ui/__init__.py +11 -0
- codegraph/utils.py +218 -0
- codegraph_py-1.0.0.dist-info/METADATA +238 -0
- codegraph_py-1.0.0.dist-info/RECORD +44 -0
- codegraph_py-1.0.0.dist-info/WHEEL +5 -0
- codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
- codegraph_py-1.0.0.dist-info/top_level.txt +1 -0
codegraph/cli.py
ADDED
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeGraph CLI
|
|
3
|
+
|
|
4
|
+
Command-line interface for CodeGraph code intelligence.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional, List, Dict
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from codegraph.codegraph import CodeGraph
|
|
18
|
+
from codegraph.directory import (
|
|
19
|
+
is_initialized, find_nearest_codegraph_root,
|
|
20
|
+
unsafe_index_root_reason, derive_project_name_tokens,
|
|
21
|
+
)
|
|
22
|
+
from codegraph.types import SearchOptions, GraphStats
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# CLI Colors & Helpers
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
class Colors:
|
|
30
|
+
"""ANSI color helpers."""
|
|
31
|
+
RESET = '\x1b[0m'
|
|
32
|
+
BOLD = '\x1b[1m'
|
|
33
|
+
DIM = '\x1b[2m'
|
|
34
|
+
RED = '\x1b[31m'
|
|
35
|
+
GREEN = '\x1b[32m'
|
|
36
|
+
YELLOW = '\x1b[33m'
|
|
37
|
+
BLUE = '\x1b[34m'
|
|
38
|
+
CYAN = '\x1b[36m'
|
|
39
|
+
GRAY = '\x1b[90m'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def bold(s: str) -> str:
|
|
43
|
+
return f'{Colors.BOLD}{s}{Colors.RESET}'
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def dim(s: str) -> str:
|
|
47
|
+
return f'{Colors.DIM}{s}{Colors.RESET}'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def red(s: str) -> str:
|
|
51
|
+
return f'{Colors.RED}{s}{Colors.RESET}'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def green(s: str) -> str:
|
|
55
|
+
return f'{Colors.GREEN}{s}{Colors.RESET}'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def yellow(s: str) -> str:
|
|
59
|
+
return f'{Colors.YELLOW}{s}{Colors.RESET}'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def blue(s: str) -> str:
|
|
63
|
+
return f'{Colors.BLUE}{s}{Colors.RESET}'
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def format_number(n: int) -> str:
|
|
67
|
+
"""Format a number with commas."""
|
|
68
|
+
return f'{n:,}'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def format_duration(ms: float) -> str:
|
|
72
|
+
"""Format duration in milliseconds to human readable."""
|
|
73
|
+
if ms < 1000:
|
|
74
|
+
return f'{ms:.0f}ms'
|
|
75
|
+
seconds = ms / 1000
|
|
76
|
+
if seconds < 60:
|
|
77
|
+
return f'{seconds:.1f}s'
|
|
78
|
+
minutes = int(seconds / 60)
|
|
79
|
+
remaining = seconds % 60
|
|
80
|
+
return f'{minutes}m {remaining:.0f}s'
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_project_path(path_arg: Optional[str] = None) -> str:
|
|
84
|
+
"""Resolve project path from argument or current directory."""
|
|
85
|
+
absolute = os.path.abspath(path_arg or os.getcwd())
|
|
86
|
+
|
|
87
|
+
if is_initialized(absolute):
|
|
88
|
+
return absolute
|
|
89
|
+
|
|
90
|
+
# Walk up to find nearest parent with CodeGraph initialized
|
|
91
|
+
current = absolute
|
|
92
|
+
root = os.path.splitdrive(absolute)[0] + os.sep
|
|
93
|
+
|
|
94
|
+
while current != root:
|
|
95
|
+
parent = os.path.dirname(current)
|
|
96
|
+
if parent == current:
|
|
97
|
+
break
|
|
98
|
+
current = parent
|
|
99
|
+
if is_initialized(current):
|
|
100
|
+
return current
|
|
101
|
+
|
|
102
|
+
return absolute
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# =============================================================================
|
|
106
|
+
# CLI Commands
|
|
107
|
+
# =============================================================================
|
|
108
|
+
|
|
109
|
+
@click.group()
|
|
110
|
+
@click.version_option(version='1.0.0', prog_name='codegraph')
|
|
111
|
+
def main():
|
|
112
|
+
"""CodeGraph - Semantic code intelligence for AI coding agents.
|
|
113
|
+
|
|
114
|
+
Build a knowledge graph of your codebase for surgical context,
|
|
115
|
+
fewer tool calls, and faster answers.
|
|
116
|
+
"""
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@main.command()
|
|
121
|
+
@click.argument('path', required=False, default=None)
|
|
122
|
+
@click.option('-f', '--force', is_flag=True, help='Initialize even if path looks unsafe')
|
|
123
|
+
@click.option('-v', '--verbose', is_flag=True, help='Show detailed progress')
|
|
124
|
+
def init(path: Optional[str], force: bool, verbose: bool):
|
|
125
|
+
"""Initialize CodeGraph in a project directory and build initial index."""
|
|
126
|
+
project_path = os.path.abspath(path or os.getcwd())
|
|
127
|
+
|
|
128
|
+
# Check unsafe paths
|
|
129
|
+
unsafe = unsafe_index_root_reason(project_path)
|
|
130
|
+
if unsafe and not force:
|
|
131
|
+
click.echo(red(
|
|
132
|
+
f'Refusing to initialize in {project_path} ā it looks like {unsafe}.'
|
|
133
|
+
))
|
|
134
|
+
click.echo('Run this inside a specific project directory, or pass --force.')
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if is_initialized(project_path):
|
|
138
|
+
click.echo(yellow(f'Already initialized in {project_path}'))
|
|
139
|
+
click.echo('Use "codegraph index" to re-index or "codegraph sync" to update')
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
click.echo(bold('\nInitializing CodeGraph...\n'))
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
cg = CodeGraph.init_sync(project_path)
|
|
146
|
+
click.echo(green(f'ā Initialized in {project_path}'))
|
|
147
|
+
|
|
148
|
+
# Run initial index
|
|
149
|
+
click.echo('Indexing project...')
|
|
150
|
+
|
|
151
|
+
result = cg.index_all(verbose=verbose)
|
|
152
|
+
|
|
153
|
+
if result.success and result.files_indexed > 0:
|
|
154
|
+
click.echo(green(
|
|
155
|
+
f'ā Indexed {format_number(result.files_indexed)} files'
|
|
156
|
+
))
|
|
157
|
+
click.echo(
|
|
158
|
+
f'{format_number(result.nodes_created)} nodes, '
|
|
159
|
+
f'{format_number(result.edges_created)} edges '
|
|
160
|
+
f'in {format_duration(result.duration_ms)}'
|
|
161
|
+
)
|
|
162
|
+
elif result.files_errored > 0:
|
|
163
|
+
click.echo(yellow(
|
|
164
|
+
f'Indexed {format_number(result.files_indexed)} files '
|
|
165
|
+
f'({format_number(result.files_errored)} errors)'
|
|
166
|
+
))
|
|
167
|
+
else:
|
|
168
|
+
click.echo(yellow('No files found to index'))
|
|
169
|
+
|
|
170
|
+
cg.destroy()
|
|
171
|
+
click.echo(green('\nDone! Run "codegraph status" to see the results.'))
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@main.command()
|
|
179
|
+
@click.argument('path', required=False, default=None)
|
|
180
|
+
def uninit(path: Optional[str]):
|
|
181
|
+
"""Remove CodeGraph from a project (deletes .codegraph/ directory)."""
|
|
182
|
+
project_path = resolve_project_path(path)
|
|
183
|
+
|
|
184
|
+
if not is_initialized(project_path):
|
|
185
|
+
click.echo(yellow(f'CodeGraph is not initialized in {project_path}'))
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
click.echo(yellow(
|
|
189
|
+
f'This will permanently delete all CodeGraph data from {project_path}'
|
|
190
|
+
))
|
|
191
|
+
if not click.confirm('Continue?'):
|
|
192
|
+
click.echo('Cancelled')
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
cg = CodeGraph.open_sync(project_path)
|
|
197
|
+
cg.uninitialize()
|
|
198
|
+
click.echo(green(f'Removed CodeGraph from {project_path}'))
|
|
199
|
+
except Exception as e:
|
|
200
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@main.command()
|
|
205
|
+
@click.argument('path', required=False, default=None)
|
|
206
|
+
@click.option('-f', '--force', is_flag=True, help='Index even if path looks unsafe')
|
|
207
|
+
@click.option('-q', '--quiet', is_flag=True, help='Suppress progress output')
|
|
208
|
+
@click.option('-v', '--verbose', is_flag=True, help='Show detailed progress')
|
|
209
|
+
def index(path: Optional[str], force: bool, quiet: bool, verbose: bool):
|
|
210
|
+
"""Rebuild the full index from scratch."""
|
|
211
|
+
project_path = resolve_project_path(path)
|
|
212
|
+
|
|
213
|
+
# Check unsafe paths
|
|
214
|
+
unsafe = unsafe_index_root_reason(project_path)
|
|
215
|
+
if unsafe and not force:
|
|
216
|
+
click.echo(red(
|
|
217
|
+
f'Refusing to index {project_path} ā it looks like {unsafe}. '
|
|
218
|
+
f'Pass --force to override.'
|
|
219
|
+
))
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
if not is_initialized(project_path):
|
|
223
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
224
|
+
click.echo('Run "codegraph init" first')
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
click.echo(bold('\nIndexing project...\n'))
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
# Recreate database for fresh index
|
|
231
|
+
cg = CodeGraph.open_sync(project_path)
|
|
232
|
+
|
|
233
|
+
result = cg.index_all(verbose=verbose)
|
|
234
|
+
|
|
235
|
+
if not quiet:
|
|
236
|
+
if result.success and result.files_indexed > 0:
|
|
237
|
+
click.echo(green(
|
|
238
|
+
f'ā Indexed {format_number(result.files_indexed)} files'
|
|
239
|
+
))
|
|
240
|
+
click.echo(
|
|
241
|
+
f'{format_number(result.nodes_created)} nodes, '
|
|
242
|
+
f'{format_number(result.edges_created)} edges '
|
|
243
|
+
f'in {format_duration(result.duration_ms)}'
|
|
244
|
+
)
|
|
245
|
+
elif result.files_errored > 0:
|
|
246
|
+
click.echo(yellow(
|
|
247
|
+
f'Indexed {format_number(result.files_indexed)} files '
|
|
248
|
+
f'({format_number(result.files_errored)} errors)'
|
|
249
|
+
))
|
|
250
|
+
else:
|
|
251
|
+
click.echo(yellow('No files found to index'))
|
|
252
|
+
|
|
253
|
+
cg.destroy()
|
|
254
|
+
click.echo(green('\nDone!'))
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@main.command()
|
|
262
|
+
@click.argument('path', required=False, default=None)
|
|
263
|
+
@click.option('-q', '--quiet', is_flag=True, help='Suppress output')
|
|
264
|
+
def sync(path: Optional[str], quiet: bool):
|
|
265
|
+
"""Sync changes since last index."""
|
|
266
|
+
project_path = resolve_project_path(path)
|
|
267
|
+
|
|
268
|
+
if not is_initialized(project_path):
|
|
269
|
+
if not quiet:
|
|
270
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
cg = CodeGraph.open_sync(project_path)
|
|
275
|
+
|
|
276
|
+
result = cg.sync()
|
|
277
|
+
|
|
278
|
+
if not quiet:
|
|
279
|
+
total = result.files_added + result.files_modified + result.files_removed
|
|
280
|
+
if total == 0:
|
|
281
|
+
click.echo('Already up to date')
|
|
282
|
+
else:
|
|
283
|
+
click.echo(green(f'Synced {format_number(total)} changed files'))
|
|
284
|
+
details = []
|
|
285
|
+
if result.files_added > 0:
|
|
286
|
+
details.append(f'Added: {result.files_added}')
|
|
287
|
+
if result.files_modified > 0:
|
|
288
|
+
details.append(f'Modified: {result.files_modified}')
|
|
289
|
+
if result.files_removed > 0:
|
|
290
|
+
details.append(f'Removed: {result.files_removed}')
|
|
291
|
+
click.echo(
|
|
292
|
+
f'{", ".join(details)} ā '
|
|
293
|
+
f'{format_number(result.nodes_updated)} nodes '
|
|
294
|
+
f'in {format_duration(result.duration_ms)}'
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
cg.destroy()
|
|
298
|
+
if not quiet:
|
|
299
|
+
click.echo(green('Done!'))
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
if not quiet:
|
|
303
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@main.command()
|
|
308
|
+
@click.argument('path', required=False, default=None)
|
|
309
|
+
@click.option('-j', '--json', 'json_output', is_flag=True, help='Output as JSON')
|
|
310
|
+
def status(path: Optional[str], json_output: bool):
|
|
311
|
+
"""Show index status and statistics."""
|
|
312
|
+
project_path = resolve_project_path(path)
|
|
313
|
+
|
|
314
|
+
if not is_initialized(project_path):
|
|
315
|
+
if json_output:
|
|
316
|
+
click.echo(json.dumps({
|
|
317
|
+
'initialized': False,
|
|
318
|
+
'version': '1.0.0',
|
|
319
|
+
'projectPath': project_path,
|
|
320
|
+
}))
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
click.echo(bold('\nCodeGraph Status\n'))
|
|
324
|
+
click.echo(f'Project: {project_path}')
|
|
325
|
+
click.echo(yellow('Not initialized'))
|
|
326
|
+
click.echo('Run "codegraph init" to initialize')
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
cg = CodeGraph.open_sync(project_path)
|
|
331
|
+
stats = cg.get_stats()
|
|
332
|
+
changes = cg.get_changed_files()
|
|
333
|
+
build_info = cg.get_index_build_info()
|
|
334
|
+
|
|
335
|
+
if json_output:
|
|
336
|
+
click.echo(json.dumps({
|
|
337
|
+
'initialized': True,
|
|
338
|
+
'version': '1.0.0',
|
|
339
|
+
'projectPath': project_path,
|
|
340
|
+
'fileCount': stats.file_count,
|
|
341
|
+
'nodeCount': stats.node_count,
|
|
342
|
+
'edgeCount': stats.edge_count,
|
|
343
|
+
'dbSizeBytes': stats.db_size_bytes,
|
|
344
|
+
'backend': 'sqlite3',
|
|
345
|
+
'nodesByKind': stats.nodes_by_kind,
|
|
346
|
+
'languages': [k for k, v in stats.files_by_language.items() if v > 0],
|
|
347
|
+
'pendingChanges': {
|
|
348
|
+
'added': len(changes.get('added', [])),
|
|
349
|
+
'modified': len(changes.get('modified', [])),
|
|
350
|
+
'removed': len(changes.get('removed', [])),
|
|
351
|
+
},
|
|
352
|
+
}, indent=2))
|
|
353
|
+
cg.destroy()
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
click.echo(bold('\nCodeGraph Status\n'))
|
|
357
|
+
click.echo(f'{blue("Project:")} {project_path}')
|
|
358
|
+
click.echo()
|
|
359
|
+
|
|
360
|
+
# Index stats
|
|
361
|
+
click.echo(bold('Index Statistics:'))
|
|
362
|
+
click.echo(f' Files: {format_number(stats.file_count)}')
|
|
363
|
+
click.echo(f' Nodes: {format_number(stats.node_count)}')
|
|
364
|
+
click.echo(f' Edges: {format_number(stats.edge_count)}')
|
|
365
|
+
click.echo(f' DB Size: {stats.db_size_bytes / 1024 / 1024:.2f} MB')
|
|
366
|
+
click.echo(f' Backend: {green("sqlite3 ā built-in (full WAL)")}')
|
|
367
|
+
click.echo()
|
|
368
|
+
|
|
369
|
+
# Node breakdown
|
|
370
|
+
if stats.nodes_by_kind:
|
|
371
|
+
click.echo(bold('Nodes by Kind:'))
|
|
372
|
+
for kind, count in sorted(
|
|
373
|
+
stats.nodes_by_kind.items(),
|
|
374
|
+
key=lambda x: -x[1]
|
|
375
|
+
):
|
|
376
|
+
click.echo(f' {kind:15} {format_number(count)}')
|
|
377
|
+
click.echo()
|
|
378
|
+
|
|
379
|
+
# Pending changes
|
|
380
|
+
total = (
|
|
381
|
+
len(changes.get('added', []))
|
|
382
|
+
+ len(changes.get('modified', []))
|
|
383
|
+
+ len(changes.get('removed', []))
|
|
384
|
+
)
|
|
385
|
+
if total > 0:
|
|
386
|
+
click.echo(bold('Pending Changes:'))
|
|
387
|
+
if changes.get('added'):
|
|
388
|
+
click.echo(f' Added: {len(changes["added"])} files')
|
|
389
|
+
if changes.get('modified'):
|
|
390
|
+
click.echo(f' Modified: {len(changes["modified"])} files')
|
|
391
|
+
if changes.get('removed'):
|
|
392
|
+
click.echo(f' Removed: {len(changes["removed"])} files')
|
|
393
|
+
click.echo(blue('Run "codegraph sync" to update the index'))
|
|
394
|
+
else:
|
|
395
|
+
click.echo(green('Index is up to date'))
|
|
396
|
+
click.echo()
|
|
397
|
+
|
|
398
|
+
cg.destroy()
|
|
399
|
+
|
|
400
|
+
except Exception as e:
|
|
401
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
402
|
+
sys.exit(1)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@main.command()
|
|
406
|
+
@click.argument('search')
|
|
407
|
+
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
408
|
+
@click.option('-l', '--limit', default=10, type=int, help='Maximum results')
|
|
409
|
+
@click.option('-k', '--kind', help='Filter by node kind (function, class, etc.)')
|
|
410
|
+
@click.option('-j', '--json', 'json_output', is_flag=True, help='Output as JSON')
|
|
411
|
+
def query(search: str, path_arg: Optional[str], limit: int,
|
|
412
|
+
kind: Optional[str], json_output: bool):
|
|
413
|
+
"""Search for symbols in the codebase."""
|
|
414
|
+
project_path = resolve_project_path(path_arg)
|
|
415
|
+
|
|
416
|
+
if not is_initialized(project_path):
|
|
417
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
cg = CodeGraph.open_sync(project_path)
|
|
422
|
+
|
|
423
|
+
opts = SearchOptions(limit=limit)
|
|
424
|
+
if kind:
|
|
425
|
+
opts.kinds = [kind]
|
|
426
|
+
|
|
427
|
+
results = cg.search_nodes(search, opts)
|
|
428
|
+
|
|
429
|
+
if json_output:
|
|
430
|
+
output = []
|
|
431
|
+
for r in results:
|
|
432
|
+
output.append({
|
|
433
|
+
'node': {
|
|
434
|
+
'id': r.node.id,
|
|
435
|
+
'kind': r.node.kind,
|
|
436
|
+
'name': r.node.name,
|
|
437
|
+
'qualifiedName': r.node.qualified_name,
|
|
438
|
+
'filePath': r.node.file_path,
|
|
439
|
+
'startLine': r.node.start_line,
|
|
440
|
+
'endLine': r.node.end_line,
|
|
441
|
+
'signature': r.node.signature,
|
|
442
|
+
},
|
|
443
|
+
'score': r.score,
|
|
444
|
+
})
|
|
445
|
+
click.echo(json.dumps(output, indent=2))
|
|
446
|
+
cg.destroy()
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
if not results:
|
|
450
|
+
click.echo(f'No results found for "{search}"')
|
|
451
|
+
else:
|
|
452
|
+
click.echo(bold(f'\nSearch Results for "{search}":\n'))
|
|
453
|
+
for r in results:
|
|
454
|
+
node = r.node
|
|
455
|
+
location = f'{node.file_path}:{node.start_line}'
|
|
456
|
+
click.echo(f' {blue(node.kind.ljust(12))} {bold(node.name)}')
|
|
457
|
+
click.echo(f' {dim(" " + location)}')
|
|
458
|
+
if node.signature:
|
|
459
|
+
click.echo(f' {dim(" " + node.signature)}')
|
|
460
|
+
click.echo()
|
|
461
|
+
|
|
462
|
+
cg.destroy()
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
click.echo(red(f'Search failed: {str(e)}'))
|
|
466
|
+
sys.exit(1)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@main.command()
|
|
470
|
+
@click.argument('query_parts', nargs=-1, required=True)
|
|
471
|
+
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
472
|
+
@click.option('--max-files', type=int, default=12, help='Maximum files to include')
|
|
473
|
+
def explore(query_parts: List[str], path_arg: Optional[str], max_files: int):
|
|
474
|
+
"""Explore code: relevant symbols' source + call paths in one shot.
|
|
475
|
+
|
|
476
|
+
Same output as the codegraph_explore MCP tool.
|
|
477
|
+
"""
|
|
478
|
+
query = ' '.join(query_parts)
|
|
479
|
+
project_path = resolve_project_path(path_arg)
|
|
480
|
+
|
|
481
|
+
if not is_initialized(project_path):
|
|
482
|
+
click.echo(red(
|
|
483
|
+
f'CodeGraph not initialized in {project_path}. '
|
|
484
|
+
f'Run "codegraph init" first.'
|
|
485
|
+
))
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
cg = CodeGraph.open_sync(project_path)
|
|
490
|
+
results = cg.search_nodes(query, SearchOptions(limit=max_files))
|
|
491
|
+
|
|
492
|
+
if not results:
|
|
493
|
+
click.echo(f'No results found for "{query}"')
|
|
494
|
+
cg.destroy()
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
# Group by file
|
|
498
|
+
from collections import defaultdict
|
|
499
|
+
files: Dict[str, List] = defaultdict(list)
|
|
500
|
+
for r in results:
|
|
501
|
+
files[r.node.file_path].append(r.node)
|
|
502
|
+
|
|
503
|
+
for filepath, nodes in files.items():
|
|
504
|
+
click.echo(bold(f'\n### {filepath}'))
|
|
505
|
+
for node in nodes:
|
|
506
|
+
sig = f' {node.signature}' if node.signature else ''
|
|
507
|
+
click.echo(
|
|
508
|
+
f' {blue(node.kind.ljust(12))} '
|
|
509
|
+
f'{bold(node.name)}{sig} '
|
|
510
|
+
f'({dim(f"line {node.start_line}")})'
|
|
511
|
+
)
|
|
512
|
+
# Show docstring if present
|
|
513
|
+
if node.docstring:
|
|
514
|
+
doc = node.docstring.strip()[:150]
|
|
515
|
+
click.echo(f' {dim(" " + doc)}')
|
|
516
|
+
|
|
517
|
+
cg.destroy()
|
|
518
|
+
|
|
519
|
+
except Exception as e:
|
|
520
|
+
click.echo(red(f'Explore failed: {str(e)}'))
|
|
521
|
+
sys.exit(1)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@main.command()
|
|
525
|
+
@click.argument('symbol')
|
|
526
|
+
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
527
|
+
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth')
|
|
528
|
+
def callers(symbol: str, path_arg: Optional[str], depth: int):
|
|
529
|
+
"""Find what calls a function/method."""
|
|
530
|
+
project_path = resolve_project_path(path_arg)
|
|
531
|
+
|
|
532
|
+
if not is_initialized(project_path):
|
|
533
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
try:
|
|
537
|
+
cg = CodeGraph.open_sync(project_path)
|
|
538
|
+
nodes = cg.get_nodes_by_name(symbol)
|
|
539
|
+
|
|
540
|
+
if not nodes:
|
|
541
|
+
click.echo(yellow(f'Symbol not found: {symbol}'))
|
|
542
|
+
cg.destroy()
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
callers_list = cg.get_callers(nodes[0].id, depth)
|
|
546
|
+
|
|
547
|
+
if not callers_list:
|
|
548
|
+
click.echo(f'No callers found for {symbol}')
|
|
549
|
+
else:
|
|
550
|
+
click.echo(bold(f'\nCallers of "{symbol}":\n'))
|
|
551
|
+
seen = set()
|
|
552
|
+
for caller_node, edge in callers_list:
|
|
553
|
+
key = f'{caller_node.file_path}:{caller_node.start_line}'
|
|
554
|
+
if key in seen:
|
|
555
|
+
continue
|
|
556
|
+
seen.add(key)
|
|
557
|
+
location = f'{caller_node.file_path}:{caller_node.start_line}'
|
|
558
|
+
click.echo(f' {blue(caller_node.kind.ljust(12))} {bold(caller_node.name)}')
|
|
559
|
+
click.echo(f' {dim(" " + location)}')
|
|
560
|
+
click.echo()
|
|
561
|
+
|
|
562
|
+
cg.destroy()
|
|
563
|
+
|
|
564
|
+
except Exception as e:
|
|
565
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
566
|
+
sys.exit(1)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@main.command()
|
|
570
|
+
@click.argument('symbol')
|
|
571
|
+
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
572
|
+
@click.option('-d', '--depth', default=1, type=int, help='Traversal depth')
|
|
573
|
+
def callees(symbol: str, path_arg: Optional[str], depth: int):
|
|
574
|
+
"""Find what a function/method calls."""
|
|
575
|
+
project_path = resolve_project_path(path_arg)
|
|
576
|
+
|
|
577
|
+
if not is_initialized(project_path):
|
|
578
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
579
|
+
return
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
cg = CodeGraph.open_sync(project_path)
|
|
583
|
+
nodes = cg.get_nodes_by_name(symbol)
|
|
584
|
+
|
|
585
|
+
if not nodes:
|
|
586
|
+
click.echo(yellow(f'Symbol not found: {symbol}'))
|
|
587
|
+
cg.destroy()
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
callees_list = cg.get_callees(nodes[0].id, depth)
|
|
591
|
+
|
|
592
|
+
if not callees_list:
|
|
593
|
+
click.echo(f'No callees found for {symbol}')
|
|
594
|
+
else:
|
|
595
|
+
click.echo(bold(f'\nCallees of "{symbol}":\n'))
|
|
596
|
+
seen = set()
|
|
597
|
+
for callee_node, edge in callees_list:
|
|
598
|
+
key = f'{callee_node.file_path}:{callee_node.start_line}'
|
|
599
|
+
if key in seen:
|
|
600
|
+
continue
|
|
601
|
+
seen.add(key)
|
|
602
|
+
location = f'{callee_node.file_path}:{callee_node.start_line}'
|
|
603
|
+
click.echo(f' {blue(callee_node.kind.ljust(12))} {bold(callee_node.name)}')
|
|
604
|
+
click.echo(f' {dim(" " + location)}')
|
|
605
|
+
click.echo()
|
|
606
|
+
|
|
607
|
+
cg.destroy()
|
|
608
|
+
|
|
609
|
+
except Exception as e:
|
|
610
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
611
|
+
sys.exit(1)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@main.command()
|
|
615
|
+
@click.argument('symbol')
|
|
616
|
+
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
617
|
+
@click.option('-d', '--depth', default=3, type=int, help='Traversal depth')
|
|
618
|
+
@click.option('-j', '--json', 'json_output', is_flag=True, help='Output as JSON')
|
|
619
|
+
def impact(symbol: str, path_arg: Optional[str], depth: int, json_output: bool):
|
|
620
|
+
"""Analyze what code is affected by changing a symbol."""
|
|
621
|
+
project_path = resolve_project_path(path_arg)
|
|
622
|
+
|
|
623
|
+
if not is_initialized(project_path):
|
|
624
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
625
|
+
return
|
|
626
|
+
|
|
627
|
+
try:
|
|
628
|
+
cg = CodeGraph.open_sync(project_path)
|
|
629
|
+
nodes = cg.get_nodes_by_name(symbol)
|
|
630
|
+
|
|
631
|
+
if not nodes:
|
|
632
|
+
click.echo(yellow(f'Symbol not found: {symbol}'))
|
|
633
|
+
cg.destroy()
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
subgraph = cg.get_impact_radius(nodes[0].id, depth)
|
|
637
|
+
|
|
638
|
+
if json_output:
|
|
639
|
+
output = {
|
|
640
|
+
'symbol': symbol,
|
|
641
|
+
'nodeCount': len(subgraph.nodes),
|
|
642
|
+
'edgeCount': len(subgraph.edges),
|
|
643
|
+
'files': list(set(n.file_path for n in subgraph.nodes.values())),
|
|
644
|
+
'nodes': [
|
|
645
|
+
{
|
|
646
|
+
'id': n.id,
|
|
647
|
+
'kind': n.kind,
|
|
648
|
+
'name': n.name,
|
|
649
|
+
'filePath': n.file_path,
|
|
650
|
+
'startLine': n.start_line,
|
|
651
|
+
}
|
|
652
|
+
for n in subgraph.nodes.values()
|
|
653
|
+
],
|
|
654
|
+
}
|
|
655
|
+
click.echo(json.dumps(output, indent=2))
|
|
656
|
+
cg.destroy()
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
files = set(n.file_path for n in subgraph.nodes.values())
|
|
660
|
+
click.echo(bold(f'\nImpact analysis for "{symbol}":\n'))
|
|
661
|
+
click.echo(f' {len(subgraph.nodes)} symbols affected')
|
|
662
|
+
click.echo(f' {len(subgraph.edges)} relationships')
|
|
663
|
+
click.echo(f' {len(files)} files involved')
|
|
664
|
+
click.echo()
|
|
665
|
+
|
|
666
|
+
if subgraph.nodes:
|
|
667
|
+
click.echo(bold('Affected symbols:'))
|
|
668
|
+
for n in subgraph.nodes.values():
|
|
669
|
+
if n.id != nodes[0].id:
|
|
670
|
+
location = f'{n.file_path}:{n.start_line}'
|
|
671
|
+
click.echo(f' {blue(n.kind.ljust(12))} {bold(n.name)}')
|
|
672
|
+
click.echo(f' {dim(" " + location)}')
|
|
673
|
+
|
|
674
|
+
cg.destroy()
|
|
675
|
+
|
|
676
|
+
except Exception as e:
|
|
677
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
678
|
+
sys.exit(1)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@main.command()
|
|
682
|
+
@click.argument('files', nargs=-1)
|
|
683
|
+
@click.option('-p', '--path', 'path_arg', help='Project path')
|
|
684
|
+
def affected(files: List[str], path_arg: Optional[str]):
|
|
685
|
+
"""Find test files affected by changes."""
|
|
686
|
+
project_path = resolve_project_path(path_arg)
|
|
687
|
+
|
|
688
|
+
if not is_initialized(project_path):
|
|
689
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
try:
|
|
693
|
+
cg = CodeGraph.open_sync(project_path)
|
|
694
|
+
|
|
695
|
+
affected_files: set = set()
|
|
696
|
+
for filepath in files:
|
|
697
|
+
# Get nodes in the changed file
|
|
698
|
+
nodes = cg.get_nodes_by_file(filepath)
|
|
699
|
+
for node in nodes:
|
|
700
|
+
# Find what depends on these nodes
|
|
701
|
+
dependents = cg.get_impact_radius(node.id, depth=2)
|
|
702
|
+
for n in dependents.nodes.values():
|
|
703
|
+
if n.file_path.endswith('_test.py') or n.file_path.endswith('test_'):
|
|
704
|
+
affected_files.add(n.file_path)
|
|
705
|
+
|
|
706
|
+
if not affected_files:
|
|
707
|
+
click.echo('No affected test files found')
|
|
708
|
+
else:
|
|
709
|
+
click.echo(bold('\nAffected test files:\n'))
|
|
710
|
+
for f in sorted(affected_files):
|
|
711
|
+
click.echo(f' {f}')
|
|
712
|
+
|
|
713
|
+
cg.destroy()
|
|
714
|
+
|
|
715
|
+
except Exception as e:
|
|
716
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
717
|
+
sys.exit(1)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@main.command()
|
|
721
|
+
@click.argument('name', required=True)
|
|
722
|
+
@click.option('--path', '-p', 'path_arg', default=None, help='Project path')
|
|
723
|
+
def node(name: str, path_arg: Optional[str]):
|
|
724
|
+
"""View details about a specific symbol node (source code, metadata)."""
|
|
725
|
+
project_path = resolve_project_path(path_arg)
|
|
726
|
+
|
|
727
|
+
if not is_initialized(project_path):
|
|
728
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
729
|
+
click.echo('Run "codegraph init" first')
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
cg = CodeGraph(project_path)
|
|
734
|
+
|
|
735
|
+
# Search for nodes by name
|
|
736
|
+
results = cg.search(name)
|
|
737
|
+
if not results:
|
|
738
|
+
click.echo(yellow(f'No nodes found matching "{name}"'))
|
|
739
|
+
return
|
|
740
|
+
|
|
741
|
+
# If exact match, show details
|
|
742
|
+
exact_matches = [r for r in results if r.node.name == name]
|
|
743
|
+
if exact_matches:
|
|
744
|
+
target = exact_matches[0].node
|
|
745
|
+
else:
|
|
746
|
+
target = results[0].node
|
|
747
|
+
|
|
748
|
+
click.echo(bold(f'\nš¦ {target.kind}: {target.name}'))
|
|
749
|
+
click.echo(f' {dim("ID:")} {target.id}')
|
|
750
|
+
click.echo(f' {dim("File:")} {target.file_path}')
|
|
751
|
+
click.echo(f' {dim("Lines:")} {target.start_line}-{target.end_line}')
|
|
752
|
+
if target.signature:
|
|
753
|
+
click.echo(f' {dim("Signature:")} {target.signature}')
|
|
754
|
+
if target.docstring:
|
|
755
|
+
click.echo(f' {dim("Docstring:")} {target.docstring}')
|
|
756
|
+
if target.qualified_name:
|
|
757
|
+
click.echo(f' {dim("QName:")} {target.qualified_name}')
|
|
758
|
+
click.echo(f' {dim("Exported:")} {green("yes") if target.is_exported else "no"}')
|
|
759
|
+
|
|
760
|
+
# Show callers
|
|
761
|
+
callers = cg.get_callers(target.id)
|
|
762
|
+
if callers:
|
|
763
|
+
click.echo(bold(f'\n Callers ({len(callers)}):'))
|
|
764
|
+
for c in callers:
|
|
765
|
+
click.echo(f' ā {c.name} ({c.file_path}:{c.start_line})')
|
|
766
|
+
|
|
767
|
+
# Show callees
|
|
768
|
+
callees = cg.get_callees(target.id)
|
|
769
|
+
if callees:
|
|
770
|
+
click.echo(bold(f'\n Callees ({len(callees)}):'))
|
|
771
|
+
for c in callees:
|
|
772
|
+
click.echo(f' ā {c.name} ({c.file_path}:{c.start_line})')
|
|
773
|
+
|
|
774
|
+
cg.destroy()
|
|
775
|
+
|
|
776
|
+
except Exception as e:
|
|
777
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
778
|
+
sys.exit(1)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
@main.command()
|
|
782
|
+
@click.option('--path', '-p', 'path_arg', default=None, help='Project path')
|
|
783
|
+
@click.option('--json-output', 'json_output', is_flag=True, help='Output as JSON')
|
|
784
|
+
def files(path_arg: Optional[str], json_output: bool):
|
|
785
|
+
"""List indexed files with language and symbol statistics."""
|
|
786
|
+
project_path = resolve_project_path(path_arg)
|
|
787
|
+
|
|
788
|
+
if not is_initialized(project_path):
|
|
789
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
790
|
+
click.echo('Run "codegraph init" first')
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
try:
|
|
794
|
+
cg = CodeGraph(project_path)
|
|
795
|
+
stats = cg.get_stats()
|
|
796
|
+
all_files = cg.db.get_all_file_paths()
|
|
797
|
+
|
|
798
|
+
if json_output:
|
|
799
|
+
result = {
|
|
800
|
+
'total_files': len(all_files),
|
|
801
|
+
'total_nodes': stats.node_count,
|
|
802
|
+
'total_edges': stats.edge_count,
|
|
803
|
+
'files': [],
|
|
804
|
+
}
|
|
805
|
+
for f in all_files:
|
|
806
|
+
nodes = cg.db.get_nodes_by_file(f)
|
|
807
|
+
langs = set(n.language for n in nodes if n.language)
|
|
808
|
+
result['files'].append({
|
|
809
|
+
'path': f,
|
|
810
|
+
'node_count': len(nodes),
|
|
811
|
+
'languages': list(langs),
|
|
812
|
+
})
|
|
813
|
+
click.echo(json.dumps(result, indent=2))
|
|
814
|
+
else:
|
|
815
|
+
click.echo(bold(f'\nš Indexed Files ({len(all_files)} total)\n'))
|
|
816
|
+
for f in sorted(all_files):
|
|
817
|
+
nodes = cg.db.get_nodes_by_file(f)
|
|
818
|
+
lang = ''
|
|
819
|
+
for n in nodes:
|
|
820
|
+
if n.language and n.language != 'unknown':
|
|
821
|
+
lang = n.language
|
|
822
|
+
break
|
|
823
|
+
kinds = {}
|
|
824
|
+
for n in nodes:
|
|
825
|
+
k = n.kind
|
|
826
|
+
kinds[k] = kinds.get(k, 0) + 1
|
|
827
|
+
kind_str = ', '.join(f'{v} {k}' for k, v in sorted(kinds.items()) if k != 'file')
|
|
828
|
+
if kind_str:
|
|
829
|
+
click.echo(f' {dim(lang+" ") if lang else ""}{f} {dim("("+kind_str+")")}')
|
|
830
|
+
else:
|
|
831
|
+
click.echo(f' {f}')
|
|
832
|
+
|
|
833
|
+
cg.destroy()
|
|
834
|
+
|
|
835
|
+
except Exception as e:
|
|
836
|
+
click.echo(red(f'Failed: {str(e)}'))
|
|
837
|
+
sys.exit(1)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@main.command()
|
|
841
|
+
@click.option('--path', '-p', 'path_arg', default=None, help='Project path')
|
|
842
|
+
def install(path_arg: Optional[str]):
|
|
843
|
+
"""Install shell command integration (alias, completion)."""
|
|
844
|
+
project_path = resolve_project_path(path_arg)
|
|
845
|
+
click.echo(bold('\nš§ CodeGraph Shell Integration'))
|
|
846
|
+
click.echo()
|
|
847
|
+
click.echo('To add CodeGraph to your shell, add one of the following to your')
|
|
848
|
+
click.echo('shell configuration file (~/.bashrc, ~/.zshrc, etc.):')
|
|
849
|
+
click.echo()
|
|
850
|
+
click.echo(dim(' # Bash/Zsh'))
|
|
851
|
+
click.echo(f' alias cg="codegraph"')
|
|
852
|
+
click.echo()
|
|
853
|
+
click.echo(dim(' # Or for click shell completion:'))
|
|
854
|
+
click.echo(' eval "$(_CODEGRAPH_COMPLETE=bash_source codegraph)" # Bash')
|
|
855
|
+
click.echo(' eval "$(_CODEGRAPH_COMPLETE=zsh_source codegraph)" # Zsh')
|
|
856
|
+
click.echo(' codegraph completion install # Fish')
|
|
857
|
+
click.echo()
|
|
858
|
+
click.echo(green(' CodeGraph is ready at: ' + project_path))
|
|
859
|
+
click.echo()
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
@main.command()
|
|
863
|
+
@click.argument('path', required=False, default=None)
|
|
864
|
+
@click.option('-w', '--watch', is_flag=True, help='Watch for file changes and auto-sync')
|
|
865
|
+
@click.option('--verbose', '-v', is_flag=True, help='Log sync info to stderr')
|
|
866
|
+
def serve(path: Optional[str], watch: bool, verbose: bool):
|
|
867
|
+
"""Start MCP server for AI agent integration.
|
|
868
|
+
|
|
869
|
+
Listens on stdin/stdout using the Model Context Protocol.
|
|
870
|
+
AI assistants connect to this server to query the code graph.
|
|
871
|
+
"""
|
|
872
|
+
project_path = resolve_project_path(path)
|
|
873
|
+
|
|
874
|
+
if not is_initialized(project_path):
|
|
875
|
+
click.echo(red(f'CodeGraph not initialized in {project_path}'))
|
|
876
|
+
click.echo('Run "codegraph init" first')
|
|
877
|
+
return
|
|
878
|
+
|
|
879
|
+
click.echo(f'Starting CodeGraph MCP server for {project_path}...')
|
|
880
|
+
if watch:
|
|
881
|
+
click.echo(green(' File watching enabled ā auto-syncing on changes'))
|
|
882
|
+
|
|
883
|
+
from codegraph.mcp import MCPServer
|
|
884
|
+
server = MCPServer(project_path, watch=watch)
|
|
885
|
+
|
|
886
|
+
import asyncio
|
|
887
|
+
async def run():
|
|
888
|
+
await server.start()
|
|
889
|
+
|
|
890
|
+
try:
|
|
891
|
+
asyncio.run(run())
|
|
892
|
+
except KeyboardInterrupt:
|
|
893
|
+
click.echo('\nServer stopped')
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
@main.command()
|
|
897
|
+
@click.argument('path', required=False, default=None)
|
|
898
|
+
def unlock(path: Optional[str]):
|
|
899
|
+
"""Remove stale lock file."""
|
|
900
|
+
project_path = resolve_project_path(path)
|
|
901
|
+
lock_path = os.path.join(project_path, '.codegraph', 'codegraph.lock')
|
|
902
|
+
|
|
903
|
+
if os.path.isfile(lock_path):
|
|
904
|
+
os.remove(lock_path)
|
|
905
|
+
click.echo(green('Lock file removed'))
|
|
906
|
+
else:
|
|
907
|
+
click.echo('No lock file found')
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
if __name__ == '__main__':
|
|
911
|
+
main()
|