opencontext-cli 0.4.0b0__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 (36) hide show
  1. {opencontext_cli-0.4.0b0 → 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.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/ci_check_cmd.py +3 -1
  5. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/config_cmd.py +13 -10
  6. opencontext_cli-1.0.0/opencontext_cli/commands/extension_cmd.py +145 -0
  7. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/git_cmd.py +5 -3
  8. {opencontext_cli-0.4.0b0 → 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.4.0b0 → 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.4.0b0 → 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.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/update_cmd.py +14 -1
  19. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/main.py +893 -618
  20. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/PKG-INFO +3 -2
  21. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/SOURCES.txt +8 -0
  22. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/requires.txt +1 -0
  23. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/pyproject.toml +3 -2
  24. opencontext_cli-0.4.0b0/opencontext_cli/commands/kg_cmd.py +0 -193
  25. opencontext_cli-0.4.0b0/opencontext_cli/commands/menu_cmd.py +0 -442
  26. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/LICENSE +0 -0
  27. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/README.md +0 -0
  28. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/__init__.py +0 -0
  29. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/__main__.py +0 -0
  30. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/__init__.py +0 -0
  31. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/plugin_cmd.py +0 -0
  32. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/verify_cmd.py +0 -0
  33. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/dependency_links.txt +0 -0
  34. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/entry_points.txt +0 -0
  35. {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/top_level.txt +0 -0
  36. {opencontext_cli-0.4.0b0 → 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.4.0b0
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."
@@ -94,6 +94,8 @@ def handle_config(args: Any) -> None:
94
94
 
95
95
  run_wizard_menu()
96
96
  else:
97
+ from opencontext_core.wizard import run_wizard
98
+
97
99
  run_wizard(non_interactive=True)
98
100
  elif command == "show":
99
101
  show_config()
@@ -237,6 +239,12 @@ def _config_get(key: str) -> None:
237
239
  print(f"{key} = {value}")
238
240
  else:
239
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}?")
240
248
  print(f"Available paths ({len(CONFIG_PATHS)}):")
241
249
  for path, (typ, desc) in sorted(CONFIG_PATHS.items()):
242
250
  print(f" {path} ({typ.__name__}) {desc}")
@@ -302,23 +310,18 @@ def _config_cleanup(keep_days: int) -> None:
302
310
  except (ValueError, OSError):
303
311
  continue
304
312
 
305
- # Rebuild index
306
- [b for b in backups if b.id not in [r.id for r in ConfigBackupManager.list_backups()]]
307
- # Actually let's rebuild from disk
313
+ # Rebuild index from disk — removes stale index entries for deleted dirs
308
314
  index = []
309
315
  for entry_dir in sorted(ConfigBackupManager.BACKUP_DIR.iterdir()):
310
316
  if entry_dir.is_dir() and entry_dir.name.startswith("backup-"):
311
317
  try:
312
- ts = entry_dir.name.replace("backup-", "")
313
- desc = "auto-pre-change" # rough default
314
- files = []
315
- for f in entry_dir.iterdir():
316
- if f.is_file():
317
- 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())
318
321
  index.append(
319
322
  {
320
323
  "id": entry_dir.name,
321
- "timestamp": ts,
324
+ "timestamp": ts_str,
322
325
  "description": desc,
323
326
  "files": files,
324
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}")