opencontext-cli 0.3.0__tar.gz → 1.0.0__tar.gz

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 (35) hide show
  1. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/PKG-INFO +3 -2
  2. opencontext_cli-1.0.0/opencontext_cli/commands/benchmark_cmd.py +162 -0
  3. opencontext_cli-1.0.0/opencontext_cli/commands/bridges_cmd.py +151 -0
  4. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/ci_check_cmd.py +3 -1
  5. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/config_cmd.py +25 -13
  6. opencontext_cli-1.0.0/opencontext_cli/commands/extension_cmd.py +145 -0
  7. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/git_cmd.py +5 -3
  8. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/hints_cmd.py +7 -5
  9. opencontext_cli-1.0.0/opencontext_cli/commands/kg_cmd.py +1056 -0
  10. opencontext_cli-1.0.0/opencontext_cli/commands/menu_cmd.py +653 -0
  11. opencontext_cli-1.0.0/opencontext_cli/commands/privacy_cmd.py +260 -0
  12. opencontext_cli-1.0.0/opencontext_cli/commands/review_cmd.py +221 -0
  13. opencontext_cli-1.0.0/opencontext_cli/commands/routes_cmd.py +81 -0
  14. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/setup_cmd.py +155 -80
  15. opencontext_cli-1.0.0/opencontext_cli/commands/skill_cmd.py +148 -0
  16. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/sync_cmd.py +130 -12
  17. opencontext_cli-1.0.0/opencontext_cli/commands/telemetry_cmd.py +74 -0
  18. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/update_cmd.py +34 -10
  19. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/main.py +951 -626
  20. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/PKG-INFO +3 -2
  21. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/SOURCES.txt +9 -0
  22. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/requires.txt +1 -0
  23. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/pyproject.toml +3 -2
  24. opencontext_cli-0.3.0/opencontext_cli/commands/kg_cmd.py +0 -193
  25. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/LICENSE +0 -0
  26. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/README.md +0 -0
  27. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/__init__.py +0 -0
  28. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/__main__.py +0 -0
  29. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/__init__.py +0 -0
  30. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/plugin_cmd.py +0 -0
  31. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli/commands/verify_cmd.py +0 -0
  32. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/dependency_links.txt +0 -0
  33. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/entry_points.txt +0 -0
  34. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/top_level.txt +0 -0
  35. {opencontext_cli-0.3.0 → opencontext_cli-1.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencontext-cli
3
- Version: 0.3.0
3
+ Version: 1.0.0
4
4
  Summary: CLI adapter for OpenContext Runtime
5
5
  Author: OpenContext Runtime maintainers
6
6
  License-Expression: MIT
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://github.com/CesarMSFelipe/OpenContext-Runtime
9
9
  Project-URL: Issues, https://github.com/CesarMSFelipe/OpenContext-Runtime/issues
10
10
  Project-URL: Source, https://github.com/CesarMSFelipe/OpenContext-Runtime
11
11
  Keywords: ai,llm,context-engineering,cli,security
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Programming Language :: Python :: 3
@@ -21,6 +21,7 @@ Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: opencontext-core>=0.1.0
23
23
  Requires-Dist: opencontext-profiles>=0.1.0
24
+ Requires-Dist: argcomplete>=3.0.0
24
25
  Dynamic: license-file
25
26
 
26
27
  # opencontext-cli
@@ -0,0 +1,162 @@
1
+ """Benchmark CLI commands: run, list, and compare benchmarks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from opencontext_core.dx.console_styles import console
10
+ from opencontext_core.evaluation.benchmark_suite import (
11
+ BenchmarkSuite,
12
+ format_benchmark_report_markdown,
13
+ format_benchmark_result,
14
+ format_benchmark_result_json,
15
+ load_last_result,
16
+ save_result,
17
+ )
18
+
19
+
20
+ def add_benchmark_parser(subparsers: Any) -> None:
21
+ """Add benchmark command parsers."""
22
+ bm_parser = subparsers.add_parser("benchmark", help="Run and manage benchmarks.")
23
+ bm_sub = bm_parser.add_subparsers(dest="benchmark_command", required=True)
24
+
25
+ # benchmark list
26
+ list_parser = bm_sub.add_parser("list", help="List available benchmark cases.")
27
+ list_parser.add_argument("--category", default=None, help="Filter by category.")
28
+
29
+ # benchmark run
30
+ run_parser = bm_sub.add_parser("run", help="Run benchmark cases.")
31
+ run_parser.add_argument("--case", default=None, help="Specific case ID to run.")
32
+ run_parser.add_argument("--category", default=None, help="Filter by category.")
33
+ run_parser.add_argument(
34
+ "--format",
35
+ default="text",
36
+ choices=["text", "json", "markdown"],
37
+ help="Output format.",
38
+ )
39
+ run_parser.add_argument("--output", default=None, help="Output file (for markdown).")
40
+ run_parser.add_argument("--save", action="store_true", help="Save results.")
41
+
42
+ # benchmark compare
43
+ compare_parser = bm_sub.add_parser("compare", help="Compare against last baseline.")
44
+ compare_parser.add_argument(
45
+ "--output", default=None, help="Output file for markdown comparison."
46
+ )
47
+
48
+
49
+ def handle_benchmark(args: Any) -> None:
50
+ """Handle benchmark commands."""
51
+ command = args.benchmark_command
52
+
53
+ if command == "list":
54
+ _handle_list(args)
55
+ elif command == "run":
56
+ _handle_run(args)
57
+ elif command == "compare":
58
+ _handle_compare(args)
59
+
60
+
61
+ def _handle_list(args: Any) -> None:
62
+ """List available benchmark cases."""
63
+ suite = BenchmarkSuite()
64
+ cases = suite.list_cases(category=args.category)
65
+
66
+ if not cases:
67
+ console.print("[yellow]No benchmark cases found.[/]")
68
+ return
69
+
70
+ from rich.table import Table
71
+
72
+ table = Table(title=f"Benchmark Cases ({len(cases)})")
73
+ table.add_column("ID", style="cyan")
74
+ table.add_column("Name")
75
+ table.add_column("Category")
76
+ table.add_column("Min Score", justify="right")
77
+
78
+ for case in cases:
79
+ table.add_row(case.id, case.name, case.category, str(case.expected_min_score))
80
+
81
+ console.print(table)
82
+
83
+
84
+ def _handle_run(args: Any) -> None:
85
+ """Run benchmark cases."""
86
+ suite = BenchmarkSuite()
87
+
88
+ if sys.stdout.encoding and "utf" not in sys.stdout.encoding.lower():
89
+ import io
90
+
91
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
92
+
93
+ # Determine which cases to run
94
+ if args.case:
95
+ case_ids = [args.case]
96
+ elif args.category:
97
+ cases = suite.list_cases(category=args.category)
98
+ case_ids = [c.id for c in cases]
99
+ else:
100
+ case_ids = None
101
+
102
+ with console.status("[bold green]Running benchmarks..."):
103
+ result = suite.run(case_ids=case_ids)
104
+
105
+ # Format output
106
+ if args.format == "json":
107
+ # Plain print to avoid Rich line wrapping breaking JSON
108
+ print(format_benchmark_result_json(result))
109
+ elif args.format == "markdown":
110
+ output = format_benchmark_report_markdown(result, output_path=args.output)
111
+ if not args.output:
112
+ console.print(output)
113
+ else:
114
+ console.print(format_benchmark_result(result))
115
+
116
+ if args.format == "markdown" and args.output:
117
+ console.print(f"[green]Report written to {args.output}[/]")
118
+
119
+ # Save baseline
120
+ if args.save:
121
+ path = save_result(result)
122
+ # Print to stderr so it doesn't mix with JSON/markdown stdout output
123
+ import sys as _sys
124
+
125
+ print(f"Baseline saved to {path}", file=_sys.stderr)
126
+
127
+ # Exit code
128
+ if result.failed > 0:
129
+ sys.exit(1)
130
+
131
+
132
+ def _handle_compare(args: Any) -> None:
133
+ """Compare against last saved baseline."""
134
+ from opencontext_core.evaluation.benchmark_suite import compare_results
135
+
136
+ baseline = load_last_result()
137
+ if baseline is None:
138
+ console.print("[yellow]No baseline found. Run `opencontext benchmark run --save` first.[/]")
139
+ sys.exit(1)
140
+
141
+ suite = BenchmarkSuite()
142
+ with console.status("[bold green]Running current benchmarks..."):
143
+ current = suite.run()
144
+
145
+ output = compare_results(baseline, current)
146
+
147
+ if args.output:
148
+ Path(args.output).write_text(output, encoding="utf-8")
149
+ console.print(f"[green]Comparison written to {args.output}[/]")
150
+ else:
151
+ console.print(output)
152
+
153
+ # Check for regressions
154
+ regressions = 0
155
+ for r in current.results:
156
+ b_map = {r.case_id: r.score.overall for r in baseline.results}
157
+ if r.case_id in b_map and r.score.overall < b_map[r.case_id] - 2:
158
+ regressions += 1
159
+
160
+ if regressions:
161
+ console.print(f"[red]{regressions} regression(s) detected.[/]")
162
+ sys.exit(1)
@@ -0,0 +1,151 @@
1
+ """Bridges CLI command — scan and display cross-language bridge boundaries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import json
7
+ from typing import Any
8
+
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from opencontext_core.indexing.bridge_detector import BridgeDetector
13
+
14
+ console = Console()
15
+
16
+
17
+ def add_bridges_parser(subparsers: Any) -> None:
18
+ """Add bridges command parser."""
19
+ bridges_parser = subparsers.add_parser(
20
+ "bridges",
21
+ help="Detect cross-language call boundaries in your project.",
22
+ )
23
+ bridges_sub = bridges_parser.add_subparsers(dest="bridges_command")
24
+
25
+ scan_parser = bridges_sub.add_parser("scan", help="Scan project for cross-language bridges.")
26
+ scan_parser.add_argument("root", nargs="?", default=".", help="Project root to scan.")
27
+ scan_parser.add_argument(
28
+ "--type",
29
+ default=None,
30
+ choices=["HTTP", "GRPC", "CLI_SUBPROCESS", "IPC"],
31
+ help="Filter by bridge type.",
32
+ )
33
+ scan_parser.add_argument(
34
+ "--min-confidence",
35
+ type=float,
36
+ default=0.0,
37
+ help="Minimum confidence threshold (0.0-1.0).",
38
+ )
39
+ scan_parser.add_argument(
40
+ "--json",
41
+ action="store_true",
42
+ dest="json",
43
+ help="Output as JSON.",
44
+ )
45
+
46
+ show_parser = bridges_sub.add_parser("show", help="Show bridges for a specific symbol or file.")
47
+ show_parser.add_argument("symbol", help="Symbol name or file path fragment to filter by.")
48
+ show_parser.add_argument("root", nargs="?", default=".", help="Project root to scan.")
49
+
50
+
51
+ def handle_bridges(args: Any) -> None:
52
+ """Handle bridges commands."""
53
+ command = getattr(args, "bridges_command", None)
54
+ root = getattr(args, "root", ".")
55
+
56
+ if command is None:
57
+ import subprocess
58
+
59
+ subprocess.run(["opencontext", "bridges", "--help"])
60
+ return
61
+
62
+ if command == "scan":
63
+ _handle_scan(
64
+ root,
65
+ bridge_type=getattr(args, "type", None),
66
+ min_confidence=getattr(args, "min_confidence", 0.0),
67
+ output_json=getattr(args, "json", False),
68
+ )
69
+ elif command == "show":
70
+ _handle_show(root, symbol=args.symbol)
71
+
72
+
73
+ def _handle_scan(
74
+ root: str,
75
+ bridge_type: str | None = None,
76
+ min_confidence: float = 0.0,
77
+ output_json: bool = False,
78
+ ) -> None:
79
+ """Scan and display all detected bridges."""
80
+ with console.status("[bold green]Scanning for cross-language bridges..."):
81
+ detector = BridgeDetector()
82
+ bridges = detector.scan(root)
83
+
84
+ if bridge_type:
85
+ bridges = [b for b in bridges if b.bridge_type == bridge_type]
86
+ if min_confidence > 0:
87
+ bridges = [b for b in bridges if b.confidence >= min_confidence]
88
+
89
+ if not bridges:
90
+ console.print("[dim]No cross-language bridges detected.[/]")
91
+ return
92
+
93
+ if output_json:
94
+ print(json.dumps([dataclasses.asdict(b) for b in bridges], indent=2))
95
+ return
96
+
97
+ table = Table(title=f"Cross-Language Bridges ({len(bridges)} found)")
98
+ table.add_column("File", style="cyan", max_width=40)
99
+ table.add_column("Line", justify="right")
100
+ table.add_column("Type", style="yellow")
101
+ table.add_column("Confidence", justify="right")
102
+ table.add_column("Target Hint", max_width=40)
103
+
104
+ for b in bridges:
105
+ conf_style = (
106
+ "green" if b.confidence >= 0.85 else ("yellow" if b.confidence >= 0.7 else "dim")
107
+ )
108
+ table.add_row(
109
+ b.source_file,
110
+ str(b.line),
111
+ b.bridge_type,
112
+ f"[{conf_style}]{b.confidence:.0%}[/]",
113
+ b.target_hint,
114
+ )
115
+
116
+ console.print(table)
117
+
118
+ type_counts: dict[str, int] = {}
119
+ for b in bridges:
120
+ type_counts[b.bridge_type] = type_counts.get(b.bridge_type, 0) + 1
121
+ summary = " | ".join(f"{t}: {c}" for t, c in sorted(type_counts.items()))
122
+ console.print(f"[dim]By type: {summary}[/]")
123
+
124
+
125
+ def _handle_show(root: str, symbol: str) -> None:
126
+ """Show bridges filtered by symbol or file path fragment."""
127
+ detector = BridgeDetector()
128
+ bridges = detector.scan(root)
129
+ sym = symbol.lower()
130
+ filtered = [
131
+ b
132
+ for b in bridges
133
+ if sym in b.source_file.lower()
134
+ or sym in b.bridge_type.lower()
135
+ or sym in b.target_hint.lower()
136
+ ]
137
+
138
+ if not filtered:
139
+ console.print(f"[dim]No bridges found matching '{symbol}'.[/]")
140
+ return
141
+
142
+ table = Table(title=f"Bridges matching '{symbol}'")
143
+ table.add_column("File", style="cyan")
144
+ table.add_column("Line", justify="right")
145
+ table.add_column("Type", style="yellow")
146
+ table.add_column("Target Hint")
147
+
148
+ for b in filtered:
149
+ table.add_row(b.source_file, str(b.line), b.bridge_type, b.target_hint)
150
+
151
+ console.print(table)
@@ -53,7 +53,9 @@ jobs:
53
53
 
54
54
  def add_ci_check_parser(subparsers: Any) -> None:
55
55
  """Add ci-check command parsers."""
56
- check_parser = subparsers.add_parser("ci-check", help="CI check management.")
56
+ import argparse
57
+
58
+ check_parser = subparsers.add_parser("ci-check", help=argparse.SUPPRESS)
57
59
  check_sub = check_parser.add_subparsers(dest="ci_check_command", required=True)
58
60
  check_init = check_sub.add_parser(
59
61
  "init", help="Initialize checks directory and ContextBench workflow."
@@ -16,7 +16,6 @@ from opencontext_core.user_prefs import UserConfigStore
16
16
  from opencontext_core.wizard import (
17
17
  reconfigure,
18
18
  reset_config,
19
- run_wizard,
20
19
  show_config,
21
20
  )
22
21
 
@@ -25,7 +24,7 @@ def add_config_parser(subparsers: Any) -> None:
25
24
  """Add config command parsers."""
26
25
 
27
26
  config_parser = subparsers.add_parser("config", help="Manage OpenContext configuration.")
28
- config_sub = config_parser.add_subparsers(dest="config_command", required=True)
27
+ config_sub = config_parser.add_subparsers(dest="config_command")
29
28
 
30
29
  # Wizard
31
30
  wizard_parser = config_sub.add_parser("wizard", help="Run configuration wizard.")
@@ -76,7 +75,17 @@ def add_config_parser(subparsers: Any) -> None:
76
75
  def handle_config(args: Any) -> None:
77
76
  """Handle config commands."""
78
77
 
79
- command = args.config_command
78
+ command = getattr(args, "config_command", None)
79
+
80
+ if command is None:
81
+ # No subcommand — run the interactive wizard by default
82
+ from opencontext_core.wizard import run_wizard, run_wizard_menu
83
+
84
+ try:
85
+ run_wizard_menu()
86
+ except Exception:
87
+ run_wizard(non_interactive=True)
88
+ return
80
89
 
81
90
  if command == "wizard":
82
91
  use_tui = not getattr(args, "non_interactive", False)
@@ -85,6 +94,8 @@ def handle_config(args: Any) -> None:
85
94
 
86
95
  run_wizard_menu()
87
96
  else:
97
+ from opencontext_core.wizard import run_wizard
98
+
88
99
  run_wizard(non_interactive=True)
89
100
  elif command == "show":
90
101
  show_config()
@@ -228,6 +239,12 @@ def _config_get(key: str) -> None:
228
239
  print(f"{key} = {value}")
229
240
  else:
230
241
  print(f"Unknown key: {key}")
242
+ # Suggest the closest key (replace dots with underscores for display)
243
+ candidates = sorted(CONFIG_PATHS.keys())
244
+ key_norm = key.lower().replace(".", "_")
245
+ suggestions = [c for c in candidates if key_norm in c.lower() or c.lower() in key_norm]
246
+ if suggestions:
247
+ print(f"Hint: did you mean {suggestions[0]!r}?")
231
248
  print(f"Available paths ({len(CONFIG_PATHS)}):")
232
249
  for path, (typ, desc) in sorted(CONFIG_PATHS.items()):
233
250
  print(f" {path} ({typ.__name__}) {desc}")
@@ -293,23 +310,18 @@ def _config_cleanup(keep_days: int) -> None:
293
310
  except (ValueError, OSError):
294
311
  continue
295
312
 
296
- # Rebuild index
297
- [b for b in backups if b.id not in [r.id for r in ConfigBackupManager.list_backups()]]
298
- # Actually let's rebuild from disk
313
+ # Rebuild index from disk — removes stale index entries for deleted dirs
299
314
  index = []
300
315
  for entry_dir in sorted(ConfigBackupManager.BACKUP_DIR.iterdir()):
301
316
  if entry_dir.is_dir() and entry_dir.name.startswith("backup-"):
302
317
  try:
303
- ts = entry_dir.name.replace("backup-", "")
304
- desc = "auto-pre-change" # rough default
305
- files = []
306
- for f in entry_dir.iterdir():
307
- if f.is_file():
308
- files.append(f.name)
318
+ ts_str = entry_dir.name.replace("backup-", "")
319
+ desc = "auto-pre-change"
320
+ files = sorted(f.name for f in entry_dir.iterdir() if f.is_file())
309
321
  index.append(
310
322
  {
311
323
  "id": entry_dir.name,
312
- "timestamp": ts,
324
+ "timestamp": ts_str,
313
325
  "description": desc,
314
326
  "files": files,
315
327
  }
@@ -0,0 +1,145 @@
1
+ """Extension CLI command — search, install, list, and remove workflow extensions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from opencontext_core.workflow.extension_registry import ExtensionRegistry
12
+
13
+ console = Console()
14
+
15
+
16
+ def add_extension_parser(subparsers: Any) -> None:
17
+ """Add extension command parser."""
18
+ ext_parser = subparsers.add_parser(
19
+ "extension",
20
+ help="Manage OpenContext workflow extensions.",
21
+ )
22
+ ext_sub = ext_parser.add_subparsers(dest="extension_command", required=True)
23
+
24
+ search_parser = ext_sub.add_parser("search", help="Search available extensions.")
25
+ search_parser.add_argument("query", nargs="?", default="", help="Search query.")
26
+ search_parser.add_argument(
27
+ "--json",
28
+ action="store_true",
29
+ dest="json",
30
+ help="Output as JSON.",
31
+ )
32
+
33
+ install_parser = ext_sub.add_parser("install", help="Install an extension.")
34
+ install_parser.add_argument("name", help="Extension name to install.")
35
+ install_parser.add_argument("--root", default=".", help="Project root.")
36
+
37
+ list_parser = ext_sub.add_parser("list", help="List installed extensions.")
38
+ list_parser.add_argument("--root", default=".", help="Project root.")
39
+
40
+ info_parser = ext_sub.add_parser("info", help="Show details for an available extension.")
41
+ info_parser.add_argument("name", help="Extension name.")
42
+
43
+ remove_parser = ext_sub.add_parser("remove", help="Remove an installed extension.")
44
+ remove_parser.add_argument("name", help="Extension name to remove.")
45
+ remove_parser.add_argument("--root", default=".", help="Project root.")
46
+
47
+
48
+ def handle_extension(args: Any) -> None:
49
+ """Handle extension commands."""
50
+ command = args.extension_command
51
+ registry = ExtensionRegistry()
52
+ root = getattr(args, "root", ".")
53
+
54
+ if command == "search":
55
+ _handle_search(
56
+ registry, getattr(args, "query", ""), output_json=getattr(args, "json", False)
57
+ )
58
+ elif command == "install":
59
+ _handle_install(registry, args.name, root)
60
+ elif command == "list":
61
+ _handle_list(registry, root)
62
+ elif command == "info":
63
+ _handle_info(registry, args.name)
64
+ elif command == "remove":
65
+ _handle_remove(registry, args.name, root)
66
+
67
+
68
+ def _handle_search(registry: ExtensionRegistry, query: str, output_json: bool = False) -> None:
69
+ results = registry.search(query)
70
+ if not results:
71
+ console.print("[yellow]No extensions found.[/]")
72
+ return
73
+
74
+ if output_json:
75
+ print(json.dumps(results, indent=2))
76
+ return
77
+
78
+ table = Table(title=f"Extensions ({len(results)} found)")
79
+ table.add_column("Name", style="cyan")
80
+ table.add_column("Version")
81
+ table.add_column("Description")
82
+ table.add_column("Tags")
83
+ for ext in results:
84
+ table.add_row(
85
+ ext.get("name", ""),
86
+ ext.get("version", ""),
87
+ ext.get("description", ""),
88
+ ", ".join(ext.get("tags", [])),
89
+ )
90
+ console.print(table)
91
+
92
+
93
+ def _handle_install(registry: ExtensionRegistry, name: str, root: str) -> None:
94
+ try:
95
+ path = registry.install(name, root=root)
96
+ console.print(f"[green]✓ Installed extension '{name}' to {path}[/]")
97
+ except ValueError as e:
98
+ console.print(f"[red]Error: {e}[/]")
99
+
100
+
101
+ def _handle_list(registry: ExtensionRegistry, root: str) -> None:
102
+ installed = registry.list_installed(root=root)
103
+ if not installed:
104
+ console.print(
105
+ "[dim]No extensions installed. Use 'opencontext extension search' to find some.[/]"
106
+ )
107
+ return
108
+
109
+ table = Table(title="Installed Extensions")
110
+ table.add_column("Name", style="cyan")
111
+ table.add_column("Version")
112
+ table.add_column("Author")
113
+ table.add_column("Description")
114
+ for m in installed:
115
+ table.add_row(m.name, m.version, m.author, m.description)
116
+ console.print(table)
117
+
118
+
119
+ def _handle_info(registry: ExtensionRegistry, name: str) -> None:
120
+ matches = [e for e in registry.search() if e.get("name") == name]
121
+ if not matches:
122
+ console.print(f"[red]Extension not found: {name}[/]")
123
+ return
124
+ ext = matches[0]
125
+ from rich.panel import Panel
126
+
127
+ content = "\n".join(
128
+ [
129
+ f"[bold]Name:[/] {ext.get('name', '')}",
130
+ f"[bold]Version:[/] {ext.get('version', '')}",
131
+ f"[bold]Author:[/] {ext.get('author', '')}",
132
+ f"[bold]Description:[/] {ext.get('description', '')}",
133
+ f"[bold]Tags:[/] {', '.join(ext.get('tags', []))}",
134
+ f"[bold]Requires:[/] opencontext-core >= {ext.get('requires_version', 'any')}",
135
+ ]
136
+ )
137
+ console.print(Panel(content, title=f"Extension: {name}", border_style="cyan"))
138
+
139
+
140
+ def _handle_remove(registry: ExtensionRegistry, name: str, root: str) -> None:
141
+ removed = registry.remove(name, root=root)
142
+ if removed:
143
+ console.print(f"[green]✓ Removed extension '{name}'[/]")
144
+ else:
145
+ console.print(f"[yellow]Extension '{name}' not found (already removed?)[/]")
@@ -11,7 +11,9 @@ from opencontext_core.indexing.git_context import GitContextProvider
11
11
 
12
12
  def add_git_parser(subparsers: Any) -> None:
13
13
  """Add git command parsers."""
14
- git_parser = subparsers.add_parser("git", help="Git context and history.")
14
+ import argparse
15
+
16
+ git_parser = subparsers.add_parser("git", help=argparse.SUPPRESS)
15
17
  git_sub = git_parser.add_subparsers(dest="git_command", required=True)
16
18
  git_sub.add_parser("status", help="Show git repository stats.")
17
19
  git_history = git_sub.add_parser("history", help="Show git history for a file.")
@@ -72,7 +74,7 @@ def handle_git(args: Any) -> None:
72
74
  elif command == "recent":
73
75
  diffs = provider.get_recent_changes(days=days, max_commits=max_commits)
74
76
  if json_output:
75
- data = [
77
+ recent_data: list[dict[str, Any]] = [
76
78
  {
77
79
  "commit_hash": d.commit_hash,
78
80
  "author": d.author,
@@ -82,7 +84,7 @@ def handle_git(args: Any) -> None:
82
84
  }
83
85
  for d in diffs
84
86
  ]
85
- print(json.dumps(data, indent=2))
87
+ print(json.dumps(recent_data, indent=2))
86
88
  else:
87
89
  console.header(f"Recent Changes (last {days} days)")
88
90
  console.print(_format_git_recent(diffs))
@@ -11,7 +11,9 @@ from opencontext_core.dx.console_styles import console
11
11
 
12
12
  def add_hints_parser(subparsers: Any) -> None:
13
13
  """Add hints command parsers."""
14
- hints_parser = subparsers.add_parser("hints", help="Agent hints management.")
14
+ import argparse
15
+
16
+ hints_parser = subparsers.add_parser("hints", help=argparse.SUPPRESS)
15
17
  hints_sub = hints_parser.add_subparsers(dest="hints_command", required=True)
16
18
  hints_sub.add_parser("init", help="Initialize .opencontexthints file.")
17
19
  hints_sub.add_parser("show", help="Show combined hints.")
@@ -45,8 +47,8 @@ def handle_hints(args: Any) -> None:
45
47
  console.warning("No hints found. Run 'opencontext hints init' to create them.")
46
48
  elif command == "validate":
47
49
  files = manager.discover_hints()
48
- valid = []
49
- invalid = []
50
+ valid: list[str] = []
51
+ invalid: list[str] = []
50
52
  for f in files:
51
53
  parsed = manager.parse_hints_file(f)
52
54
  if parsed:
@@ -60,8 +62,8 @@ def handle_hints(args: Any) -> None:
60
62
  console.success(f"Valid: {len(valid)}")
61
63
  if invalid:
62
64
  console.error(f"Invalid: {len(invalid)}")
63
- for f in invalid:
64
- console.print(f" [dim]✗ {f}[/]")
65
+ for item in invalid:
66
+ console.print(f" [dim]✗ {item}[/]")
65
67
  console.info(f"Total files checked: {len(files)}")
66
68
  else:
67
69
  console.error(f"Unknown hints command: {command}")