cicada-mcp 0.1.7__py3-none-any.whl → 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 (53) hide show
  1. cicada/ascii_art.py +60 -0
  2. cicada/clean.py +195 -60
  3. cicada/cli.py +757 -0
  4. cicada/colors.py +27 -0
  5. cicada/command_logger.py +14 -16
  6. cicada/dead_code_analyzer.py +12 -19
  7. cicada/extractors/__init__.py +6 -6
  8. cicada/extractors/base.py +3 -3
  9. cicada/extractors/call.py +11 -15
  10. cicada/extractors/dependency.py +39 -51
  11. cicada/extractors/doc.py +8 -9
  12. cicada/extractors/function.py +12 -24
  13. cicada/extractors/module.py +11 -15
  14. cicada/extractors/spec.py +8 -12
  15. cicada/find_dead_code.py +15 -39
  16. cicada/formatter.py +37 -91
  17. cicada/git_helper.py +22 -34
  18. cicada/indexer.py +122 -107
  19. cicada/interactive_setup.py +490 -0
  20. cicada/keybert_extractor.py +286 -0
  21. cicada/keyword_search.py +22 -30
  22. cicada/keyword_test.py +127 -0
  23. cicada/lightweight_keyword_extractor.py +5 -13
  24. cicada/mcp_entry.py +683 -0
  25. cicada/mcp_server.py +103 -209
  26. cicada/parser.py +9 -9
  27. cicada/pr_finder.py +15 -19
  28. cicada/pr_indexer/__init__.py +3 -3
  29. cicada/pr_indexer/cli.py +4 -9
  30. cicada/pr_indexer/github_api_client.py +22 -37
  31. cicada/pr_indexer/indexer.py +17 -29
  32. cicada/pr_indexer/line_mapper.py +8 -12
  33. cicada/pr_indexer/pr_index_builder.py +22 -34
  34. cicada/setup.py +189 -87
  35. cicada/utils/__init__.py +9 -9
  36. cicada/utils/call_site_formatter.py +4 -6
  37. cicada/utils/function_grouper.py +4 -4
  38. cicada/utils/hash_utils.py +12 -15
  39. cicada/utils/index_utils.py +15 -15
  40. cicada/utils/path_utils.py +24 -29
  41. cicada/utils/signature_builder.py +3 -3
  42. cicada/utils/subprocess_runner.py +17 -19
  43. cicada/utils/text_utils.py +1 -2
  44. cicada/version_check.py +2 -5
  45. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/METADATA +144 -55
  46. cicada_mcp-0.2.0.dist-info/RECORD +53 -0
  47. cicada_mcp-0.2.0.dist-info/entry_points.txt +4 -0
  48. cicada/install.py +0 -741
  49. cicada_mcp-0.1.7.dist-info/RECORD +0 -47
  50. cicada_mcp-0.1.7.dist-info/entry_points.txt +0 -9
  51. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/WHEEL +0 -0
  52. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  53. {cicada_mcp-0.1.7.dist-info → cicada_mcp-0.2.0.dist-info}/top_level.txt +0 -0
@@ -31,9 +31,7 @@ def _find_modules_recursive(node, source_code: bytes, modules: list):
31
31
 
32
32
  # Check if this is a defmodule call
33
33
  if target and arguments:
34
- target_text = source_code[target.start_byte : target.end_byte].decode(
35
- "utf-8"
36
- )
34
+ target_text = source_code[target.start_byte : target.end_byte].decode("utf-8")
37
35
 
38
36
  if target_text == "defmodule":
39
37
  # Extract module name from arguments
@@ -41,9 +39,9 @@ def _find_modules_recursive(node, source_code: bytes, modules: list):
41
39
 
42
40
  for arg_child in arguments.children:
43
41
  if arg_child.type == "alias":
44
- module_name = source_code[
45
- arg_child.start_byte : arg_child.end_byte
46
- ].decode("utf-8")
42
+ module_name = source_code[arg_child.start_byte : arg_child.end_byte].decode(
43
+ "utf-8"
44
+ )
47
45
  break
48
46
 
49
47
  if module_name and do_block:
@@ -84,17 +82,15 @@ def _find_moduledoc_recursive(node, source_code: bytes) -> str | None:
84
82
  # Check if this is a moduledoc attribute
85
83
  for call_child in operand.children:
86
84
  if call_child.type == "identifier":
87
- attr_name = source_code[
88
- call_child.start_byte : call_child.end_byte
89
- ].decode("utf-8")
85
+ attr_name = source_code[call_child.start_byte : call_child.end_byte].decode(
86
+ "utf-8"
87
+ )
90
88
 
91
89
  if attr_name == "moduledoc":
92
90
  # Extract the documentation string from the arguments
93
91
  for arg_child in operand.children:
94
92
  if arg_child.type == "arguments":
95
- doc_string = extract_string_from_arguments(
96
- arg_child, source_code
97
- )
93
+ doc_string = extract_string_from_arguments(arg_child, source_code)
98
94
  if doc_string:
99
95
  return doc_string
100
96
 
@@ -106,9 +102,9 @@ def _find_moduledoc_recursive(node, source_code: bytes) -> str | None:
106
102
  is_defmodule = False
107
103
  for call_child in child.children:
108
104
  if call_child.type == "identifier":
109
- target_text = source_code[
110
- call_child.start_byte : call_child.end_byte
111
- ].decode("utf-8")
105
+ target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
106
+ "utf-8"
107
+ )
112
108
  if target_text == "defmodule":
113
109
  is_defmodule = True
114
110
  break
cicada/extractors/spec.py CHANGED
@@ -27,9 +27,9 @@ def _find_specs_recursive(node, source_code: bytes, specs: dict):
27
27
  # Check if this is a spec attribute
28
28
  for call_child in operand.children:
29
29
  if call_child.type == "identifier":
30
- attr_name = source_code[
31
- call_child.start_byte : call_child.end_byte
32
- ].decode("utf-8")
30
+ attr_name = source_code[call_child.start_byte : call_child.end_byte].decode(
31
+ "utf-8"
32
+ )
33
33
 
34
34
  if attr_name == "spec":
35
35
  # Extract the spec definition
@@ -45,9 +45,9 @@ def _find_specs_recursive(node, source_code: bytes, specs: dict):
45
45
  is_defmodule_or_def = False
46
46
  for call_child in child.children:
47
47
  if call_child.type == "identifier":
48
- target_text = source_code[
49
- call_child.start_byte : call_child.end_byte
50
- ].decode("utf-8")
48
+ target_text = source_code[call_child.start_byte : call_child.end_byte].decode(
49
+ "utf-8"
50
+ )
51
51
  if target_text in ["defmodule", "def", "defp"]:
52
52
  is_defmodule_or_def = True
53
53
  break
@@ -95,9 +95,7 @@ def _parse_spec(spec_node, source_code: bytes) -> dict | None:
95
95
  fc_child.start_byte : fc_child.end_byte
96
96
  ].decode("utf-8")
97
97
  elif fc_child.type == "arguments":
98
- param_types = _extract_param_types(
99
- fc_child, source_code
100
- )
98
+ param_types = _extract_param_types(fc_child, source_code)
101
99
 
102
100
  if func_name:
103
101
  return {
@@ -137,9 +135,7 @@ def match_specs_to_functions(functions: list, specs: dict) -> list:
137
135
  args_with_types = []
138
136
  for i, arg_name in enumerate(func["args"]):
139
137
  if i < len(spec["param_types"]):
140
- args_with_types.append(
141
- {"name": arg_name, "type": spec["param_types"][i]}
142
- )
138
+ args_with_types.append({"name": arg_name, "type": spec["param_types"][i]})
143
139
  else:
144
140
  args_with_types.append({"name": arg_name, "type": None})
145
141
  func["args_with_types"] = args_with_types
cicada/find_dead_code.py CHANGED
@@ -10,10 +10,9 @@ Author: Cursor(Auto)
10
10
  import argparse
11
11
  import json
12
12
  import sys
13
- from pathlib import Path
14
13
 
15
14
  from cicada.dead_code_analyzer import DeadCodeAnalyzer
16
- from cicada.utils import load_index
15
+ from cicada.utils import get_index_path, load_index
17
16
 
18
17
 
19
18
  def format_markdown(results: dict) -> str:
@@ -34,9 +33,7 @@ def format_markdown(results: dict) -> str:
34
33
  f"(skipped {summary['skipped_impl']} with @impl, "
35
34
  f"{summary['skipped_files']} in test/script files)"
36
35
  )
37
- lines.append(
38
- f"Found **{summary['total_candidates']} potentially unused functions**\n"
39
- )
36
+ lines.append(f"Found **{summary['total_candidates']} potentially unused functions**\n")
40
37
 
41
38
  candidates = results["candidates"]
42
39
 
@@ -46,9 +43,7 @@ def format_markdown(results: dict) -> str:
46
43
  label = f" HIGH CONFIDENCE ({count} function{'s' if count != 1 else ''}) "
47
44
  bar_length = 80
48
45
  padding = (bar_length - len(label)) // 2
49
- lines.append(
50
- f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}"
51
- )
46
+ lines.append(f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}")
52
47
  lines.append("Functions with zero usage in codebase\n")
53
48
 
54
49
  # Group by module
@@ -62,9 +57,7 @@ def format_markdown(results: dict) -> str:
62
57
  lines.append(f"### {module}")
63
58
  lines.append(f"{funcs[0]['file']}\n")
64
59
  for func in funcs:
65
- lines.append(
66
- f"- `{func['function']}/{func['arity']}` (line {func['line']})"
67
- )
60
+ lines.append(f"- `{func['function']}/{func['arity']}` (line {func['line']})")
68
61
  lines.append("")
69
62
 
70
63
  # Medium confidence
@@ -73,9 +66,7 @@ def format_markdown(results: dict) -> str:
73
66
  label = f" MEDIUM CONFIDENCE ({count} function{'s' if count != 1 else ''}) "
74
67
  bar_length = 80
75
68
  padding = (bar_length - len(label)) // 2
76
- lines.append(
77
- f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}"
78
- )
69
+ lines.append(f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}")
79
70
  lines.append(
80
71
  "Functions with zero usage, but module has behaviors/uses (possible callbacks)\n"
81
72
  )
@@ -101,9 +92,7 @@ def format_markdown(results: dict) -> str:
101
92
  lines.append("")
102
93
 
103
94
  for func in funcs:
104
- lines.append(
105
- f"- `{func['function']}/{func['arity']}` (line {func['line']})"
106
- )
95
+ lines.append(f"- `{func['function']}/{func['arity']}` (line {func['line']})")
107
96
  lines.append("")
108
97
 
109
98
  # Low confidence
@@ -112,9 +101,7 @@ def format_markdown(results: dict) -> str:
112
101
  label = f" LOW CONFIDENCE ({count} function{'s' if count != 1 else ''}) "
113
102
  bar_length = 80
114
103
  padding = (bar_length - len(label)) // 2
115
- lines.append(
116
- f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}"
117
- )
104
+ lines.append(f"\n{'═' * padding}{label}{'═' * (bar_length - padding - len(label))}")
118
105
  lines.append(
119
106
  "Functions with zero usage, but module passed as value (possible dynamic calls)\n"
120
107
  )
@@ -139,9 +126,7 @@ def format_markdown(results: dict) -> str:
139
126
  lines.append("")
140
127
 
141
128
  for func in funcs:
142
- lines.append(
143
- f"- `{func['function']}/{func['arity']}` (line {func['line']})"
144
- )
129
+ lines.append(f"- `{func['function']}/{func['arity']}` (line {func['line']})")
145
130
  lines.append("")
146
131
 
147
132
  if summary["total_candidates"] == 0:
@@ -205,18 +190,12 @@ Confidence Levels:
205
190
  low - Zero usage, but module passed as value (possible dynamic calls)
206
191
 
207
192
  Examples:
208
- cicada-find-dead-code # Show high confidence candidates
209
- cicada-find-dead-code --min-confidence low # Show all candidates
210
- cicada-find-dead-code --format json # Output as JSON
193
+ cicada find-dead-code # Show high confidence candidates
194
+ cicada find-dead-code --min-confidence low # Show all candidates
195
+ cicada find-dead-code --format json # Output as JSON
211
196
  """,
212
197
  )
213
198
 
214
- parser.add_argument(
215
- "--index",
216
- default=".cicada/index.json",
217
- help="Path to index file (default: .cicada/index.json)",
218
- )
219
-
220
199
  parser.add_argument(
221
200
  "--format",
222
201
  choices=["markdown", "json"],
@@ -233,11 +212,11 @@ Examples:
233
212
 
234
213
  args = parser.parse_args()
235
214
 
236
- # Load index
237
- index_path = Path(args.index)
215
+ # Load index from centralized storage
216
+ index_path = get_index_path(".")
238
217
  if not index_path.exists():
239
218
  print(f"Error: Index file not found: {index_path}", file=sys.stderr)
240
- print(f"\nRun 'cicada-index' first to create the index.", file=sys.stderr)
219
+ print("\nRun 'cicada index' first to create the index.", file=sys.stderr)
241
220
  sys.exit(1)
242
221
 
243
222
  try:
@@ -258,10 +237,7 @@ Examples:
258
237
  results = filter_by_confidence(results, args.min_confidence)
259
238
 
260
239
  # Format output
261
- if args.format == "json":
262
- output = format_json(results)
263
- else:
264
- output = format_markdown(results)
240
+ output = format_json(results) if args.format == "json" else format_markdown(results)
265
241
 
266
242
  print(output)
267
243
 
cicada/formatter.py CHANGED
@@ -6,13 +6,13 @@ This module provides formatting utilities for Cicada MCP server responses,
6
6
  supporting both Markdown and JSON output formats.
7
7
  """
8
8
 
9
+ import argparse
9
10
  import json
10
11
  import sys
11
12
  from pathlib import Path
12
- from typing import Optional, Dict, Any
13
- import argparse
13
+ from typing import Any
14
14
 
15
- from cicada.utils import FunctionGrouper, CallSiteFormatter, SignatureBuilder
15
+ from cicada.utils import CallSiteFormatter, FunctionGrouper, SignatureBuilder
16
16
 
17
17
 
18
18
  class ModuleFormatter:
@@ -24,7 +24,7 @@ class ModuleFormatter:
24
24
 
25
25
  @staticmethod
26
26
  def format_module_markdown(
27
- module_name: str, data: Dict[str, Any], private_functions: str = "exclude"
27
+ module_name: str, data: dict[str, Any], private_functions: str = "exclude"
28
28
  ) -> str:
29
29
  """
30
30
  Format module data as Markdown.
@@ -69,9 +69,7 @@ class ModuleFormatter:
69
69
  if public_grouped and private_functions != "only":
70
70
  lines.extend(["", "Public:", ""])
71
71
  # Sort by line number instead of function name
72
- for (_, _), clauses in sorted(
73
- public_grouped.items(), key=lambda x: x[1][0]["line"]
74
- ):
72
+ for (_, _), clauses in sorted(public_grouped.items(), key=lambda x: x[1][0]["line"]):
75
73
  # Use the first clause for display (they all have same name/arity)
76
74
  func = clauses[0]
77
75
  func_sig = SignatureBuilder.build(func)
@@ -81,9 +79,7 @@ class ModuleFormatter:
81
79
  if private_grouped and private_functions in ["include", "only"]:
82
80
  lines.extend(["", "Private:", ""])
83
81
  # Sort by line number instead of function name
84
- for (_, _), clauses in sorted(
85
- private_grouped.items(), key=lambda x: x[1][0]["line"]
86
- ):
82
+ for (_, _), clauses in sorted(private_grouped.items(), key=lambda x: x[1][0]["line"]):
87
83
  # Use the first clause for display (they all have same name/arity)
88
84
  func = clauses[0]
89
85
  func_sig = SignatureBuilder.build(func)
@@ -104,7 +100,7 @@ class ModuleFormatter:
104
100
 
105
101
  @staticmethod
106
102
  def format_module_json(
107
- module_name: str, data: Dict[str, Any], private_functions: str = "exclude"
103
+ module_name: str, data: dict[str, Any], private_functions: str = "exclude"
108
104
  ) -> str:
109
105
  """
110
106
  Format module data as JSON.
@@ -203,9 +199,7 @@ Module names are case-sensitive and must match exactly (e.g., `MyApp.User`, not
203
199
  return json.dumps(error_result, indent=2)
204
200
 
205
201
  @staticmethod
206
- def format_function_results_markdown(
207
- function_name: str, results: list[Dict[str, Any]]
208
- ) -> str:
202
+ def format_function_results_markdown(function_name: str, results: list[dict[str, Any]]) -> str:
209
203
  """
210
204
  Format function search results as Markdown.
211
205
 
@@ -254,7 +248,7 @@ No functions matching `{function_name}` were found in the index.
254
248
  else:
255
249
  lines = [
256
250
  f"Functions matching {function_name}",
257
- f"",
251
+ "",
258
252
  f"Found {len(consolidated_results)} match(es):",
259
253
  ]
260
254
 
@@ -287,18 +281,14 @@ No functions matching `{function_name}` were found in the index.
287
281
  # Add documentation if present
288
282
  if func.get("doc"):
289
283
  if len(consolidated_results) == 1:
290
- lines.extend(
291
- ["", f"{indent}Documentation:", "", f"{indent}{func['doc']}"]
292
- )
284
+ lines.extend(["", f"{indent}Documentation:", "", f"{indent}{func['doc']}"])
293
285
  else:
294
286
  lines.extend(["", "Documentation:", "", func["doc"]])
295
287
 
296
288
  # Add examples if present
297
289
  if func.get("examples"):
298
290
  if len(consolidated_results) == 1:
299
- lines.extend(
300
- ["", f"{indent}Examples:", "", f"{indent}{func['examples']}"]
301
- )
291
+ lines.extend(["", f"{indent}Examples:", "", f"{indent}{func['examples']}"])
302
292
  else:
303
293
  lines.extend(["", "Examples:", "", func["examples"]])
304
294
 
@@ -321,23 +311,17 @@ No functions matching `{function_name}` were found in the index.
321
311
  if has_examples:
322
312
  # Separate into code and test call sites WITH examples
323
313
  code_sites_with_examples = [
324
- s
325
- for s in call_sites_with_examples
326
- if "test" not in s["file"].lower()
314
+ s for s in call_sites_with_examples if "test" not in s["file"].lower()
327
315
  ]
328
316
  test_sites_with_examples = [
329
- s
330
- for s in call_sites_with_examples
331
- if "test" in s["file"].lower()
317
+ s for s in call_sites_with_examples if "test" in s["file"].lower()
332
318
  ]
333
319
 
334
320
  lines.append(f"{indent}Usage Examples:")
335
321
 
336
322
  if code_sites_with_examples:
337
323
  # Group code sites by caller
338
- grouped_code = CallSiteFormatter.group_by_caller(
339
- code_sites_with_examples
340
- )
324
+ grouped_code = CallSiteFormatter.group_by_caller(code_sites_with_examples)
341
325
  code_count = sum(len(site["lines"]) for site in grouped_code)
342
326
  lines.append(f"{indent}Code ({code_count}):")
343
327
  for site in grouped_code:
@@ -350,12 +334,8 @@ No functions matching `{function_name}` were found in the index.
350
334
 
351
335
  # Show consolidated line numbers only if multiple lines
352
336
  if len(site["lines"]) > 1:
353
- line_list = ", ".join(
354
- f":{line}" for line in site["lines"]
355
- )
356
- lines.append(
357
- f"{indent}- {caller} at {site['file']}{line_list}"
358
- )
337
+ line_list = ", ".join(f":{line}" for line in site["lines"])
338
+ lines.append(f"{indent}- {caller} at {site['file']}{line_list}")
359
339
  else:
360
340
  lines.append(f"{indent}- {caller} at {site['file']}")
361
341
 
@@ -371,9 +351,7 @@ No functions matching `{function_name}` were found in the index.
371
351
  if code_sites_with_examples:
372
352
  lines.append("") # Blank line between sections
373
353
  # Group test sites by caller
374
- grouped_test = CallSiteFormatter.group_by_caller(
375
- test_sites_with_examples
376
- )
354
+ grouped_test = CallSiteFormatter.group_by_caller(test_sites_with_examples)
377
355
  test_count = sum(len(site["lines"]) for site in grouped_test)
378
356
  lines.append(f"{indent}Test ({test_count}):")
379
357
  for site in grouped_test:
@@ -386,12 +364,8 @@ No functions matching `{function_name}` were found in the index.
386
364
 
387
365
  # Show consolidated line numbers only if multiple lines
388
366
  if len(site["lines"]) > 1:
389
- line_list = ", ".join(
390
- f":{line}" for line in site["lines"]
391
- )
392
- lines.append(
393
- f"{indent}- {caller} at {site['file']}{line_list}"
394
- )
367
+ line_list = ", ".join(f":{line}" for line in site["lines"])
368
+ lines.append(f"{indent}- {caller} at {site['file']}{line_list}")
395
369
  else:
396
370
  lines.append(f"{indent}- {caller} at {site['file']}")
397
371
 
@@ -419,14 +393,10 @@ No functions matching `{function_name}` were found in the index.
419
393
  if remaining_call_sites:
420
394
  # Separate into code and test
421
395
  remaining_code = [
422
- s
423
- for s in remaining_call_sites
424
- if "test" not in s["file"].lower()
396
+ s for s in remaining_call_sites if "test" not in s["file"].lower()
425
397
  ]
426
398
  remaining_test = [
427
- s
428
- for s in remaining_call_sites
429
- if "test" in s["file"].lower()
399
+ s for s in remaining_call_sites if "test" in s["file"].lower()
430
400
  ]
431
401
 
432
402
  lines.append("")
@@ -447,12 +417,8 @@ No functions matching `{function_name}` were found in the index.
447
417
  else:
448
418
  caller = site["calling_module"]
449
419
 
450
- line_list = ", ".join(
451
- f":{line}" for line in site["lines"]
452
- )
453
- lines.append(
454
- f"{indent}- {caller} at {site['file']}{line_list}"
455
- )
420
+ line_list = ", ".join(f":{line}" for line in site["lines"])
421
+ lines.append(f"{indent}- {caller} at {site['file']}{line_list}")
456
422
 
457
423
  if remaining_test:
458
424
  if remaining_code:
@@ -471,17 +437,11 @@ No functions matching `{function_name}` were found in the index.
471
437
  else:
472
438
  caller = site["calling_module"]
473
439
 
474
- line_list = ", ".join(
475
- f":{line}" for line in site["lines"]
476
- )
477
- lines.append(
478
- f"{indent}- {caller} at {site['file']}{line_list}"
479
- )
440
+ line_list = ", ".join(f":{line}" for line in site["lines"])
441
+ lines.append(f"{indent}- {caller} at {site['file']}{line_list}")
480
442
  else:
481
443
  # Separate into code and test call sites
482
- code_sites = [
483
- s for s in call_sites if "test" not in s["file"].lower()
484
- ]
444
+ code_sites = [s for s in call_sites if "test" not in s["file"].lower()]
485
445
  test_sites = [s for s in call_sites if "test" in s["file"].lower()]
486
446
 
487
447
  call_count = len(call_sites)
@@ -504,9 +464,7 @@ No functions matching `{function_name}` were found in the index.
504
464
 
505
465
  # Show consolidated line numbers
506
466
  line_list = ", ".join(f":{line}" for line in site["lines"])
507
- lines.append(
508
- f"{indent}- {caller} at {site['file']}{line_list}"
509
- )
467
+ lines.append(f"{indent}- {caller} at {site['file']}{line_list}")
510
468
 
511
469
  if test_sites:
512
470
  if code_sites:
@@ -525,9 +483,7 @@ No functions matching `{function_name}` were found in the index.
525
483
 
526
484
  # Show consolidated line numbers
527
485
  line_list = ", ".join(f":{line}" for line in site["lines"])
528
- lines.append(
529
- f"{indent}- {caller} at {site['file']}{line_list}"
530
- )
486
+ lines.append(f"{indent}- {caller} at {site['file']}{line_list}")
531
487
  lines.append("")
532
488
  else:
533
489
  lines.extend([f"{indent}*No call sites found*"])
@@ -535,9 +491,7 @@ No functions matching `{function_name}` were found in the index.
535
491
  return "\n".join(lines)
536
492
 
537
493
  @staticmethod
538
- def format_function_results_json(
539
- function_name: str, results: list[Dict[str, Any]]
540
- ) -> str:
494
+ def format_function_results_json(function_name: str, results: list[dict[str, Any]]) -> str:
541
495
  """
542
496
  Format function search results as JSON.
543
497
 
@@ -593,9 +547,7 @@ No functions matching `{function_name}` were found in the index.
593
547
  return json.dumps(output, indent=2)
594
548
 
595
549
  @staticmethod
596
- def format_module_usage_markdown(
597
- module_name: str, usage_results: Dict[str, Any]
598
- ) -> str:
550
+ def format_module_usage_markdown(module_name: str, usage_results: dict[str, Any]) -> str:
599
551
  """
600
552
  Format module usage results as Markdown.
601
553
 
@@ -624,9 +576,7 @@ No functions matching `{function_name}` were found in the index.
624
576
  if imp["alias_name"] != module_name.split(".")[-1]
625
577
  else ""
626
578
  )
627
- lines.append(
628
- f"- `{imp['importing_module']}` {alias_info} — `{imp['file']}`"
629
- )
579
+ lines.append(f"- `{imp['importing_module']}` {alias_info} — `{imp['file']}`")
630
580
  lines.append("")
631
581
 
632
582
  # Show imports section
@@ -674,9 +624,7 @@ No functions matching `{function_name}` were found in the index.
674
624
  lines.append("")
675
625
 
676
626
  for call in fc["calls"]:
677
- alias_info = (
678
- f" (via `{call['alias_used']}`)" if call["alias_used"] else ""
679
- )
627
+ alias_info = f" (via `{call['alias_used']}`)" if call["alias_used"] else ""
680
628
  # Show unique line numbers for this function
681
629
  line_list = ", ".join(f":{line}" for line in sorted(call["lines"]))
682
630
  lines.append(
@@ -692,9 +640,7 @@ No functions matching `{function_name}` were found in the index.
692
640
  return "\n".join(lines)
693
641
 
694
642
  @staticmethod
695
- def format_module_usage_json(
696
- module_name: str, usage_results: Dict[str, Any]
697
- ) -> str:
643
+ def format_module_usage_json(module_name: str, usage_results: dict[str, Any]) -> str:
698
644
  """
699
645
  Format module usage results as JSON.
700
646
 
@@ -726,7 +672,7 @@ No functions matching `{function_name}` were found in the index.
726
672
 
727
673
  @staticmethod
728
674
  def format_keyword_search_results_markdown(
729
- _keywords: list[str], results: list[Dict[str, Any]]
675
+ _keywords: list[str], results: list[dict[str, Any]]
730
676
  ) -> str:
731
677
  """
732
678
  Format keyword search results as Markdown.
@@ -814,9 +760,9 @@ class JSONFormatter:
814
760
  data = json.loads(json_string)
815
761
  return json.dumps(data, indent=self.indent, sort_keys=self.sort_keys)
816
762
  except json.JSONDecodeError as e:
817
- raise ValueError(f"Invalid JSON: {e}")
763
+ raise ValueError(f"Invalid JSON: {e}") from e
818
764
 
819
- def format_file(self, input_path: Path, output_path: Optional[Path] = None) -> str:
765
+ def format_file(self, input_path: Path, output_path: Path | None = None) -> str:
820
766
  """
821
767
  Format a JSON file.
822
768
 
@@ -835,7 +781,7 @@ class JSONFormatter:
835
781
  raise FileNotFoundError(f"Input file not found: {input_path}")
836
782
 
837
783
  # Read the input file
838
- with open(input_path, "r") as f:
784
+ with open(input_path) as f:
839
785
  json_string = f.read()
840
786
 
841
787
  # Format the JSON