descry-codegraph 0.1.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.
descry/cli.py ADDED
@@ -0,0 +1,457 @@
1
+ """Descry CLI — standalone command-line interface for codebase knowledge graph tools.
2
+
3
+ Delegates all business logic to DescryService.
4
+
5
+ Usage:
6
+ descry health
7
+ descry search foo bar --limit 5
8
+ descry context FILE:src/lib.rs::main --full
9
+ """
10
+
11
+ import argparse
12
+ import asyncio
13
+ import sys
14
+
15
+ from descry.handlers import DescryService, DescryConfig
16
+
17
+
18
+ def _make_service() -> DescryService:
19
+ config = DescryConfig.from_env()
20
+ return DescryService(config)
21
+
22
+
23
+ def _run(coro) -> str:
24
+ return asyncio.run(coro)
25
+
26
+
27
+ def _print_result(coro):
28
+ """Run a service coroutine, print the result, and translate service-side
29
+ errors to a nonzero exit code so shell chains like `descry callers X &&
30
+ next_step` don't silently advance on "ERROR: Graph not found".
31
+ """
32
+ result = _run(coro)
33
+ print(result)
34
+ if isinstance(result, str) and result.lstrip().upper().startswith("ERROR:"):
35
+ sys.exit(2)
36
+
37
+
38
+ def cmd_health(args):
39
+ svc = _make_service()
40
+ _print_result(svc.health())
41
+
42
+
43
+ def cmd_status(args):
44
+ svc = _make_service()
45
+ _print_result(svc.status())
46
+
47
+
48
+ def cmd_ensure(args):
49
+ svc = _make_service()
50
+ _print_result(svc.ensure(max_age_hours=args.max_age_hours))
51
+
52
+
53
+ def cmd_index(args):
54
+ svc = _make_service()
55
+ path = args.path if args.path else "."
56
+ _print_result(svc.index(path))
57
+
58
+
59
+ def cmd_callers(args):
60
+ svc = _make_service()
61
+ _print_result(svc.callers(name=args.name, limit=args.limit))
62
+
63
+
64
+ def cmd_callees(args):
65
+ svc = _make_service()
66
+ _print_result(svc.callees(name=args.name, limit=args.limit))
67
+
68
+
69
+ def cmd_context(args):
70
+ svc = _make_service()
71
+ _print_result(
72
+ svc.context(
73
+ node_id=args.node_id,
74
+ brief=args.brief,
75
+ full=args.full,
76
+ expand_callees=args.expand_callees,
77
+ deduplicate=args.deduplicate,
78
+ depth=args.depth,
79
+ max_tokens=args.max_tokens,
80
+ callee_budget=args.callee_budget,
81
+ head_lines=args.head_lines,
82
+ max_output_tokens=args.max_output_tokens,
83
+ )
84
+ )
85
+
86
+
87
+ def cmd_flow(args):
88
+ svc = _make_service()
89
+ _print_result(
90
+ svc.flow(
91
+ start=args.start,
92
+ direction=args.direction,
93
+ depth=args.depth,
94
+ target=args.target,
95
+ inline_threshold=args.inline_threshold,
96
+ )
97
+ )
98
+
99
+
100
+ def cmd_search(args):
101
+ svc = _make_service()
102
+ _print_result(
103
+ svc.search(
104
+ terms=args.terms,
105
+ compact=args.compact,
106
+ limit=args.limit,
107
+ lang=args.lang,
108
+ crate=args.crate,
109
+ symbol_type=args.type,
110
+ exclude_tests=args.exclude_tests,
111
+ )
112
+ )
113
+
114
+
115
+ def cmd_structure(args):
116
+ svc = _make_service()
117
+ _print_result(svc.structure(filename=args.filename))
118
+
119
+
120
+ def cmd_flatten(args):
121
+ svc = _make_service()
122
+ _print_result(svc.flatten(class_node_id=args.class_node_id))
123
+
124
+
125
+ def cmd_semantic(args):
126
+ svc = _make_service()
127
+ _print_result(svc.semantic(query=args.query, limit=args.limit))
128
+
129
+
130
+ def cmd_quick(args):
131
+ svc = _make_service()
132
+ _print_result(svc.quick(name=args.name, full=args.full, brief=args.brief))
133
+
134
+
135
+ def cmd_impls(args):
136
+ svc = _make_service()
137
+ _print_result(svc.impls(method=args.method, trait_name=args.trait_name))
138
+
139
+
140
+ def cmd_path(args):
141
+ svc = _make_service()
142
+ _print_result(
143
+ svc.path(
144
+ start=args.start,
145
+ end=args.end,
146
+ max_depth=args.max_depth,
147
+ direction=args.direction,
148
+ )
149
+ )
150
+
151
+
152
+ def cmd_cross_lang(args):
153
+ svc = _make_service()
154
+ _print_result(
155
+ svc.cross_lang(
156
+ mode=args.mode,
157
+ method=args.method,
158
+ path=args.path,
159
+ tag=args.tag,
160
+ )
161
+ )
162
+
163
+
164
+ def cmd_churn(args):
165
+ svc = _make_service()
166
+ _print_result(
167
+ svc.churn(
168
+ time_range=args.time_range,
169
+ path_filter=args.path_filter,
170
+ limit=args.limit,
171
+ mode=args.mode,
172
+ exclude_generated=args.exclude_generated,
173
+ )
174
+ )
175
+
176
+
177
+ def cmd_evolution(args):
178
+ svc = _make_service()
179
+ _print_result(
180
+ svc.evolution(
181
+ name=args.name,
182
+ time_range=args.time_range,
183
+ limit=args.limit,
184
+ show_diff=args.show_diff,
185
+ crate=args.crate,
186
+ )
187
+ )
188
+
189
+
190
+ def cmd_changes(args):
191
+ svc = _make_service()
192
+ _print_result(
193
+ svc.changes(
194
+ commit_range=args.commit_range,
195
+ time_range=args.time_range,
196
+ path_filter=args.path_filter,
197
+ show_callers=args.show_callers,
198
+ limit=args.limit,
199
+ )
200
+ )
201
+
202
+
203
+ def main():
204
+ parser = argparse.ArgumentParser(
205
+ prog="descry",
206
+ description="Codebase knowledge graph tools",
207
+ )
208
+ subparsers = parser.add_subparsers(dest="command")
209
+
210
+ # health
211
+ sub = subparsers.add_parser("health", help="Quick diagnostic check")
212
+ sub.set_defaults(func=cmd_health)
213
+
214
+ # status
215
+ sub = subparsers.add_parser("status", help="Check graph existence and freshness")
216
+ sub.set_defaults(func=cmd_status)
217
+
218
+ # ensure
219
+ sub = subparsers.add_parser("ensure", help="Ensure graph exists and is fresh")
220
+ sub.add_argument(
221
+ "--max-age-hours",
222
+ type=float,
223
+ default=24,
224
+ help="Max graph age in hours (default: 24)",
225
+ )
226
+ sub.set_defaults(func=cmd_ensure)
227
+
228
+ # index
229
+ sub = subparsers.add_parser("index", help="Regenerate the codebase graph")
230
+ sub.add_argument(
231
+ "path", nargs="?", default=None, help="Path to index (default: project root)"
232
+ )
233
+ sub.set_defaults(func=cmd_index)
234
+
235
+ # callers
236
+ sub = subparsers.add_parser("callers", help="Find all callers of a symbol")
237
+ sub.add_argument("name", help="Symbol name to look up")
238
+ sub.add_argument("--limit", type=int, default=20, help="Max results (default: 20)")
239
+ sub.set_defaults(func=cmd_callers)
240
+
241
+ # callees
242
+ sub = subparsers.add_parser("callees", help="Find what a symbol calls")
243
+ sub.add_argument("name", help="Symbol name to look up")
244
+ sub.add_argument("--limit", type=int, default=20, help="Max results (default: 20)")
245
+ sub.set_defaults(func=cmd_callees)
246
+
247
+ # context
248
+ sub = subparsers.add_parser(
249
+ "context", help="Get full context for a symbol by node ID"
250
+ )
251
+ sub.add_argument("node_id", help="Graph node ID (e.g. FILE:src/lib.rs::main)")
252
+ sub.add_argument("--brief", action="store_true", help="Brief output")
253
+ sub.add_argument("--full", action="store_true", help="Full source output")
254
+ sub.add_argument(
255
+ "--expand-callees", action="store_true", help="Include callee source"
256
+ )
257
+ sub.add_argument(
258
+ "--deduplicate", action="store_true", help="Deduplicate repeated lookups"
259
+ )
260
+ sub.add_argument(
261
+ "--depth", type=int, default=1, help="Traversal depth (default: 1)"
262
+ )
263
+ sub.add_argument(
264
+ "--max-tokens", type=int, default=2000, help="Max token budget (default: 2000)"
265
+ )
266
+ sub.add_argument(
267
+ "--callee-budget",
268
+ type=int,
269
+ default=2000,
270
+ help="Token budget for expanded callees (default: 2000)",
271
+ )
272
+ sub.add_argument(
273
+ "--head-lines",
274
+ type=int,
275
+ default=None,
276
+ help="Truncate source head to N lines (default: full)",
277
+ )
278
+ sub.add_argument(
279
+ "--max-output-tokens",
280
+ type=int,
281
+ default=None,
282
+ help="Overall output token cap (default: unlimited)",
283
+ )
284
+ sub.set_defaults(func=cmd_context)
285
+
286
+ # flow
287
+ sub = subparsers.add_parser("flow", help="Trace call flow from a starting symbol")
288
+ sub.add_argument("start", help="Starting symbol name")
289
+ sub.add_argument(
290
+ "--direction",
291
+ default="forward",
292
+ choices=["forward", "backward"],
293
+ help="Trace direction (default: forward)",
294
+ )
295
+ sub.add_argument("--depth", type=int, default=3, help="Trace depth (default: 3)")
296
+ sub.add_argument("--target", default=None, help="Target symbol to reach")
297
+ sub.add_argument(
298
+ "--inline-threshold",
299
+ type=int,
300
+ default=100,
301
+ help="Inline callee source below this token count (default: 100)",
302
+ )
303
+ sub.set_defaults(func=cmd_flow)
304
+
305
+ # search
306
+ sub = subparsers.add_parser("search", help="Search symbol names and docstrings")
307
+ sub.add_argument("terms", nargs="+", help="Search terms")
308
+ sub.add_argument(
309
+ "--compact", action="store_true", default=True, help="Compact output (default)"
310
+ )
311
+ sub.add_argument(
312
+ "--no-compact", dest="compact", action="store_false", help="Detailed output"
313
+ )
314
+ sub.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
315
+ sub.add_argument("--lang", default=None, help="Filter by language")
316
+ sub.add_argument("--crate", default=None, help="Filter by crate")
317
+ sub.add_argument("--type", default=None, help="Filter by symbol type")
318
+ sub.add_argument(
319
+ "--exclude-tests", action="store_true", help="Exclude test symbols"
320
+ )
321
+ sub.set_defaults(func=cmd_search)
322
+
323
+ # structure
324
+ sub = subparsers.add_parser("structure", help="Show file structure")
325
+ sub.add_argument("filename", help="Filename to inspect")
326
+ sub.set_defaults(func=cmd_structure)
327
+
328
+ # flatten
329
+ sub = subparsers.add_parser(
330
+ "flatten", help="Show effective API of a class including inherited methods"
331
+ )
332
+ sub.add_argument("class_node_id", help="Class node ID")
333
+ sub.set_defaults(func=cmd_flatten)
334
+
335
+ # semantic
336
+ sub = subparsers.add_parser(
337
+ "semantic", help="Pure semantic search using embeddings"
338
+ )
339
+ sub.add_argument("query", help="Natural language query")
340
+ sub.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
341
+ sub.set_defaults(func=cmd_semantic)
342
+
343
+ # quick
344
+ sub = subparsers.add_parser(
345
+ "quick", help="Find symbol and show full context in one step"
346
+ )
347
+ sub.add_argument("name", help="Symbol name")
348
+ sub.add_argument("--full", action="store_true", help="Full source output")
349
+ sub.add_argument("--brief", action="store_true", help="Brief output")
350
+ sub.set_defaults(func=cmd_quick)
351
+
352
+ # impls
353
+ sub = subparsers.add_parser(
354
+ "impls", help="Find all implementations of a trait method"
355
+ )
356
+ sub.add_argument("method", help="Method name")
357
+ sub.add_argument("--trait-name", default=None, help="Filter by trait name")
358
+ sub.set_defaults(func=cmd_impls)
359
+
360
+ # path
361
+ sub = subparsers.add_parser(
362
+ "path", help="Find shortest call path between two symbols"
363
+ )
364
+ sub.add_argument("start", help="Starting symbol")
365
+ sub.add_argument("end", help="Ending symbol")
366
+ sub.add_argument(
367
+ "--max-depth", type=int, default=10, help="Max search depth (default: 10)"
368
+ )
369
+ sub.add_argument(
370
+ "--direction",
371
+ default="forward",
372
+ choices=["forward", "backward"],
373
+ help="Search direction (default: forward)",
374
+ )
375
+ sub.set_defaults(func=cmd_path)
376
+
377
+ # cross-lang
378
+ sub = subparsers.add_parser(
379
+ "cross-lang", help="Trace API calls from frontend to backend via OpenAPI"
380
+ )
381
+ sub.add_argument(
382
+ "--mode",
383
+ default="endpoint",
384
+ choices=["endpoint", "list", "stats"],
385
+ help="Trace mode (default: endpoint)",
386
+ )
387
+ sub.add_argument("--method", default=None, help="HTTP method (e.g. GET, POST)")
388
+ sub.add_argument("--path", default=None, help="API path (e.g. /api/v1/deployments)")
389
+ sub.add_argument("--tag", default=None, help="Filter by tag")
390
+ sub.set_defaults(func=cmd_cross_lang)
391
+
392
+ # churn
393
+ sub = subparsers.add_parser("churn", help="Find code churn hotspots")
394
+ sub.add_argument("--time-range", default=None, help='Time range (e.g. "30 days")')
395
+ sub.add_argument("--path-filter", default=None, help='Path filter (e.g. "src/")')
396
+ sub.add_argument("--limit", type=int, default=20, help="Max results (default: 20)")
397
+ sub.add_argument(
398
+ "--mode",
399
+ default="symbols",
400
+ choices=["symbols", "files", "co-change"],
401
+ help="Churn mode (default: symbols)",
402
+ )
403
+ sub.add_argument(
404
+ "--include-generated",
405
+ dest="exclude_generated",
406
+ action="store_false",
407
+ default=True,
408
+ help="Include generated/lockfiles (default: excluded)",
409
+ )
410
+ sub.set_defaults(func=cmd_churn)
411
+
412
+ # evolution
413
+ sub = subparsers.add_parser(
414
+ "evolution", help="Track how a symbol has changed over time"
415
+ )
416
+ sub.add_argument("name", help="Symbol name")
417
+ sub.add_argument("--time-range", default=None, help='Time range (e.g. "90 days")')
418
+ sub.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
419
+ sub.add_argument("--show-diff", action="store_true", help="Show diffs")
420
+ sub.add_argument("--crate", default=None, help="Filter by crate")
421
+ sub.set_defaults(func=cmd_evolution)
422
+
423
+ # changes
424
+ sub = subparsers.add_parser(
425
+ "changes", help="Analyze change impact for a commit range"
426
+ )
427
+ sub.add_argument(
428
+ "--commit-range", default=None, help="Commit range (e.g. HEAD~3..HEAD)"
429
+ )
430
+ sub.add_argument("--time-range", default=None, help='Time range (e.g. "7 days")')
431
+ sub.add_argument("--path-filter", default=None, help='Path filter (e.g. "src/")')
432
+ sub.add_argument(
433
+ "--show-callers",
434
+ action="store_true",
435
+ default=True,
436
+ help="Show callers of changed symbols (default)",
437
+ )
438
+ sub.add_argument(
439
+ "--no-show-callers",
440
+ dest="show_callers",
441
+ action="store_false",
442
+ help="Hide callers",
443
+ )
444
+ sub.add_argument("--limit", type=int, default=50, help="Max results (default: 50)")
445
+ sub.set_defaults(func=cmd_changes)
446
+
447
+ args = parser.parse_args()
448
+
449
+ if not args.command:
450
+ parser.print_help()
451
+ sys.exit(1)
452
+
453
+ args.func(args)
454
+
455
+
456
+ if __name__ == "__main__":
457
+ main()