opencontext-cli 0.2.1b0__tar.gz → 0.4.0b0__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 (27) hide show
  1. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/PKG-INFO +1 -1
  2. opencontext_cli-0.4.0b0/opencontext_cli/__main__.py +5 -0
  3. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/ci_check_cmd.py +77 -1
  4. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/config_cmd.py +128 -38
  5. opencontext_cli-0.4.0b0/opencontext_cli/commands/menu_cmd.py +442 -0
  6. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/plugin_cmd.py +163 -1
  7. opencontext_cli-0.4.0b0/opencontext_cli/commands/setup_cmd.py +582 -0
  8. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/update_cmd.py +21 -10
  9. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/verify_cmd.py +2 -2
  10. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/main.py +933 -766
  11. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/PKG-INFO +1 -1
  12. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/SOURCES.txt +2 -0
  13. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/pyproject.toml +1 -1
  14. opencontext_cli-0.2.1b0/opencontext_cli/commands/setup_cmd.py +0 -346
  15. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/LICENSE +0 -0
  16. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/README.md +0 -0
  17. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/__init__.py +0 -0
  18. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/__init__.py +0 -0
  19. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/git_cmd.py +0 -0
  20. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/hints_cmd.py +0 -0
  21. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/kg_cmd.py +0 -0
  22. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/sync_cmd.py +0 -0
  23. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/dependency_links.txt +0 -0
  24. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/entry_points.txt +0 -0
  25. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/requires.txt +0 -0
  26. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/top_level.txt +0 -0
  27. {opencontext_cli-0.2.1b0 → opencontext_cli-0.4.0b0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencontext-cli
3
- Version: 0.2.1b0
3
+ Version: 0.4.0b0
4
4
  Summary: CLI adapter for OpenContext Runtime
5
5
  Author: OpenContext Runtime maintainers
6
6
  License-Expression: MIT
@@ -0,0 +1,5 @@
1
+ """CLI entry point for `python -m opencontext_cli`."""
2
+
3
+ from opencontext_cli.main import main
4
+
5
+ main()
@@ -3,17 +3,64 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ from pathlib import Path
6
7
  from typing import Any
7
8
 
8
9
  from opencontext_core.dx.console_styles import console
9
10
  from opencontext_core.quality.ci_checks import CheckRunner
10
11
 
12
+ CONTEXTBENCH_WORKFLOW = """\
13
+ # OpenContext ContextBench CI
14
+ # Auto-generated by `opencontext ci-check init`
15
+ name: OpenContext ContextBench
16
+
17
+ on:
18
+ push:
19
+ branches: [main, master]
20
+ pull_request:
21
+ branches: [main, master]
22
+
23
+ jobs:
24
+ contextbench:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - name: Install OpenContext
29
+ run: pip install opencontext-cli
30
+ - name: Initialize checks
31
+ run: opencontext ci-check init
32
+ - name: Run ContextBench checks
33
+ run: opencontext ci-check run --json > contextbench-report.json
34
+ - name: Upload report
35
+ uses: actions/upload-artifact@v4
36
+ with:
37
+ name: contextbench-report
38
+ path: contextbench-report.json
39
+ - name: Fail on errors
40
+ run: |
41
+ python3 -c "
42
+ import json
43
+ with open('contextbench-report.json') as f:
44
+ report = json.load(f)
45
+ summary = report.get('summary', {})
46
+ if summary.get('failed', 0) > 0:
47
+ print('❌ ContextBench checks failed')
48
+ exit(1)
49
+ print('✅ All ContextBench checks passed')
50
+ "
51
+ """
52
+
11
53
 
12
54
  def add_ci_check_parser(subparsers: Any) -> None:
13
55
  """Add ci-check command parsers."""
14
56
  check_parser = subparsers.add_parser("ci-check", help="CI check management.")
15
57
  check_sub = check_parser.add_subparsers(dest="ci_check_command", required=True)
16
- check_sub.add_parser("init", help="Initialize checks directory.")
58
+ check_init = check_sub.add_parser(
59
+ "init", help="Initialize checks directory and ContextBench workflow."
60
+ )
61
+ check_init.add_argument(
62
+ "--no-workflow", action="store_true", help="Skip GitHub Actions workflow generation."
63
+ )
17
64
  check_sub.add_parser("list", help="List discovered checks.")
18
65
  check_run = check_sub.add_parser("run", help="Run all checks.")
19
66
  check_run.add_argument("--file", help="Run on specific file only.")
@@ -21,6 +68,10 @@ def add_ci_check_parser(subparsers: Any) -> None:
21
68
  check_create = check_sub.add_parser("create", help="Create a new check template.")
22
69
  check_create.add_argument("name", help="Check name.")
23
70
  check_create.add_argument("--description", default="", help="Check description.")
71
+ check_gh = check_sub.add_parser(
72
+ "github-actions", help="Generate ContextBench GitHub Actions workflow."
73
+ )
74
+ check_gh.add_argument("--force", action="store_true", help="Overwrite existing workflow file.")
24
75
 
25
76
 
26
77
  def handle_ci_check(args: Any) -> None:
@@ -35,6 +86,18 @@ def handle_ci_check(args: Any) -> None:
35
86
  if command == "init":
36
87
  path = runner.init_checks_directory()
37
88
  console.success(f"Initialized checks directory: {path}")
89
+ skip_workflow = getattr(args, "no_workflow", False)
90
+ if not skip_workflow:
91
+ workflow_path = _generate_contextbench_workflow()
92
+ console.success(f"Generated ContextBench workflow: {workflow_path}")
93
+ elif command == "github-actions":
94
+ force = getattr(args, "force", False)
95
+ workflow_path = Path(".github/workflows/opencontext-contextbench.yml")
96
+ if workflow_path.exists() and not force:
97
+ console.warning(f"Workflow already exists: {workflow_path}. Use --force to overwrite.")
98
+ return
99
+ _write_contextbench_workflow(workflow_path)
100
+ console.success(f"Generated ContextBench workflow: {workflow_path}")
38
101
  elif command == "list":
39
102
  checks = runner.discover_checks()
40
103
  if json_output:
@@ -113,3 +176,16 @@ def _display_check_report(report: dict[str, Any]) -> None:
113
176
  console.print(f" [dim]File: {r['file']}:{r.get('line', 'N/A')}[/]")
114
177
  if r.get("suggestion"):
115
178
  console.print(f" [dim]Suggestion: {r['suggestion']}[/]")
179
+
180
+
181
+ def _generate_contextbench_workflow() -> Path:
182
+ """Generate the ContextBench GitHub Actions workflow as part of init."""
183
+ workflow_path = Path(".github/workflows/opencontext-contextbench.yml")
184
+ _write_contextbench_workflow(workflow_path)
185
+ return workflow_path
186
+
187
+
188
+ def _write_contextbench_workflow(workflow_path: Path) -> None:
189
+ """Write the ContextBench workflow file."""
190
+ workflow_path.parent.mkdir(parents=True, exist_ok=True)
191
+ workflow_path.write_text(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,10 +75,26 @@ 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
- run_wizard(non_interactive=getattr(args, "non_interactive", False))
91
+ use_tui = not getattr(args, "non_interactive", False)
92
+ if use_tui:
93
+ from opencontext_core.wizard import run_wizard_menu
94
+
95
+ run_wizard_menu()
96
+ else:
97
+ run_wizard(non_interactive=True)
83
98
  elif command == "show":
84
99
  show_config()
85
100
  elif command == "reset":
@@ -100,56 +115,131 @@ def handle_config(args: Any) -> None:
100
115
  _config_cleanup(args.keep_days)
101
116
 
102
117
 
118
+ # ── Dot-notation config paths ──────────────────────────────────────────────
119
+
120
+ # Schema of configurable paths: "path" -> (type, description)
121
+ CONFIG_PATHS: dict[str, tuple[type, str]] = {
122
+ # Flat keys
123
+ "security_mode": (str, "Security mode: private_project, enterprise, or air-gapped"),
124
+ "default_token_budget": (int, "Default token budget per operation"),
125
+ "max_input_tokens": (int, "Maximum input tokens"),
126
+ "reserve_output_tokens": (int, "Reserved output tokens"),
127
+ "check_updates": (bool, "Check for updates automatically"),
128
+ "auto_optimize": (bool, "Auto-optimize token budgets based on usage"),
129
+ "first_run": (bool, "Whether this is the first run"),
130
+ "default_provider": (str, "Default LLM provider"),
131
+ "default_model": (str, "Default LLM model"),
132
+ # Nested: features.*
133
+ "features.knowledge_graph": (bool, "Knowledge Graph (code indexing & search)"),
134
+ "features.call_graph": (bool, "Call Graph (function call analysis)"),
135
+ "features.learning_system": (bool, "Learning System (auto-optimize)"),
136
+ "features.governance": (bool, "Governance (audit trails & policies)"),
137
+ "features.mcp_server": (bool, "MCP Server (agent integration)"),
138
+ "features.git_integration": (bool, "Git Integration"),
139
+ "features.embeddings": (bool, "Embeddings (semantic search)"),
140
+ "features.semantic_search": (bool, "Semantic Search"),
141
+ # Nested: sdd.*
142
+ "sdd.tdd_mode": (str, "TDD mode: ask, strict, or off"),
143
+ "sdd.sdd_model_profile": (str, "SDD model profile: default, cheap, hybrid, premium"),
144
+ "sdd.orchestrator_profile": (
145
+ str,
146
+ "Orchestrator profile: solo-compact, multi-phase, subagent-native",
147
+ ),
148
+ # Nested: agents.*
149
+ "agents.default_client": (str, "Default agent client"),
150
+ "agents.active_clients": (list, "Active agent clients (comma-separated)"),
151
+ }
152
+
153
+
154
+ def _resolve_config_path(prefs: Any, dotted: str) -> tuple[Any, str] | None:
155
+ """Resolve a dotted path to (parent_object, attr_name) or None if invalid.
156
+
157
+ Example: "features.knowledge_graph" -> (prefs.features, "knowledge_graph")
158
+ """
159
+ parts = dotted.split(".")
160
+ obj = prefs
161
+ for _i, part in enumerate(parts[:-1]):
162
+ if hasattr(obj, part):
163
+ obj = getattr(obj, part)
164
+ else:
165
+ return None
166
+ return (obj, parts[-1])
167
+
168
+
169
+ def _get_all_config_paths() -> list[str]:
170
+ """Return all available config paths sorted."""
171
+ return sorted(CONFIG_PATHS.keys())
172
+
173
+
174
+ def _coerce_value(value: str, target_type: type) -> object:
175
+ """Coerce a string value to the target type."""
176
+ if target_type is bool:
177
+ return value.lower() in ("true", "1", "yes", "on")
178
+ elif target_type is int:
179
+ return int(value)
180
+ elif target_type is list:
181
+ import json
182
+
183
+ try:
184
+ parsed = json.loads(value)
185
+ if isinstance(parsed, list):
186
+ return parsed
187
+ except (json.JSONDecodeError, TypeError):
188
+ pass
189
+ # Fallback: comma-separated
190
+ return [item.strip() for item in value.split(",") if item.strip()]
191
+ else:
192
+ return value
193
+
194
+
103
195
  def _config_set(key: str, value: str) -> None:
104
- """Set a config value by key."""
196
+ """Set a config value using dot notation."""
105
197
 
106
198
  store = UserConfigStore()
107
199
  prefs = store.load()
108
200
 
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}")
201
+ if key in CONFIG_PATHS:
202
+ _target_type, _description = CONFIG_PATHS[key]
203
+ resolved = _resolve_config_path(prefs, key)
204
+ if resolved is None:
205
+ print(f"Error: Cannot resolve path '{key}'")
206
+ return
207
+ parent, attr = resolved
208
+ try:
209
+ parsed = _coerce_value(value, _target_type)
210
+ setattr(parent, attr, parsed)
211
+ store.save(prefs)
212
+ print(f"Set {key} = {parsed}")
213
+ except (ValueError, TypeError) as exc:
214
+ print(f"Error: Cannot set '{key}' to '{value}': {exc}")
215
+ print(f"Expected type: {_target_type.__name__}")
129
216
  else:
130
217
  print(f"Unknown key: {key}")
131
- print(f"Available: {', '.join(key_map.keys())}")
218
+ print(f"Available paths ({len(CONFIG_PATHS)}):")
219
+ for path, (typ, desc) in sorted(CONFIG_PATHS.items()):
220
+ print(f" {path} ({typ.__name__}) {desc}")
132
221
 
133
222
 
134
223
  def _config_get(key: str) -> None:
135
- """Get a config value by key."""
224
+ """Get a config value by dot-notation key."""
136
225
 
137
226
  store = UserConfigStore()
138
227
  prefs = store.load()
139
228
 
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]}")
229
+ if key in CONFIG_PATHS:
230
+ _target_type, _description = CONFIG_PATHS[key]
231
+ resolved = _resolve_config_path(prefs, key)
232
+ if resolved is None:
233
+ print(f"Error: Cannot resolve path '{key}'")
234
+ return
235
+ parent, attr = resolved
236
+ value = getattr(parent, attr, "<not set>")
237
+ print(f"{key} = {value}")
151
238
  else:
152
239
  print(f"Unknown key: {key}")
240
+ print(f"Available paths ({len(CONFIG_PATHS)}):")
241
+ for path, (typ, desc) in sorted(CONFIG_PATHS.items()):
242
+ print(f" {path} ({typ.__name__}) {desc}")
153
243
 
154
244
 
155
245
  def _config_backup() -> None: