opencontext-cli 0.1.0__tar.gz → 0.2.1b0__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 (25) hide show
  1. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/PKG-INFO +1 -1
  2. opencontext_cli-0.2.1b0/opencontext_cli/commands/__init__.py +1 -0
  3. opencontext_cli-0.2.1b0/opencontext_cli/commands/ci_check_cmd.py +115 -0
  4. opencontext_cli-0.2.1b0/opencontext_cli/commands/config_cmd.py +242 -0
  5. opencontext_cli-0.2.1b0/opencontext_cli/commands/git_cmd.py +139 -0
  6. opencontext_cli-0.2.1b0/opencontext_cli/commands/hints_cmd.py +67 -0
  7. opencontext_cli-0.2.1b0/opencontext_cli/commands/kg_cmd.py +193 -0
  8. opencontext_cli-0.2.1b0/opencontext_cli/commands/plugin_cmd.py +366 -0
  9. opencontext_cli-0.2.1b0/opencontext_cli/commands/setup_cmd.py +346 -0
  10. opencontext_cli-0.2.1b0/opencontext_cli/commands/sync_cmd.py +187 -0
  11. opencontext_cli-0.2.1b0/opencontext_cli/commands/update_cmd.py +86 -0
  12. opencontext_cli-0.2.1b0/opencontext_cli/commands/verify_cmd.py +122 -0
  13. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli/main.py +510 -54
  14. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/PKG-INFO +1 -1
  15. opencontext_cli-0.2.1b0/opencontext_cli.egg-info/SOURCES.txt +22 -0
  16. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/pyproject.toml +1 -1
  17. opencontext_cli-0.1.0/opencontext_cli.egg-info/SOURCES.txt +0 -11
  18. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/LICENSE +0 -0
  19. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/README.md +0 -0
  20. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli/__init__.py +0 -0
  21. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/dependency_links.txt +0 -0
  22. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/entry_points.txt +0 -0
  23. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/requires.txt +0 -0
  24. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/top_level.txt +0 -0
  25. {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencontext-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1b0
4
4
  Summary: CLI adapter for OpenContext Runtime
5
5
  Author: OpenContext Runtime maintainers
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ """CLI command modules for OpenContext."""
@@ -0,0 +1,115 @@
1
+ """CI check CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from opencontext_core.dx.console_styles import console
9
+ from opencontext_core.quality.ci_checks import CheckRunner
10
+
11
+
12
+ def add_ci_check_parser(subparsers: Any) -> None:
13
+ """Add ci-check command parsers."""
14
+ check_parser = subparsers.add_parser("ci-check", help="CI check management.")
15
+ check_sub = check_parser.add_subparsers(dest="ci_check_command", required=True)
16
+ check_sub.add_parser("init", help="Initialize checks directory.")
17
+ check_sub.add_parser("list", help="List discovered checks.")
18
+ check_run = check_sub.add_parser("run", help="Run all checks.")
19
+ check_run.add_argument("--file", help="Run on specific file only.")
20
+ check_run.add_argument("--json", action="store_true")
21
+ check_create = check_sub.add_parser("create", help="Create a new check template.")
22
+ check_create.add_argument("name", help="Check name.")
23
+ check_create.add_argument("--description", default="", help="Check description.")
24
+
25
+
26
+ def handle_ci_check(args: Any) -> None:
27
+ """Handle ci-check commands."""
28
+ command = args.ci_check_command
29
+ name = getattr(args, "name", None)
30
+ file = getattr(args, "file", None)
31
+ json_output = getattr(args, "json", False)
32
+
33
+ runner = CheckRunner()
34
+
35
+ if command == "init":
36
+ path = runner.init_checks_directory()
37
+ console.success(f"Initialized checks directory: {path}")
38
+ elif command == "list":
39
+ checks = runner.discover_checks()
40
+ if json_output:
41
+ data = [
42
+ {"name": c.name, "description": c.description, "severity": c.severity.value}
43
+ for c in checks
44
+ ]
45
+ print(json.dumps(data, indent=2))
46
+ else:
47
+ console.header("CI Checks")
48
+ if not checks:
49
+ console.dim("No checks found. Run 'opencontext ci-check init'")
50
+ else:
51
+ console.table(
52
+ "Discovered Checks",
53
+ ["Name", "Severity", "Description"],
54
+ [[c.name, c.severity.value, c.description] for c in checks],
55
+ )
56
+ elif command == "run":
57
+ files = [file] if file else None
58
+ with console.progress("Running checks...") as progress:
59
+ task = progress.add_task("Running checks...", total=None)
60
+ results = runner.run_all_checks(files)
61
+ progress.update(task, completed=True)
62
+ report = runner.generate_report(results)
63
+ if json_output:
64
+ print(json.dumps(report, indent=2))
65
+ else:
66
+ _display_check_report(report)
67
+ elif command == "create" and name:
68
+ template = runner.create_check_template(name, "Custom check")
69
+ console.print(template)
70
+ else:
71
+ console.error(f"Unknown ci-check command: {command}")
72
+
73
+
74
+ def _display_check_report(report: dict[str, Any]) -> None:
75
+ """Display a formatted check report."""
76
+ summary = report.get("summary", {})
77
+ total = summary.get("total_checks", 0)
78
+ passed = summary.get("passed", 0)
79
+ failed = summary.get("failed", 0)
80
+ warnings = summary.get("warnings", 0)
81
+ errors = summary.get("errors", 0)
82
+ success = summary.get("success", False)
83
+
84
+ console.header("Check Report")
85
+
86
+ if success:
87
+ console.success(f"All {total} checks passed")
88
+ else:
89
+ console.error(f"{failed}/{total} checks failed")
90
+
91
+ console.table(
92
+ "Summary",
93
+ ["Metric", "Count"],
94
+ [
95
+ ["Total", str(total)],
96
+ ["Passed", str(passed)],
97
+ ["Failed", str(failed)],
98
+ ["Warnings", str(warnings)],
99
+ ["Errors", str(errors)],
100
+ ],
101
+ )
102
+
103
+ # Show failed results
104
+ failed_results = [r for r in report.get("results", []) if r["status"] != "passed"]
105
+ if failed_results:
106
+ console.section("Failed Checks")
107
+ for r in failed_results:
108
+ severity_color = "#FF6F91" if r["severity"] in ("error", "critical") else "#FFC75F"
109
+ msg = f" [bold {severity_color}]{r['severity'].upper()}[/]"
110
+ msg += f" {r['check']}: {r['message']}"
111
+ console.print(msg)
112
+ if r.get("file"):
113
+ console.print(f" [dim]File: {r['file']}:{r.get('line', 'N/A')}[/]")
114
+ if r.get("suggestion"):
115
+ console.print(f" [dim]Suggestion: {r['suggestion']}[/]")
@@ -0,0 +1,242 @@
1
+ """Configuration management CLI commands.
2
+
3
+ Provides wizard, show, reset, reconfigure, backup,
4
+ restore, and cleanup subcommands for managing
5
+ OpenContext user preferences.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from typing import Any
13
+
14
+ from opencontext_core.state import ConfigBackupManager
15
+ from opencontext_core.user_prefs import UserConfigStore
16
+ from opencontext_core.wizard import (
17
+ reconfigure,
18
+ reset_config,
19
+ run_wizard,
20
+ show_config,
21
+ )
22
+
23
+
24
+ def add_config_parser(subparsers: Any) -> None:
25
+ """Add config command parsers."""
26
+
27
+ config_parser = subparsers.add_parser("config", help="Manage OpenContext configuration.")
28
+ config_sub = config_parser.add_subparsers(dest="config_command", required=True)
29
+
30
+ # Wizard
31
+ wizard_parser = config_sub.add_parser("wizard", help="Run configuration wizard.")
32
+ wizard_parser.add_argument(
33
+ "--non-interactive", action="store_true", help="Use defaults without prompts."
34
+ )
35
+
36
+ # Show
37
+ config_sub.add_parser("show", help="Display current configuration.")
38
+
39
+ # Reset
40
+ config_sub.add_parser("reset", help="Reset to factory defaults.")
41
+
42
+ # Reconfigure section
43
+ reconf_parser = config_sub.add_parser("reconfigure", help="Reconfigure a specific section.")
44
+ reconf_parser.add_argument(
45
+ "section",
46
+ choices=["security", "features", "tokens", "agents", "plugins"],
47
+ help="Section to reconfigure.",
48
+ )
49
+
50
+ # Set individual values
51
+ set_parser = config_sub.add_parser("set", help="Set a configuration value.")
52
+ set_parser.add_argument("key", help="Configuration key (dot notation).")
53
+ set_parser.add_argument("value", help="Value to set.")
54
+
55
+ # Get individual values
56
+ get_parser = config_sub.add_parser("get", help="Get a configuration value.")
57
+ get_parser.add_argument("key", help="Configuration key (dot notation).")
58
+
59
+ # Backup
60
+ config_sub.add_parser("backup", help="Create a manual backup of configuration.")
61
+
62
+ # List backups
63
+ config_sub.add_parser("backups", help="List saved configuration backups.")
64
+
65
+ # Restore
66
+ restore_parser = config_sub.add_parser("restore", help="Restore configuration from a backup.")
67
+ restore_parser.add_argument("id", help="Backup ID to restore.")
68
+
69
+ # Cleanup old backups
70
+ cleanup_parser = config_sub.add_parser("cleanup", help="Remove old backups.")
71
+ cleanup_parser.add_argument(
72
+ "--keep-days", type=int, default=30, help="Keep backups newer than this many days."
73
+ )
74
+
75
+
76
+ def handle_config(args: Any) -> None:
77
+ """Handle config commands."""
78
+
79
+ command = args.config_command
80
+
81
+ if command == "wizard":
82
+ run_wizard(non_interactive=getattr(args, "non_interactive", False))
83
+ elif command == "show":
84
+ show_config()
85
+ elif command == "reset":
86
+ reset_config()
87
+ elif command == "reconfigure":
88
+ reconfigure(args.section)
89
+ elif command == "set":
90
+ _config_set(args.key, args.value)
91
+ elif command == "get":
92
+ _config_get(args.key)
93
+ elif command == "backup":
94
+ _config_backup()
95
+ elif command == "backups":
96
+ _config_backups()
97
+ elif command == "restore":
98
+ _config_restore(args.id)
99
+ elif command == "cleanup":
100
+ _config_cleanup(args.keep_days)
101
+
102
+
103
+ def _config_set(key: str, value: str) -> None:
104
+ """Set a config value by key."""
105
+
106
+ store = UserConfigStore()
107
+ prefs = store.load()
108
+
109
+ # Simple key-value mapping
110
+ key_map: dict[str, tuple[str, type]] = {
111
+ "security_mode": ("security_mode", str),
112
+ "token_budget": ("default_token_budget", int),
113
+ "max_input_tokens": ("max_input_tokens", int),
114
+ "check_updates": ("check_updates", bool),
115
+ "auto_optimize": ("learning_auto_optimize", bool),
116
+ }
117
+
118
+ if key in key_map:
119
+ attr_name, attr_type = key_map[key]
120
+ if attr_type is bool:
121
+ parsed = value.lower() in ("true", "1", "yes", "on")
122
+ elif attr_type is int:
123
+ parsed = int(value)
124
+ else:
125
+ parsed = value
126
+ setattr(prefs, attr_name, parsed)
127
+ store.save(prefs)
128
+ print(f"Set {key} = {parsed}")
129
+ else:
130
+ print(f"Unknown key: {key}")
131
+ print(f"Available: {', '.join(key_map.keys())}")
132
+
133
+
134
+ def _config_get(key: str) -> None:
135
+ """Get a config value by key."""
136
+
137
+ store = UserConfigStore()
138
+ prefs = store.load()
139
+
140
+ key_map = {
141
+ "security_mode": prefs.security_mode,
142
+ "token_budget": prefs.default_token_budget,
143
+ "max_input_tokens": prefs.max_input_tokens,
144
+ "check_updates": prefs.check_updates,
145
+ "auto_optimize": prefs.learning_auto_optimize,
146
+ "first_run": prefs.first_run,
147
+ }
148
+
149
+ if key in key_map:
150
+ print(f"{key} = {key_map[key]}")
151
+ else:
152
+ print(f"Unknown key: {key}")
153
+
154
+
155
+ def _config_backup() -> None:
156
+ """Create a manual backup."""
157
+
158
+ backup_id = ConfigBackupManager.create_backup(description="manual")
159
+ print(f" ✓ Backup created: {backup_id}")
160
+ print(f" Location: {ConfigBackupManager.BACKUP_DIR / backup_id}")
161
+
162
+
163
+ def _config_backups() -> None:
164
+ """List backups."""
165
+
166
+ backups = ConfigBackupManager.list_backups()
167
+ if not backups:
168
+ print(" No backups found.")
169
+ print(f" Backup directory: {ConfigBackupManager.BACKUP_DIR}")
170
+ return
171
+
172
+ print()
173
+ print(f" {'Backup ID':<30} {'Timestamp':<25} {'Description':<20} {'Files'}")
174
+ print(f" {'─' * 30} {'─' * 25} {'─' * 20} {'─' * 20}")
175
+ for b in backups:
176
+ files_str = ", ".join(b.files) if b.files else "—"
177
+ print(f" {b.id:<30} {b.timestamp:<25} {b.description:<20} {files_str}")
178
+ print(f"\n {len(backups)} backup(s) available")
179
+ print("\n Restore: opencontext config restore <id>")
180
+
181
+
182
+ def _config_restore(backup_id: str) -> None:
183
+ """Restore from a backup."""
184
+
185
+ if ConfigBackupManager.restore_backup(backup_id):
186
+ print(f" ✓ Restored from backup: {backup_id}")
187
+ else:
188
+ print(f" ✗ Backup not found: {backup_id}")
189
+ print(" List available: opencontext config backups")
190
+ sys.exit(1)
191
+
192
+
193
+ def _config_cleanup(keep_days: int) -> None:
194
+ """Clean up old backups beyond keep_days."""
195
+
196
+ from datetime import datetime, timedelta
197
+
198
+ backups = ConfigBackupManager.list_backups()
199
+ cutoff = datetime.now() - timedelta(days=keep_days)
200
+ removed = 0
201
+
202
+ for b in backups:
203
+ try:
204
+ ts = datetime.strptime(b.timestamp, "%Y%m%dT%H%M%S")
205
+ if ts < cutoff:
206
+ backup_dir = ConfigBackupManager.BACKUP_DIR / b.id
207
+ if backup_dir.exists():
208
+ import shutil
209
+
210
+ shutil.rmtree(backup_dir)
211
+ removed += 1
212
+ except (ValueError, OSError):
213
+ continue
214
+
215
+ # Rebuild index
216
+ [b for b in backups if b.id not in [r.id for r in ConfigBackupManager.list_backups()]]
217
+ # Actually let's rebuild from disk
218
+ index = []
219
+ for entry_dir in sorted(ConfigBackupManager.BACKUP_DIR.iterdir()):
220
+ if entry_dir.is_dir() and entry_dir.name.startswith("backup-"):
221
+ try:
222
+ ts = entry_dir.name.replace("backup-", "")
223
+ desc = "auto-pre-change" # rough default
224
+ files = []
225
+ for f in entry_dir.iterdir():
226
+ if f.is_file():
227
+ files.append(f.name)
228
+ index.append(
229
+ {
230
+ "id": entry_dir.name,
231
+ "timestamp": ts,
232
+ "description": desc,
233
+ "files": files,
234
+ }
235
+ )
236
+ except OSError:
237
+ continue
238
+
239
+ ConfigBackupManager.INDEX_FILE.write_text(json.dumps(index, indent=2), encoding="utf-8")
240
+
241
+ print(f" ✓ Removed {removed} backup(s) older than {keep_days} days")
242
+ print(f" {len(index)} backup(s) remaining")
@@ -0,0 +1,139 @@
1
+ """Git context CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from opencontext_core.dx.console_styles import console
9
+ from opencontext_core.indexing.git_context import GitContextProvider
10
+
11
+
12
+ def add_git_parser(subparsers: Any) -> None:
13
+ """Add git command parsers."""
14
+ git_parser = subparsers.add_parser("git", help="Git context and history.")
15
+ git_sub = git_parser.add_subparsers(dest="git_command", required=True)
16
+ git_sub.add_parser("status", help="Show git repository stats.")
17
+ git_history = git_sub.add_parser("history", help="Show git history for a file.")
18
+ git_history.add_argument("file", help="File path.")
19
+ git_history.add_argument("--json", action="store_true")
20
+ git_recent = git_sub.add_parser("recent", help="Show recent changes.")
21
+ git_recent.add_argument("--days", type=int, default=7)
22
+ git_recent.add_argument("--max-commits", type=int, default=20)
23
+ git_recent.add_argument("--json", action="store_true")
24
+ git_blame = git_sub.add_parser("blame", help="Show blame for file lines.")
25
+ git_blame.add_argument("file", help="File path.")
26
+ git_blame.add_argument("--start", type=int, default=1)
27
+ git_blame.add_argument("--end", type=int, default=10)
28
+ git_blame.add_argument("--json", action="store_true")
29
+
30
+
31
+ def handle_git(args: Any) -> None:
32
+ """Handle git commands."""
33
+ command = args.git_command
34
+ file = getattr(args, "file", None)
35
+ start = getattr(args, "start", 1)
36
+ end = getattr(args, "end", 10)
37
+ days = getattr(args, "days", 7)
38
+ max_commits = getattr(args, "max_commits", 20)
39
+ json_output = getattr(args, "json", False)
40
+
41
+ provider = GitContextProvider()
42
+ if not provider.available:
43
+ console.error("Not a git repository")
44
+ return
45
+
46
+ if command == "status":
47
+ stats = provider.get_repo_stats()
48
+ if json_output:
49
+ print(json.dumps(stats, indent=2))
50
+ else:
51
+ console.header("Git Repository Stats")
52
+ console.print(_format_git_status(stats))
53
+ elif command == "history" and file:
54
+ info = provider.get_file_info(file)
55
+ if info:
56
+ if json_output:
57
+ data = {
58
+ "path": info.path,
59
+ "last_modified": info.last_modified.isoformat() if info.last_modified else None,
60
+ "last_author": info.last_author,
61
+ "commit_count": info.commit_count,
62
+ "lines_added": info.lines_added,
63
+ "lines_removed": info.lines_removed,
64
+ "top_authors": info.top_authors,
65
+ }
66
+ print(json.dumps(data, indent=2))
67
+ else:
68
+ console.header(f"Git History: {file}")
69
+ console.print(_format_git_history(info))
70
+ else:
71
+ console.error(f"Could not get history for {file}")
72
+ elif command == "recent":
73
+ diffs = provider.get_recent_changes(days=days, max_commits=max_commits)
74
+ if json_output:
75
+ data = [
76
+ {
77
+ "commit_hash": d.commit_hash,
78
+ "author": d.author,
79
+ "date": d.date.isoformat(),
80
+ "message": d.message,
81
+ "files_changed": d.files_changed,
82
+ }
83
+ for d in diffs
84
+ ]
85
+ print(json.dumps(data, indent=2))
86
+ else:
87
+ console.header(f"Recent Changes (last {days} days)")
88
+ console.print(_format_git_recent(diffs))
89
+ elif command == "blame" and file:
90
+ lines = provider.get_blame_for_symbol(file, start, end)
91
+ if json_output:
92
+ print(json.dumps(lines, indent=2))
93
+ else:
94
+ console.header(f"Blame: {file} (lines {start}-{end})")
95
+ console.print(_format_git_blame(lines))
96
+ else:
97
+ console.error(f"Unknown git command: {command}")
98
+
99
+
100
+ def _format_git_status(stats: dict[str, Any]) -> str:
101
+ if not stats.get("available"):
102
+ return "Not a git repository."
103
+ lines = [
104
+ f"Commits: {stats.get('total_commits', 'N/A')}",
105
+ f"Contributors: {stats.get('contributors', 'N/A')}",
106
+ f"Branches: {stats.get('branches', 'N/A')}",
107
+ ]
108
+ return "\n".join(lines)
109
+
110
+
111
+ def _format_git_history(info: Any) -> str:
112
+ lines = [
113
+ f"File: {info.path}",
114
+ f"Commits: {info.commit_count}",
115
+ f"Last author: {info.last_author or 'N/A'}",
116
+ f"Last modified: {info.last_modified.isoformat() if info.last_modified else 'N/A'}",
117
+ f"Lines added/removed: +{info.lines_added}/-{info.lines_removed}",
118
+ f"Top authors: {', '.join(info.top_authors) if info.top_authors else 'N/A'}",
119
+ ]
120
+ return "\n".join(lines)
121
+
122
+
123
+ def _format_git_recent(diffs: list[Any]) -> str:
124
+ lines = []
125
+ for d in diffs:
126
+ lines.append(f"{d.commit_hash[:8]} {d.author} {d.date.isoformat()}")
127
+ lines.append(f" {d.message}")
128
+ lines.append(f" Files: {', '.join(d.files_changed[:5])}")
129
+ lines.append("")
130
+ return "\n".join(lines)
131
+
132
+
133
+ def _format_git_blame(lines: list[dict[str, Any]]) -> str:
134
+ out = []
135
+ for line in lines:
136
+ author = line.get("author", "?")
137
+ code = line.get("code", "")
138
+ out.append(f"{author:20} {code}")
139
+ return "\n".join(out)
@@ -0,0 +1,67 @@
1
+ """Agent hints CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from opencontext_core.dx.agent_hints import AgentHintsManager
9
+ from opencontext_core.dx.console_styles import console
10
+
11
+
12
+ def add_hints_parser(subparsers: Any) -> None:
13
+ """Add hints command parsers."""
14
+ hints_parser = subparsers.add_parser("hints", help="Agent hints management.")
15
+ hints_sub = hints_parser.add_subparsers(dest="hints_command", required=True)
16
+ hints_sub.add_parser("init", help="Initialize .opencontexthints file.")
17
+ hints_sub.add_parser("show", help="Show combined hints.")
18
+ hints_sub.add_parser("validate", help="Validate hints files.")
19
+
20
+
21
+ def handle_hints(args: Any) -> None:
22
+ """Handle hints commands."""
23
+ command = args.hints_command
24
+ json_output = getattr(args, "json", False)
25
+
26
+ manager = AgentHintsManager()
27
+
28
+ if command == "init":
29
+ path = manager.init_hints_file()
30
+ if path:
31
+ console.success(f"Created {path}")
32
+ console.info("Edit the file to add your project conventions")
33
+ else:
34
+ console.warning(".opencontexthints already exists")
35
+ elif command == "show":
36
+ hints = manager.get_all_hints()
37
+ if hints:
38
+ ctx = manager.to_context_string(hints)
39
+ if json_output:
40
+ print(json.dumps({"hints": ctx}, indent=2))
41
+ else:
42
+ console.header("Agent Hints")
43
+ console.panel(ctx, title=hints.project_name or "Project Hints")
44
+ else:
45
+ console.warning("No hints found. Run 'opencontext hints init' to create them.")
46
+ elif command == "validate":
47
+ files = manager.discover_hints()
48
+ valid = []
49
+ invalid = []
50
+ for f in files:
51
+ parsed = manager.parse_hints_file(f)
52
+ if parsed:
53
+ valid.append(str(f))
54
+ else:
55
+ invalid.append(str(f))
56
+ if json_output:
57
+ print(json.dumps({"valid": valid, "invalid": invalid, "total": len(files)}, indent=2))
58
+ else:
59
+ console.header("Hints Validation")
60
+ console.success(f"Valid: {len(valid)}")
61
+ if invalid:
62
+ console.error(f"Invalid: {len(invalid)}")
63
+ for f in invalid:
64
+ console.print(f" [dim]✗ {f}[/]")
65
+ console.info(f"Total files checked: {len(files)}")
66
+ else:
67
+ console.error(f"Unknown hints command: {command}")