k-cli-for-devs 1.0.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 (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
k_cli/cli.py ADDED
@@ -0,0 +1,3297 @@
1
+ """
2
+ cli.py - Command Line Interface & TUI Entrypoint for K-CLI (Project Bankai Engine)
3
+
4
+ Features:
5
+ 1. Live token streaming with dynamic syntax highlighting.
6
+ 2. Real-time Status Bar (Active Model, Git Branch, Active Persona, RAM, Tokens).
7
+ 3. Interactive slash commands (/model, /persona, /diff, /rollback, /help, /docs, /clear, /test).
8
+ 4. Side-by-side and inline surgical diff visualization.
9
+ """
10
+
11
+ import warnings
12
+ warnings.filterwarnings("ignore")
13
+
14
+ import difflib
15
+ import functools
16
+ import json
17
+ import os
18
+ import shlex
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # Ensure project root is in sys.path for direct CLI script execution
23
+ _pkg_root = str(Path(__file__).resolve().parent.parent)
24
+ if _pkg_root not in sys.path:
25
+ sys.path.insert(0, _pkg_root)
26
+ _module_dir = str(Path(__file__).resolve().parent)
27
+ if _module_dir not in sys.path:
28
+ sys.path.insert(0, _module_dir)
29
+
30
+ import psutil
31
+ from typing import List, Optional
32
+
33
+ import typer
34
+ from rich.console import Console
35
+ from rich.live import Live
36
+ from rich.markdown import Markdown
37
+ from rich.panel import Panel
38
+ from rich.syntax import Syntax
39
+ from rich.table import Table
40
+ from rich.text import Text
41
+
42
+ try:
43
+ from k_cli.core.llm_driver import LLMDriver
44
+ from k_cli.agents.orchestrator import Orchestrator, Persona
45
+ from k_cli.git.verifier import Verifier
46
+ from k_cli.tools.doc_retriever import DocRetriever
47
+ from k_cli.git.repo_map import RepoMap
48
+ from k_cli.core.session import SessionManager
49
+ from k_cli.core.model_manager import ModelManager, ModelPullResult, MODEL_CATALOG
50
+ from k_cli.agents.persona import DomainPersona, PersonaProfile, PersonaRegistry
51
+ from k_cli.agents.subagents import (
52
+ SubagentDispatcher,
53
+ SubagentVisualizer,
54
+ SubagentTask,
55
+ SubagentRole,
56
+ SubagentRunResult,
57
+ execute_subagents,
58
+ )
59
+ from k_cli.tui.diff_viewer import DiffVisualizer
60
+ from k_cli.core.credentials import CredentialsManager, DevPreferencesManager
61
+ from k_cli.core.sdk import create_plan
62
+ from k_cli.git.git_guard import GitGuard
63
+ from k_cli.tools.audit import run_audit
64
+ from k_cli.core.prompting import enhance_prompt, resolve_profile
65
+ from k_cli.tools.security import scan_workspace
66
+ from k_cli.tools.feature import inspect_feature
67
+ from k_cli.tools.rules import load_project_rules
68
+ from k_cli.tui.tui import (
69
+ StatusBar,
70
+ LiveStreamRenderer,
71
+ InteractiveShell,
72
+ SlashCommandHandler,
73
+ MODEL_PRESETS,
74
+ get_persona_style,
75
+ )
76
+ from k_cli.tools.mcp_client import (
77
+ MCPManager,
78
+ MCPClient,
79
+ MCPServerConfig,
80
+ mcp_list_servers,
81
+ mcp_add_server,
82
+ mcp_remove_server,
83
+ mcp_test_connection,
84
+ )
85
+ from k_cli.git.conflict_resolver import (
86
+ ConflictResolver,
87
+ ConflictBlock,
88
+ ConflictResolution,
89
+ FileResolutionResult,
90
+ ConflictSummary,
91
+ )
92
+ from k_cli.github.github_client import (
93
+ GitHubClient,
94
+ MockGitHubClient,
95
+ PRLifecycleManager,
96
+ PullRequest,
97
+ PRReviewResult,
98
+ PRFixResult,
99
+ CIStatus,
100
+ )
101
+ from k_cli.github.dedup_engine import (
102
+ DedupEngine,
103
+ DedupMatch,
104
+ CommitRecord,
105
+ SymbolRecord,
106
+ )
107
+ from k_cli.git.smart_git import (
108
+ SmartGitEngine,
109
+ SmartCommitProposal,
110
+ PRDescriptionProposal,
111
+ AtomicCommitGroup,
112
+ FileChangeAnalysis,
113
+ CommitType,
114
+ )
115
+ from k_cli.tools.security_healer import (
116
+ SecurityHealer,
117
+ SecurityScanReport,
118
+ VulnerabilityFinding,
119
+ VulnerabilityHealResult,
120
+ VulnerabilitySeverity,
121
+ VulnerabilityType,
122
+ )
123
+ from k_cli.core.models_hub import (
124
+ ModelHub,
125
+ ModelSpec,
126
+ ModelProvider,
127
+ ModelBenchmarkResult,
128
+ )
129
+ from k_cli.github.github_engine import (
130
+ GitHubEngine,
131
+ GitHubIssue,
132
+ GitHubRelease,
133
+ WorkflowRun,
134
+ IssueSolveResult,
135
+ )
136
+ from k_cli.github.local_hub import LocalGitHubHub, LocalHubSummary, LocalCommit
137
+ from k_cli.github.trending import TrendingEngine, TrendingRepo
138
+ except (ModuleNotFoundError, ImportError):
139
+ from k_cli.core.llm_driver import LLMDriver
140
+ from k_cli.agents.orchestrator import Orchestrator, Persona
141
+ from verifier import Verifier
142
+ from doc_retriever import DocRetriever
143
+ from repo_map import RepoMap
144
+ from session import SessionManager
145
+ try:
146
+ from model_manager import ModelManager, ModelPullResult, MODEL_CATALOG
147
+ except (ModuleNotFoundError, ImportError):
148
+ ModelManager = None # type: ignore
149
+ ModelPullResult = None # type: ignore
150
+ MODEL_CATALOG = {} # type: ignore
151
+ try:
152
+ from persona import DomainPersona, PersonaProfile, PersonaRegistry
153
+ except (ModuleNotFoundError, ImportError):
154
+ PersonaRegistry = None
155
+ from subagents import (
156
+ SubagentDispatcher,
157
+ SubagentVisualizer,
158
+ SubagentTask,
159
+ SubagentRole,
160
+ SubagentRunResult,
161
+ execute_subagents,
162
+ )
163
+ from diff_viewer import DiffVisualizer
164
+ try:
165
+ from k_cli.core.sdk import create_plan
166
+ except (ImportError, ModuleNotFoundError):
167
+ from workflow import create_plan
168
+ from git_guard import GitGuard
169
+ from k_cli.tools.audit import run_audit
170
+ from prompting import enhance_prompt, resolve_profile
171
+ from security import scan_workspace
172
+ from k_cli.tools.feature import inspect_feature
173
+ from k_cli.tools.rules import load_project_rules
174
+ from k_cli.tui.tui import (
175
+ StatusBar,
176
+ LiveStreamRenderer,
177
+ InteractiveShell,
178
+ SlashCommandHandler,
179
+ MODEL_PRESETS,
180
+ get_persona_style,
181
+ )
182
+ try:
183
+ from mcp_client import (
184
+ MCPManager,
185
+ MCPClient,
186
+ MCPServerConfig,
187
+ mcp_list_servers,
188
+ mcp_add_server,
189
+ mcp_remove_server,
190
+ mcp_test_connection,
191
+ )
192
+ except (ModuleNotFoundError, ImportError):
193
+ MCPManager = None # type: ignore
194
+ try:
195
+ from conflict_resolver import (
196
+ ConflictResolver,
197
+ ConflictBlock,
198
+ ConflictResolution,
199
+ FileResolutionResult,
200
+ ConflictSummary,
201
+ )
202
+ except (ModuleNotFoundError, ImportError):
203
+ ConflictResolver = None # type: ignore
204
+ try:
205
+ from github_client import (
206
+ GitHubClient,
207
+ MockGitHubClient,
208
+ PRLifecycleManager,
209
+ PullRequest,
210
+ PRReviewResult,
211
+ PRFixResult,
212
+ CIStatus,
213
+ )
214
+ except (ModuleNotFoundError, ImportError):
215
+ GitHubClient = None # type: ignore
216
+ PRLifecycleManager = None # type: ignore
217
+ try:
218
+ from dedup_engine import (
219
+ DedupEngine,
220
+ DedupMatch,
221
+ CommitRecord,
222
+ SymbolRecord,
223
+ )
224
+ except (ModuleNotFoundError, ImportError):
225
+ DedupEngine = None # type: ignore
226
+ try:
227
+ from k_cli.git.smart_git import (
228
+ SmartGitEngine,
229
+ SmartCommitProposal,
230
+ PRDescriptionProposal,
231
+ AtomicCommitGroup,
232
+ FileChangeAnalysis,
233
+ CommitType,
234
+ )
235
+ except (ModuleNotFoundError, ImportError):
236
+ SmartGitEngine = None # type: ignore
237
+ SmartCommitProposal = None # type: ignore
238
+ PRDescriptionProposal = None # type: ignore
239
+ try:
240
+ from security_healer import (
241
+ SecurityHealer,
242
+ SecurityScanReport,
243
+ VulnerabilityFinding,
244
+ VulnerabilityHealResult,
245
+ VulnerabilitySeverity,
246
+ VulnerabilityType,
247
+ )
248
+ except (ModuleNotFoundError, ImportError):
249
+ SecurityHealer = None # type: ignore
250
+ SecurityScanReport = None # type: ignore
251
+
252
+ app = typer.Typer(
253
+ name="k-cli",
254
+ help="K-CLI: Universal agentic AI coding workstation.",
255
+ add_completion=False,
256
+ )
257
+ console = Console()
258
+
259
+ ASCII_BANNER = r"""
260
+ [bold cyan]
261
+ ██╗ ██╗ ██████╗██╗ ██╗
262
+ ██║ ██╔╝ ██╔════╝██║ ██║
263
+ █████═╝ ██║ ██║ ██║
264
+ ██╔═██╗ ██║ ██║ ██║
265
+ ██║ ██╗ ╚██████╗███████╗██║
266
+ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝
267
+ [/bold cyan]
268
+ [bold bright_white]K-CLI AGENTIC WORKSTATION v1.0.0 | Verification-First Engine[/bold bright_white]
269
+ [dim]Commands: /keys | /conflict | /gh | /model | /security | /clear | /test | /help | /exit[/dim]
270
+ """
271
+
272
+
273
+ def print_banner():
274
+ console.print(ASCII_BANNER)
275
+
276
+
277
+ def _resolve_val(val, default):
278
+ """Safely extracts default values if Typer OptionInfo objects are passed directly."""
279
+ if hasattr(val, "default"):
280
+ return val.default
281
+ return val if val is not None else default
282
+
283
+
284
+ @functools.lru_cache(maxsize=128)
285
+ def get_persona_color(persona: str) -> str:
286
+ """Returns Rich color string corresponding to persona string or Enum."""
287
+ p_str = str(persona).upper().strip()
288
+ color_map = {
289
+ "RESEARCHER": "cyan",
290
+ "ARCHITECT": "magenta",
291
+ "CODER": "green",
292
+ "CRITIC": "yellow",
293
+ "DEBUGGER": "red",
294
+ "DEVOPS": "cyan",
295
+ "SYSTEMS": "magenta",
296
+ "SECURITY": "red",
297
+ "APPSEC": "red",
298
+ "FRONTEND": "green",
299
+ "DATABASE": "yellow",
300
+ "DEFAULT": "blue",
301
+ }
302
+ for key, color in color_map.items():
303
+ if key in p_str:
304
+ return color
305
+ return "blue"
306
+
307
+
308
+ def compute_diff(initial_code: str, final_code: str) -> str:
309
+ """Calculates unified diff text between candidate code and repaired code."""
310
+ diff_lines = list(
311
+ difflib.unified_diff(
312
+ initial_code.splitlines(keepends=True),
313
+ final_code.splitlines(keepends=True),
314
+ fromfile="candidate_code.py",
315
+ tofile="repaired_code.py",
316
+ )
317
+ )
318
+ return "".join(diff_lines)
319
+
320
+
321
+ def execute_run(
322
+ prompt: str,
323
+ language: str = "python",
324
+ model: str = "qwen2.5-coder:1.5b",
325
+ max_retries: int = 3,
326
+ save_to: Optional[Path] = None,
327
+ mock: bool = False,
328
+ show_banner: bool = True,
329
+ test_file: Optional[Path] = None,
330
+ test_code: Optional[str] = None,
331
+ persona: Optional[str] = None,
332
+ enhance: bool = False,
333
+ rules_file: Optional[Path] = None,
334
+ provider: Optional[str] = None,
335
+ base_url: Optional[str] = None,
336
+ ):
337
+ """Core execution logic for running prompts through persona state machine with live token streaming."""
338
+ language = str(_resolve_val(language, "python"))
339
+ model = str(_resolve_val(model, "qwen2.5-coder:1.5b"))
340
+ max_retries = int(_resolve_val(max_retries, 3))
341
+ mock = bool(_resolve_val(mock, False))
342
+ if not mock and ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM")):
343
+ mock = True
344
+ save_to_val = _resolve_val(save_to, None)
345
+ save_to_path = Path(save_to_val) if save_to_val else None
346
+ test_file_val = _resolve_val(test_file, None)
347
+ test_code_val = _resolve_val(test_code, None)
348
+ persona_val = _resolve_val(persona, None)
349
+
350
+ resolved_test_code = test_code_val
351
+ if test_file_val is not None:
352
+ tf_path = Path(test_file_val)
353
+ if tf_path.exists():
354
+ resolved_test_code = tf_path.read_text(encoding="utf-8")
355
+
356
+ if show_banner:
357
+ print_banner()
358
+
359
+ driver = LLMDriver(
360
+ model_name=model,
361
+ mock_mode=mock,
362
+ provider=provider,
363
+ openai_base_url=base_url,
364
+ )
365
+ verifier = Verifier()
366
+ orchestrator = Orchestrator(driver=driver, verifier=verifier, max_retries=max_retries, persona=persona_val)
367
+
368
+ initial_ram = orchestrator.get_current_ram_mb()
369
+ driver_type = "ONLINE (Ollama GGUF)" if driver.is_ollama_available() else "LOCAL (llama-cpp-python GGUF)"
370
+
371
+ if show_banner:
372
+ table = Table(title="System Environment Status", box=None)
373
+ table.add_column("Parameter", style="cyan")
374
+ table.add_column("Value", style="magenta")
375
+ table.add_row("Active Model", model)
376
+ if orchestrator.active_persona:
377
+ table.add_row("Active Persona", orchestrator.active_persona.title)
378
+ table.add_row("Target Language", language)
379
+ table.add_row("SLM Driver Engine", driver_type)
380
+ table.add_row("Initial RAM Allocation", f"{initial_ram:.2f} MB / 1024 MB")
381
+ console.print(table)
382
+ console.print()
383
+
384
+ effective_prompt = enhance_prompt(prompt, model, language) if enhance else prompt
385
+ if rules_file is not None:
386
+ try:
387
+ guidance = load_project_rules(Path.cwd(), rules_file)
388
+ except ValueError as exc:
389
+ console.print(f"[bold red]Invalid project guidance:[/bold red] {exc}")
390
+ raise typer.Exit(code=2)
391
+ if guidance:
392
+ effective_prompt = f"{effective_prompt}\n\n{guidance}"
393
+ console.print(f"[bold yellow]Agent Task:[/bold yellow] [italic]'{prompt}'[/italic]\n")
394
+ if enhance:
395
+ console.print(f"[dim]Prompt adapted for {resolve_profile(model).name}.[/dim]\n")
396
+
397
+ current_persona_name = "RESEARCHER"
398
+ current_persona_text = ""
399
+
400
+ def make_live_panel() -> Panel:
401
+ ram_mb = orchestrator.get_current_ram_mb()
402
+ color = get_persona_color(current_persona_name)
403
+ title = f"[{color}]Active Persona: [{current_persona_name}][/{color}] | RSS RAM: {ram_mb:.2f} MB / 1024 MB"
404
+
405
+ if not current_persona_text:
406
+ content = Text(f"Initializing [{current_persona_name}] persona...", style="dim italic")
407
+ elif current_persona_name in ("CODER", "DEBUGGER") and "```" not in current_persona_text:
408
+ try:
409
+ content = Syntax(current_persona_text, language, theme="monokai", line_numbers=True)
410
+ except Exception:
411
+ content = Text(current_persona_text)
412
+ else:
413
+ content = Text(current_persona_text)
414
+
415
+ return Panel(content, title=title, border_style=color)
416
+
417
+ with Live(make_live_panel(), console=console, refresh_per_second=15, auto_refresh=True) as live:
418
+ def stream_cb(persona, token: str):
419
+ nonlocal current_persona_name, current_persona_text
420
+ p_name = persona.value if hasattr(persona, "value") else str(persona)
421
+ if p_name != current_persona_name:
422
+ current_persona_name = p_name
423
+ current_persona_text = ""
424
+ current_persona_text += token
425
+ live.update(make_live_panel())
426
+
427
+ result = orchestrator.execute_pipeline(
428
+ user_prompt=effective_prompt,
429
+ language=language,
430
+ test_code=resolved_test_code,
431
+ token_stream_callback=stream_cb,
432
+ persona=persona_val,
433
+ )
434
+
435
+ # Display Diff Block if retries occurred (Auto-Debug Repair Diff)
436
+ if result.attempts > 1:
437
+ coder_entry = next((h for h in result.history if isinstance(h, dict) and h.get("persona") == Persona.CODER.value), None)
438
+ if coder_entry and coder_entry.get("output"):
439
+ initial_candidate = coder_entry["output"]
440
+ diff_text = compute_diff(initial_candidate, result.final_code)
441
+ if diff_text:
442
+ diff_syntax = Syntax(diff_text, "diff", theme="monokai", line_numbers=False)
443
+ diff_panel = Panel(diff_syntax, title=f"[bold yellow]Auto-Debug Repair Diff (Attempt {result.attempts - 1})[/bold yellow]", border_style="yellow")
444
+ console.print(diff_panel)
445
+
446
+ # Display Verification Results
447
+ if result.success:
448
+ console.print(f"[bold green]✔ GROUND-TRUTH VERIFIED[/bold green] [dim]({result.verification.verification_type.upper()} guard | Retries: {result.attempts - 1} | RAM: {result.ram_usage_mb:.2f} MB)[/dim]\n")
449
+
450
+ if result.architecture_plan:
451
+ plan_panel = Panel(result.architecture_plan.strip(), title="Architecture Plan & Reasoning", border_style="cyan")
452
+ console.print(plan_panel)
453
+
454
+ syntax = Syntax(result.final_code, language, theme="monokai", line_numbers=True)
455
+ panel = Panel(syntax, title=f"[bold green]Verified {language.upper()} Implementation[/bold green]", border_style="green")
456
+ console.print(panel)
457
+
458
+ if save_to_path:
459
+ save_to_path.write_text(result.final_code, encoding="utf-8")
460
+ console.print(f"\n[bold blue]Saved verified code to:[/bold blue] {save_to_path.resolve()}")
461
+
462
+ else:
463
+ console.print(f"[bold red]✘ VERIFICATION FAILED AFTER RETRIES[/bold red] [dim](Line: {result.verification.line_number or 'Unknown'} | RAM: {result.ram_usage_mb:.2f} MB)[/dim]\n")
464
+
465
+ err_trace = (result.verification.error_trace if result.verification else None) or "Verification failed."
466
+ err_panel = Panel(err_trace, title="Compiler / Verification Error Trace", border_style="red")
467
+ console.print(err_panel)
468
+
469
+ syntax = Syntax(result.final_code, language, theme="monokai", line_numbers=True)
470
+ code_panel = Panel(syntax, title="Unverified Candidate Code", border_style="yellow")
471
+ console.print(code_panel)
472
+
473
+ raise typer.Exit(code=1)
474
+
475
+
476
+ @app.command(name="exec", help="Execute any shell/terminal command locally on this machine (Google Antigravity style).")
477
+ @app.command(name="cmd", help="Alias for k-cli exec: run any shell/terminal command locally.")
478
+ def execute_local_command_cli(
479
+ command: str = typer.Argument(..., help="Shell command line to execute on local system."),
480
+ cwd: str = typer.Option(".", "--cwd", "-C", help="Working directory to run command in."),
481
+ timeout: int = typer.Option(60, "--timeout", "-t", help="Maximum execution timeout in seconds."),
482
+ ):
483
+ from k_cli.tools.command_runner import global_command_executor
484
+ console.print(f"[bold cyan]⚡ K-CLI Local Command Runner (Antigravity Engine):[/bold cyan] [white]{command}[/white]")
485
+ res = global_command_executor.execute(command=command, cwd=cwd, timeout=timeout)
486
+ if res.stdout.strip():
487
+ console.print(res.stdout.rstrip())
488
+ if res.stderr.strip():
489
+ console.print(f"[bold red]{res.stderr.rstrip()}[/bold red]")
490
+ if res.exit_code != 0:
491
+ console.print(f"[bold red]✖ Command failed with exit code {res.exit_code} ({res.duration_sec:.2f}s)[/bold red]")
492
+ raise typer.Exit(code=res.exit_code)
493
+ else:
494
+ console.print(f"[bold green]✔ Command completed in {res.duration_sec:.2f}s[/bold green]")
495
+
496
+
497
+ @app.command(name="run", help="Generate and verify code for a given prompt.")
498
+ def run(
499
+ prompt: str = typer.Argument(..., help="Natural language prompt / coding task description."),
500
+ language: str = typer.Option("python", "--language", "-l", help="Target programming language (python, bash, cpp)."),
501
+ model: str = typer.Option("qwen2.5-coder:1.5b", "--model", "-m", help="Ollama model name."),
502
+ max_retries: int = typer.Option(3, "--retries", "-r", help="Max auto-debug retry attempts."),
503
+ save_to: Optional[Path] = typer.Option(None, "--save-to", "-s", help="File path to save verified code output."),
504
+ mock: bool = typer.Option(False, "--mock", help="Force mock model execution for offline testing."),
505
+ test_file: Optional[Path] = typer.Option(None, "--test-file", "-t", help="Path to test file for verification."),
506
+ test_code: Optional[str] = typer.Option(None, "--test-code", help="Inline test code string for verification."),
507
+ persona: Optional[str] = typer.Option(None, "--persona", "-p", help="Specialized domain persona (devops, debugger, systems, security, frontend, database)."),
508
+ enhance: bool = typer.Option(False, "--enhance", help="Adapt the task to the selected model's strengths."),
509
+ rules_file: Optional[Path] = typer.Option(None, "--rules", help="Optional workspace-contained project guidance file."),
510
+ provider: Optional[str] = typer.Option(None, "--provider", help="Provider name (for example ollama, openai, or openai-compatible)."),
511
+ base_url: Optional[str] = typer.Option(None, "--base-url", help="Base URL for an OpenAI-compatible endpoint; use KCLI_API_KEY for its token."),
512
+ ):
513
+ execute_run(
514
+ prompt=prompt,
515
+ language=language,
516
+ model=model,
517
+ max_retries=max_retries,
518
+ save_to=save_to,
519
+ mock=mock,
520
+ show_banner=True,
521
+ test_file=test_file,
522
+ test_code=test_code,
523
+ persona=persona,
524
+ enhance=enhance,
525
+ rules_file=rules_file,
526
+ provider=provider,
527
+ base_url=base_url,
528
+ )
529
+
530
+
531
+ @app.command(name="prompt", help="Preview a provider-aware prompt without calling a model.")
532
+ def prompt_cmd(
533
+ task: str = typer.Argument(..., help="Task to adapt."),
534
+ model: str = typer.Option("qwen2.5-coder:1.5b", "--model", "-m", help="Target model name."),
535
+ language: str = typer.Option("python", "--language", "-l", help="Target language."),
536
+ rules_file: Optional[Path] = typer.Option(None, "--rules", help="Optional workspace-contained project guidance file."),
537
+ ):
538
+ preview = enhance_prompt(task, model, language)
539
+ if rules_file is not None:
540
+ try:
541
+ guidance = load_project_rules(Path.cwd(), rules_file)
542
+ except ValueError as exc:
543
+ console.print(f"[bold red]Invalid project guidance:[/bold red] {exc}")
544
+ raise typer.Exit(code=2)
545
+ if guidance:
546
+ preview = f"{preview}\n\n{guidance}"
547
+ console.print(Panel(preview, title=f"Prompt preview · {resolve_profile(model).name}", border_style="cyan"))
548
+
549
+
550
+ @app.command(name="audit", help="Generate candidates with 5+ models in parallel, adversarial peer review, and verify locally.")
551
+ def audit_cmd(
552
+ task: str = typer.Argument(..., help="Implementation task to audit across multiple models."),
553
+ models: str = typer.Option("gemini-2.0-flash,claude-3-7-sonnet,deepseek-reasoner,gpt-4o,qwen2.5-coder:7b", "--models", "-m", help="Comma-separated model names (supports 2 to 10+ models)."),
554
+ language: str = typer.Option("python", "--language", "-l", help="Target language."),
555
+ mock: bool = typer.Option(False, "--mock", help="Use offline mock drivers."),
556
+ as_json: bool = typer.Option(False, "--json", help="Emit machine-readable audit results."),
557
+ ):
558
+ """Executes multi-model parallel code generation, peer review, and AST verification."""
559
+ from k_cli.agents.adversarial_swarm import MultiModelConsensusSwarm
560
+
561
+ selected_models = [item.strip() for item in models.split(",") if item.strip()]
562
+ if len(selected_models) < 2:
563
+ selected_models = ["gemini-2.0-flash", "claude-3-7-sonnet", "deepseek-reasoner", "gpt-4o", "qwen2.5-coder:7b"]
564
+
565
+ swarm = MultiModelConsensusSwarm(models=selected_models, mock_mode=mock)
566
+ report = swarm.audit_and_generate(task_prompt=task, language=language)
567
+
568
+ if as_json:
569
+ payload = {
570
+ "task": report.task,
571
+ "selected_model": report.selected_model,
572
+ "consensus_score": report.consensus_score,
573
+ "cross_model_agreement_pct": report.cross_model_agreement_pct,
574
+ "total_duration_sec": report.total_duration_sec,
575
+ "candidates": [
576
+ {
577
+ "model": c.model_name,
578
+ "provider": c.provider,
579
+ "ast_valid": c.ast_valid,
580
+ "verification_passed": c.verification_passed,
581
+ "latency_sec": c.generation_time_sec,
582
+ "score": c.score,
583
+ "code": c.code,
584
+ }
585
+ for c in report.candidates
586
+ ],
587
+ }
588
+ typer.echo(json.dumps(payload, indent=2))
589
+ return
590
+
591
+
592
+ console.print(Markdown(report.render_markdown()))
593
+
594
+
595
+
596
+ @app.command(name="feature", help="Collect read-only source and test evidence for a feature claim.")
597
+ def feature_cmd(
598
+ query: str = typer.Argument(..., help="Feature or capability to look for."),
599
+ root_dir: Path = typer.Option(Path("."), "--dir", "-d", help="Workspace root directory."),
600
+ require_tests: bool = typer.Option(False, "--require-tests", help="Fail unless matching test evidence is found."),
601
+ as_json: bool = typer.Option(False, "--json", help="Emit machine-readable evidence."),
602
+ ):
603
+ """Check whether a requested capability has implementation and supporting evidence."""
604
+ evidence = inspect_feature(query, root_dir)
605
+ if as_json:
606
+ console.print(json.dumps(evidence.to_dict(), indent=2))
607
+ else:
608
+ table = Table(title="K-CLI Feature Evidence", box=None)
609
+ table.add_column("Evidence", style="cyan")
610
+ table.add_column("Count", style="bold white")
611
+ table.add_row("Source matches", str(len(evidence.source_matches)))
612
+ table.add_row("Test matches", str(len(evidence.test_matches)))
613
+ table.add_row("Symbol matches", str(len(evidence.symbol_matches)))
614
+ table.add_row("Status", "[green]PROVEN[/green]" if evidence.proven else "[yellow]INCONCLUSIVE[/yellow]")
615
+ console.print(table)
616
+ for match in (evidence.source_matches + evidence.test_matches + evidence.symbol_matches)[:15]:
617
+ console.print(f"[dim]{match.category} {match.path}:{match.line}[/dim] {match.evidence}")
618
+ if not evidence.proven or (require_tests and not evidence.test_matches):
619
+ raise typer.Exit(code=1)
620
+
621
+
622
+ def execute_subagents_run(
623
+ prompt: str,
624
+ model: str = "qwen2.5-coder:1.5b",
625
+ max_workers: int = 4,
626
+ save_to: Optional[Path] = None,
627
+ mock: bool = False,
628
+ show_banner: bool = True,
629
+ no_ui: bool = False,
630
+ context_files: Optional[List[str]] = None,
631
+ ):
632
+ """Core execution logic for decomposing prompts into parallel subagent workers."""
633
+ model = str(_resolve_val(model, "qwen2.5-coder:1.5b"))
634
+ max_workers = int(_resolve_val(max_workers, 4))
635
+ mock = bool(_resolve_val(mock, False))
636
+ if not mock and ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM")):
637
+ mock = True
638
+ save_to_val = _resolve_val(save_to, None)
639
+ save_to_path = Path(save_to_val) if save_to_val else None
640
+
641
+ if show_banner:
642
+ print_banner()
643
+
644
+ driver = LLMDriver(model_name=model, mock_mode=mock)
645
+ verifier = Verifier()
646
+ dispatcher = SubagentDispatcher(
647
+ driver=driver,
648
+ verifier=verifier,
649
+ max_workers=max_workers,
650
+ )
651
+
652
+ initial_ram = psutil.Process().memory_info().rss / (1024 * 1024)
653
+ driver_type = "ONLINE (Ollama GGUF)" if driver.is_ollama_available() else "LOCAL (llama-cpp-python GGUF)"
654
+
655
+ if show_banner:
656
+ table = Table(title="Multi-Agent System Environment", box=None)
657
+ table.add_column("Parameter", style="cyan")
658
+ table.add_column("Value", style="magenta")
659
+ table.add_row("Active Model", model)
660
+ table.add_row("Max Parallel Workers", str(max_workers))
661
+ table.add_row("SLM Driver Engine", driver_type)
662
+ table.add_row("Initial RAM Allocation", f"{initial_ram:.2f} MB / 1024 MB")
663
+ console.print(table)
664
+ console.print()
665
+
666
+ console.print(f"[bold yellow]Multi-Agent Task:[/bold yellow] [italic]'{prompt}'[/italic]\n")
667
+
668
+ tasks = dispatcher.decomposer.decompose(
669
+ prompt=prompt,
670
+ context_files=context_files,
671
+ )
672
+
673
+ # Display planned task hierarchy
674
+ tree = SubagentVisualizer.render_tree(tasks, title=f"Planned Subagent Tree ({len(tasks)} tasks)")
675
+ console.print(tree)
676
+ console.print()
677
+
678
+ if no_ui:
679
+ result = dispatcher.dispatch(tasks=tasks)
680
+ else:
681
+ result = SubagentVisualizer.execute_with_live_cli(
682
+ dispatcher=dispatcher,
683
+ tasks=tasks,
684
+ console=console,
685
+ )
686
+
687
+ console.print()
688
+ if result.success:
689
+ console.print(f"[bold green]✔ MULTI-AGENT TASK COMPLETED SUCCESSFULLY[/bold green] [dim](Tasks: {len(result.tasks)} | Duration: {result.total_duration_sec:.2f}s | RAM: {result.total_ram_mb:.2f} MB)[/dim]\n")
690
+
691
+ # Display Final Patch or Code
692
+ if result.aggregated_patch:
693
+ syntax = Syntax(result.aggregated_patch, "diff", theme="monokai", line_numbers=False)
694
+ panel = Panel(syntax, title="[bold green]Unified Aggregated Patch[/bold green]", border_style="green")
695
+ console.print(panel)
696
+ elif result.final_code:
697
+ syntax = Syntax(result.final_code, "python", theme="monokai", line_numbers=True)
698
+ panel = Panel(syntax, title="[bold green]Verified Implementation Code[/bold green]", border_style="green")
699
+ console.print(panel)
700
+
701
+ if save_to_path:
702
+ out_content = result.aggregated_patch if result.aggregated_patch else result.final_code
703
+ save_to_path.write_text(out_content, encoding="utf-8")
704
+ console.print(f"\n[bold blue]Saved output to:[/bold blue] {save_to_path.resolve()}")
705
+
706
+ return result
707
+
708
+ else:
709
+ console.print(f"[bold red]✘ SUBAGENTS PIPELINE FAILED[/bold red] [dim](Duration: {result.total_duration_sec:.2f}s | RAM: {result.total_ram_mb:.2f} MB)[/dim]\n")
710
+ if result.verification and not result.verification.success:
711
+ err_trace = result.verification.error_trace or "Verification failed."
712
+ console.print(Panel(err_trace, title="Compiler / Verification Error Trace", border_style="red"))
713
+
714
+ if result.final_code:
715
+ syntax = Syntax(result.final_code, "python", theme="monokai", line_numbers=True)
716
+ console.print(Panel(syntax, title="Unverified Candidate Output", border_style="yellow"))
717
+
718
+ raise typer.Exit(code=1)
719
+
720
+
721
+ @app.command(name="subagents", help="Decompose complex prompt into parallel subagents (Explorer, Researcher, Refactorer, Tester).")
722
+ def subagents_cmd(
723
+ prompt: str = typer.Argument(..., help="Complex user prompt or coding task."),
724
+ model: str = typer.Option("qwen2.5-coder:1.5b", "--model", "-m", help="Ollama model name."),
725
+ max_workers: int = typer.Option(4, "--workers", "-w", help="Max parallel subagent workers."),
726
+ save_to: Optional[Path] = typer.Option(None, "--save-to", "-s", help="File path to save verified patch or code."),
727
+ mock: bool = typer.Option(False, "--mock", help="Force mock model execution for offline testing."),
728
+ no_ui: bool = typer.Option(False, "--no-ui", help="Disable live Rich CLI visualization."),
729
+ ):
730
+ execute_subagents_run(
731
+ prompt=prompt,
732
+ model=model,
733
+ max_workers=max_workers,
734
+ save_to=save_to,
735
+ mock=mock,
736
+ show_banner=True,
737
+ no_ui=no_ui,
738
+ )
739
+
740
+
741
+ @app.command(name="spawn", help="Alias for subagents: Decompose and execute prompt with parallel subagents.")
742
+ def spawn_cmd(
743
+ prompt: str = typer.Argument(..., help="Complex user prompt or coding task."),
744
+ model: str = typer.Option("qwen2.5-coder:1.5b", "--model", "-m", help="Ollama model name."),
745
+ max_workers: int = typer.Option(4, "--workers", "-w", help="Max parallel subagent workers."),
746
+ save_to: Optional[Path] = typer.Option(None, "--save-to", "-s", help="File path to save verified patch or code."),
747
+ mock: bool = typer.Option(False, "--mock", help="Force mock model execution for offline testing."),
748
+ no_ui: bool = typer.Option(False, "--no-ui", help="Disable live Rich CLI visualization."),
749
+ ):
750
+ execute_subagents_run(
751
+ prompt=prompt,
752
+ model=model,
753
+ max_workers=max_workers,
754
+ save_to=save_to,
755
+ mock=mock,
756
+ show_banner=True,
757
+ no_ui=no_ui,
758
+ )
759
+
760
+
761
+ @app.command(name="verify", help="Run standalone ground-truth verification on a local code file or inline code string.")
762
+ def verify(
763
+ file_path: Optional[Path] = typer.Argument(None, help="Path to code file to verify."),
764
+ code: Optional[str] = typer.Option(None, "--code", "-c", help="Inline code string to verify."),
765
+ language: Optional[str] = typer.Option(None, "--language", "-l", help="Language override."),
766
+ test_file: Optional[Path] = typer.Option(None, "--test-file", "-t", help="Path to test file for pytest verification."),
767
+ test_code: Optional[str] = typer.Option(None, "--test-code", help="Inline test code string for pytest verification."),
768
+ as_json: bool = typer.Option(False, "--json", help="Emit machine-readable verification results."),
769
+ ):
770
+ if not as_json:
771
+ print_banner()
772
+
773
+ file_path_val = _resolve_val(file_path, None)
774
+ code_val = _resolve_val(code, None)
775
+ lang_val = _resolve_val(language, None)
776
+ test_file_val = _resolve_val(test_file, None)
777
+ test_code_val = _resolve_val(test_code, None)
778
+
779
+ if file_path_val is None and not code_val:
780
+ console.print("[bold red]Error:[/bold red] Must specify a file path or code string to verify.")
781
+ raise typer.Exit(code=1)
782
+
783
+ resolved_code = ""
784
+ display_target = ""
785
+ default_lang = "python"
786
+
787
+ if file_path_val is not None:
788
+ fp = Path(file_path_val)
789
+ if not fp.exists():
790
+ console.print(f"[bold red]Error:[/bold red] File '{fp}' does not exist.")
791
+ raise typer.Exit(code=1)
792
+ resolved_code = fp.read_text(encoding="utf-8")
793
+ display_target = fp.name
794
+ ext = fp.suffix.lstrip(".").lower()
795
+ default_lang = "python" if ext in ("py", "python") else "bash" if ext in ("sh", "bash") else "cpp" if ext in ("cpp", "cxx", "cc") else "python"
796
+ else:
797
+ resolved_code = code_val
798
+ display_target = "inline code"
799
+
800
+ lang = lang_val or default_lang
801
+
802
+ resolved_test_code = test_code_val
803
+ if test_file_val is not None:
804
+ tf_path = Path(test_file_val)
805
+ if tf_path.exists():
806
+ resolved_test_code = tf_path.read_text(encoding="utf-8")
807
+
808
+ verifier = Verifier()
809
+ result = verifier.verify(resolved_code, language=lang, test_code=resolved_test_code)
810
+
811
+ if as_json:
812
+ payload = result.to_dict()
813
+ payload["target"] = display_target
814
+ console.print(json.dumps(payload, indent=2))
815
+ if not result.success:
816
+ raise typer.Exit(code=1)
817
+ return
818
+
819
+ if result.success:
820
+ console.print(f"[bold green]✔ File '{display_target}' passed ground-truth {result.verification_type} verification![/bold green]")
821
+ else:
822
+ console.print(f"[bold red]✘ File '{display_target}' failed verification at line {result.line_number or 'unknown'}.[/bold red]\n")
823
+ err_trace = result.error_trace or "Verification failed."
824
+ console.print(Panel(err_trace, title="Compiler / Verification Error Trace", border_style="red"))
825
+ raise typer.Exit(code=1)
826
+
827
+
828
+ @app.command(name="status", help="Check K-CLI active system RAM budget, model diagnostics, and git branch.")
829
+ def status():
830
+ print_banner()
831
+ driver = LLMDriver()
832
+ orchestrator = Orchestrator(driver=driver)
833
+ session = SessionManager(model_name=driver.model_name)
834
+
835
+ ram_mb = orchestrator.get_current_ram_mb()
836
+ ollama_ok = driver.is_ollama_available()
837
+
838
+ table = Table(title="K-CLI System Diagnostics", box=None)
839
+ table.add_column("Property", style="cyan")
840
+ table.add_column("Value", style="bold white")
841
+ table.add_row("Active Model", driver.model_name)
842
+ table.add_row("Git Branch", session.get_git_branch())
843
+ table.add_row("Active Persona", session.active_persona)
844
+ table.add_row("Memory RSS Allocation", f"{ram_mb:.2f} MB / 1024 MB (Budget Limit)")
845
+ table.add_row("SLM Driver Engine", "[green]ONLINE (Ollama GGUF)[/green]" if ollama_ok else "[yellow]LOCAL (llama-cpp-python GGUF)[/yellow]")
846
+ table.add_row("Default Model", driver.model_name)
847
+ table.add_row("Python Environment", sys.version.split()[0])
848
+ console.print(table)
849
+
850
+
851
+ @app.command(name="plan", help="Create a protected, read-only implementation plan for a workspace.")
852
+ def plan_cmd(
853
+ goal: str = typer.Argument(..., help="Outcome to plan; no project files are changed."),
854
+ root_dir: Path = typer.Option(Path("."), "--dir", "-d", help="Workspace to inspect."),
855
+ rules_file: Optional[Path] = typer.Option(None, "--rules", help="Optional workspace-contained project guidance file."),
856
+ as_json: bool = typer.Option(False, "--json", help="Emit machine-readable plan data."),
857
+ ):
858
+ """Inspect a workspace and print an implementation plan without editing anything."""
859
+ result = create_plan(goal, root_dir)
860
+ if rules_file is not None:
861
+ try:
862
+ result.project_guidance = load_project_rules(root_dir, rules_file)
863
+ except ValueError as exc:
864
+ console.print(f"[bold red]Invalid project guidance:[/bold red] {exc}")
865
+ raise typer.Exit(code=2)
866
+ if as_json:
867
+ sys.stdout.write(json.dumps({
868
+ "goal": result.goal,
869
+ "workspace": str(result.workspace),
870
+ "relevant_files": result.relevant_files,
871
+ "detected_tools": result.detected_tools,
872
+ "repo_map": result.repo_map,
873
+ "project_guidance": result.project_guidance,
874
+ "read_only": True,
875
+ }, indent=2) + "\n")
876
+ else:
877
+ console.print(result.render_markdown())
878
+
879
+
880
+ @app.command(name="doctor", help="Check install, workspace, model, and safety prerequisites.")
881
+ def doctor_cmd(
882
+ root_dir: Path = typer.Option(Path("."), "--dir", "-d", help="Workspace to diagnose."),
883
+ as_json: bool = typer.Option(False, "--json", help="Emit machine-readable diagnostics."),
884
+ ):
885
+ """Print actionable diagnostics without downloading models or changing project files."""
886
+ root = root_dir.resolve()
887
+ driver = LLMDriver()
888
+ findings = scan_workspace(root)
889
+ checks = [
890
+ ("Workspace", str(root), root.exists()),
891
+ ("Python", sys.version.split()[0], sys.version_info >= (3, 11)),
892
+ ("Git repository", "detected" if GitGuard(root).is_git_repo() else "not detected", GitGuard(root).is_git_repo()),
893
+ ("Ollama", "reachable" if driver.is_ollama_available() else "not reachable (mock mode still works)", driver.is_ollama_available()),
894
+ ("KCLI_MOCK_MODE", os.getenv("KCLI_MOCK_MODE", "not set"), True),
895
+ ("Secret hygiene", "no obvious committed credentials" if not findings else f"{len(findings)} potential credential(s) found", not findings),
896
+ ]
897
+ if as_json:
898
+ payload = {
899
+ "workspace": str(root),
900
+ "checks": [
901
+ {"name": label, "detail": detail, "passed": passed}
902
+ for label, detail, passed in checks
903
+ ],
904
+ "findings": [
905
+ {"rule": finding.rule, "path": str(finding.path), "line": finding.line}
906
+ for finding in findings
907
+ ],
908
+ "ready": all(passed for _, _, passed in checks),
909
+ }
910
+ console.print(json.dumps(payload, indent=2))
911
+ if not payload["ready"]:
912
+ raise typer.Exit(code=1)
913
+ return
914
+
915
+ table = Table(title="K-CLI Doctor", box=None)
916
+ table.add_column("Check", style="cyan")
917
+ table.add_column("Result")
918
+ table.add_column("Status")
919
+ for label, detail, passed in checks:
920
+ table.add_row(label, detail, "[green]ready[/green]" if passed else "[yellow]attention[/yellow]")
921
+ console.print(table)
922
+ for finding in findings:
923
+ console.print(f"[yellow]Potential {finding.rule}: {finding.path}:{finding.line} (value intentionally hidden)[/yellow]")
924
+
925
+
926
+ web_app = typer.Typer(name="web", help="Launch the world-class K-CLI Web UI dashboard server.", invoke_without_command=True)
927
+
928
+
929
+ @app.command(name="web-ui", help="Launch the world-class K-CLI Web UI dashboard server.")
930
+ def web_ui_cmd(
931
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Web server host interface."),
932
+ port: int = typer.Option(8000, "--port", "-p", help="Web server port number."),
933
+ open_browser: bool = typer.Option(True, "--open/--no-open", help="Automatically open browser on server startup."),
934
+ ):
935
+ """Launch the world-class FastAPI Web UI dashboard."""
936
+ from k_cli.web.server import start_web_server
937
+ console.print(f"[bold cyan]⚡ Launching K-CLI World-Class Web UI on http://{host}:{port}...[/bold cyan]")
938
+ start_web_server(host=host, port=port, open_browser=open_browser)
939
+
940
+
941
+ @web_app.callback(invoke_without_command=True)
942
+ def web_callback(
943
+ ctx: typer.Context,
944
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Web server host interface."),
945
+ port: int = typer.Option(8000, "--port", "-p", help="Web server port number."),
946
+ open_browser: bool = typer.Option(True, "--open/--no-open", help="Automatically open browser on server startup."),
947
+ ):
948
+ """Launch the world-class FastAPI Web UI dashboard."""
949
+ if ctx.invoked_subcommand is None:
950
+ web_ui_cmd(host=host, port=port, open_browser=open_browser)
951
+
952
+
953
+ @web_app.command(name="ui", help="Launch the world-class K-CLI Web UI dashboard.")
954
+ def web_sub_ui_cmd(
955
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Web server host interface."),
956
+ port: int = typer.Option(8000, "--port", "-p", help="Web server port number."),
957
+ open_browser: bool = typer.Option(True, "--open/--no-open", help="Automatically open browser on server startup."),
958
+ ):
959
+ """Launch the Web UI dashboard."""
960
+ web_ui_cmd(host=host, port=port, open_browser=open_browser)
961
+
962
+
963
+ app.add_typer(web_app, name="web")
964
+
965
+
966
+ # =============================================================================
967
+ # Tier 3: Streamlined Interactive Terminal REPL (`k-cli simple` / `k-cli simple ui`)
968
+ # =============================================================================
969
+ simple_app = typer.Typer(name="simple", help="Launch the streamlined, mouse-enabled text REPL UI.", invoke_without_command=True)
970
+
971
+
972
+ @app.command(name="simple-ui", help="Launch the streamlined text REPL with mouse and slash command support.")
973
+ @app.command(name="chat", help="Launch the streamlined interactive AI coding chat REPL.")
974
+ @app.command(name="repl", help="Launch the streamlined interactive AI coding chat REPL.")
975
+ def simple_ui_cmd(
976
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Active model label."),
977
+ persona: Optional[str] = typer.Option(None, "--persona", "-p", help="Active persona label."),
978
+ mock: bool = typer.Option(False, "--mock", help="Use offline mock driver."),
979
+ workspace: Path = typer.Option(Path("."), "--workspace", "-w", help="Workspace root directory."),
980
+ ):
981
+ """Launch the streamlined Tier 3 interactive terminal REPL."""
982
+ from k_cli.ui.simple_repl import run_simple_cli
983
+ run_simple_cli(workspace_dir=str(workspace), model_name=model, persona=persona, mock_mode=mock)
984
+
985
+
986
+ @simple_app.callback(invoke_without_command=True)
987
+ def simple_callback(
988
+ ctx: typer.Context,
989
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Active model label."),
990
+ persona: Optional[str] = typer.Option(None, "--persona", "-p", help="Active persona label."),
991
+ mock: bool = typer.Option(False, "--mock", help="Use offline mock driver."),
992
+ workspace: Path = typer.Option(Path("."), "--workspace", "-w", help="Workspace root directory."),
993
+ ):
994
+ if ctx.invoked_subcommand is None:
995
+ simple_ui_cmd(model=model, persona=persona, mock=mock, workspace=workspace)
996
+
997
+
998
+ @simple_app.command(name="ui", help="Launch the streamlined text REPL with mouse support.")
999
+ def simple_sub_ui_cmd(
1000
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Active model label."),
1001
+ persona: Optional[str] = typer.Option(None, "--persona", "-p", help="Active persona label."),
1002
+ mock: bool = typer.Option(False, "--mock", help="Use offline mock driver."),
1003
+ workspace: Path = typer.Option(Path("."), "--workspace", "-w", help="Workspace root directory."),
1004
+ ):
1005
+ simple_ui_cmd(model=model, persona=persona, mock=mock, workspace=workspace)
1006
+
1007
+
1008
+ app.add_typer(simple_app, name="simple")
1009
+
1010
+
1011
+ @app.command(name="ui", help="Launch the full-screen K-CLI Textual workstation.")
1012
+ def ui_cmd(
1013
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Active model label (auto-detected if omitted)."),
1014
+ persona: str = typer.Option("Fullstack AI Systems Engineer", "--persona", "-p", help="Active persona label."),
1015
+ mock: bool = typer.Option(False, "--mock", help="Use the offline mock driver."),
1016
+ demo: bool = typer.Option(False, "--demo", "-d", help="Launch in pure zero-AI demo exploration mode."),
1017
+ continue_session: bool = typer.Option(False, "--continue", "-c", help="Continue previous multi-turn session from local storage."),
1018
+ codex: bool = typer.Option(False, "--codex", help="Open the Codex onboarding hub on launch."),
1019
+ welcome: bool = typer.Option(False, "--welcome", help="Force open the first-time welcome onboarding modal."),
1020
+ workspace: Path = typer.Option(Path("."), "--workspace", "-w", help="Workspace root."),
1021
+ ):
1022
+ """Launch the polished Textual UI with dynamic model auto-detection and first-time onboarding."""
1023
+ try:
1024
+ from k_cli.tui.tui_app import KCliApp
1025
+ except ModuleNotFoundError:
1026
+ from k_cli.tui.tui_app import KCliCyberWorkstation as KCliApp
1027
+
1028
+ is_mock = mock or demo
1029
+ effective_model = model or DevPreferencesManager.get_best_available_model()
1030
+
1031
+ KCliApp(
1032
+ workspace_dir=str(workspace),
1033
+ model_name=effective_model,
1034
+ persona=persona,
1035
+ mock_mode=is_mock,
1036
+ show_codex_on_start=codex,
1037
+ show_welcome_on_start=welcome,
1038
+ continue_session=continue_session,
1039
+ ).run()
1040
+
1041
+
1042
+ @app.command(name="tui", help="Alias for launching the full-screen K-CLI Textual workstation.")
1043
+ def tui_cmd(
1044
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Active model label."),
1045
+ persona: str = typer.Option("Fullstack AI Systems Engineer", "--persona", "-p", help="Active persona label."),
1046
+ mock: bool = typer.Option(False, "--mock", help="Use the offline mock driver."),
1047
+ demo: bool = typer.Option(False, "--demo", "-d", help="Launch in pure zero-AI demo exploration mode."),
1048
+ continue_session: bool = typer.Option(False, "--continue", "-c", help="Continue previous multi-turn session from local storage."),
1049
+ codex: bool = typer.Option(False, "--codex", help="Open the Codex onboarding hub on launch."),
1050
+ welcome: bool = typer.Option(False, "--welcome", help="Force open the first-time welcome onboarding modal."),
1051
+ workspace: Path = typer.Option(Path("."), "--workspace", "-w", help="Workspace root."),
1052
+ ):
1053
+ ui_cmd(
1054
+ model=model,
1055
+ persona=persona,
1056
+ mock=mock,
1057
+ demo=demo,
1058
+ continue_session=continue_session,
1059
+ codex=codex,
1060
+ welcome=welcome,
1061
+ workspace=workspace,
1062
+ )
1063
+
1064
+
1065
+ @app.command(name="demo-ui", help="Launch the TUI in Pure Zero-AI Demo Mode (no API key or model needed).")
1066
+ def demo_ui_cmd(
1067
+ workspace: Path = typer.Option(Path("."), "--workspace", "-w", help="Workspace root."),
1068
+ ):
1069
+ """Launch the full-screen Textual workstation in pure exploration mode without requiring any AI backend."""
1070
+ ui_cmd(mock=True, demo=True, workspace=workspace)
1071
+
1072
+
1073
+ @app.command(name="codex", help="Launch the Codex Starting & Onboarding Hub (Cloud APIs, Local Models, Bankai HF, DevDocs).")
1074
+ def codex_cmd(
1075
+ mock: bool = typer.Option(False, "--mock", help="Use the offline mock driver."),
1076
+ workspace: Path = typer.Option(Path("."), "--workspace", "-w", help="Workspace root."),
1077
+ ):
1078
+ """Launch the Codex Starting Hub screen directly in the workstation."""
1079
+ ui_cmd(mock=mock, codex=True, workspace=workspace)
1080
+
1081
+
1082
+ @app.command(name="setup", help="Alias for launching the Codex Starting & Onboarding Hub.")
1083
+ def setup_cmd(
1084
+ mock: bool = typer.Option(False, "--mock", help="Use the offline mock driver."),
1085
+ workspace: Path = typer.Option(Path("."), "--workspace", "-w", help="Workspace root."),
1086
+ ):
1087
+ """Launch the Codex Starting Hub screen."""
1088
+ codex_cmd(mock=mock, workspace=workspace)
1089
+
1090
+
1091
+
1092
+ @app.command(name="diff", help="View active uncommitted git diff or side-by-side diff.")
1093
+ def diff_cmd(
1094
+ side_by_side: bool = typer.Option(False, "--side-by-side", "--sbs", "-s", help="Render side-by-side 2-column diff."),
1095
+ ):
1096
+ """Renders workspace git diff in inline or side-by-side format."""
1097
+ session = SessionManager()
1098
+ if not session.git_guard.is_git_repo():
1099
+ console.print("[yellow]Not inside a Git repository.[/yellow]")
1100
+ return
1101
+
1102
+ diff_text = session.git_guard.get_diff()
1103
+ if not diff_text.strip():
1104
+ console.print("[dim]Working tree is clean; no uncommitted changes.[/dim]")
1105
+ return
1106
+
1107
+ panel = DiffVisualizer.render_inline_diff(diff_text, title="Git Working Tree Diff")
1108
+ console.print(panel)
1109
+
1110
+
1111
+ @app.command(name="review", help="Review changed source files without modifying the workspace.")
1112
+ def review_cmd(
1113
+ root_dir: Path = typer.Option(Path("."), "--dir", "-d", help="Workspace or Git repository root."),
1114
+ as_json: bool = typer.Option(False, "--json", help="Emit machine-readable review results."),
1115
+ ):
1116
+ """Run read-only syntax checks over changed Python files and summarize the diff."""
1117
+ root = root_dir.resolve()
1118
+ guard = GitGuard(root)
1119
+ if not guard.is_git_repo():
1120
+ payload = {
1121
+ "workspace": str(root),
1122
+ "git_repository": False,
1123
+ "changed_files": [],
1124
+ "syntax_failures": [],
1125
+ "status": "not-a-git-repository",
1126
+ }
1127
+ if as_json:
1128
+ console.print(json.dumps(payload, indent=2))
1129
+ else:
1130
+ console.print("[yellow]Review requires a Git repository; no files were inspected.[/yellow]")
1131
+ raise typer.Exit(code=2)
1132
+
1133
+ status = guard._run_git(["status", "--porcelain"])
1134
+ changed_files: List[str] = []
1135
+ if status.returncode == 0:
1136
+ for line in status.stdout.splitlines():
1137
+ if len(line) >= 4:
1138
+ changed_files.append(line[3:].strip().strip('"'))
1139
+
1140
+ verifier = Verifier()
1141
+ failures = []
1142
+ checked = []
1143
+ for relative in changed_files:
1144
+ path = root / relative
1145
+ if path.suffix.lower() != ".py" or not path.is_file():
1146
+ continue
1147
+ try:
1148
+ code = path.read_text(encoding="utf-8")
1149
+ except (OSError, UnicodeDecodeError) as error:
1150
+ failures.append({"file": relative, "error": str(error)})
1151
+ continue
1152
+ checked.append(relative)
1153
+ result = verifier.verify_python_ast(code)
1154
+ if not result.success:
1155
+ failures.append({"file": relative, "line": result.line_number, "error": result.error_trace})
1156
+
1157
+ payload = {
1158
+ "workspace": str(root),
1159
+ "git_repository": True,
1160
+ "changed_files": changed_files,
1161
+ "python_files_checked": checked,
1162
+ "syntax_failures": failures,
1163
+ "status": "failed" if failures else "passed",
1164
+ }
1165
+ if as_json:
1166
+ console.print(json.dumps(payload, indent=2))
1167
+ else:
1168
+ table = Table(title="K-CLI Read-Only Review", box=None)
1169
+ table.add_column("Metric", style="cyan")
1170
+ table.add_column("Value", style="bold white")
1171
+ table.add_row("Changed files", str(len(changed_files)))
1172
+ table.add_row("Python files checked", str(len(checked)))
1173
+ table.add_row("Syntax failures", str(len(failures)))
1174
+ console.print(table)
1175
+ for failure in failures:
1176
+ console.print(
1177
+ f"[red]✘ {failure['file']}:{failure.get('line') or 'unknown'} "
1178
+ f"{failure['error']}[/red]"
1179
+ )
1180
+ if not failures:
1181
+ console.print("[green]✔ Changed Python files passed AST review.[/green]")
1182
+ if failures:
1183
+ raise typer.Exit(code=1)
1184
+
1185
+
1186
+ @app.command(name="test", help="Run ground-truth compiler and pytest verification.")
1187
+ def test_cmd(
1188
+ target: Optional[str] = typer.Argument(None, help="Target file or test code to verify."),
1189
+ ):
1190
+ """Runs ground-truth verification on target file or workspace."""
1191
+ session = SessionManager()
1192
+ passed, summary = session.run_test(target)
1193
+ if passed:
1194
+ console.print(f"[bold green]✔[/bold green] {summary}")
1195
+ else:
1196
+ console.print(f"[bold red]✗[/bold red] {summary}")
1197
+ raise typer.Exit(code=1)
1198
+
1199
+
1200
+ @app.command(name="doc", help="Search offline DevDocs SQLite database for API signatures.")
1201
+ def doc(
1202
+ query: str = typer.Argument(..., help="Query string or API symbol name."),
1203
+ limit: int = typer.Option(3, "--limit", "-n", help="Max number of results to return."),
1204
+ max_tokens: int = typer.Option(250, "--max-tokens", "-t", help="Max tokens budget for context."),
1205
+ db_path: Optional[Path] = typer.Option(None, "--db", help="Path to SQLite docs database."),
1206
+ ):
1207
+ """Searches DevDocs FTS5 offline database for function and class signatures."""
1208
+ retriever = DocRetriever(db_path=str(db_path) if db_path else None)
1209
+ results = retriever.search(query, limit=limit, max_tokens=max_tokens)
1210
+ if not results:
1211
+ console.print(f"[yellow]No documentation found for '{query}'.[/yellow]")
1212
+ raise typer.Exit(code=2)
1213
+
1214
+ console.print(f"[bold cyan]DevDocs search results for '{query}':[/bold cyan]\n")
1215
+ for r in results:
1216
+ name = r.get("name", "")
1217
+ sig = r.get("signature", "")
1218
+ doc_str = r.get("doc", "")
1219
+ module = r.get("module", "")
1220
+ panel_content = f"[bold green]{sig}[/bold green]\n\n[dim]{doc_str}[/dim]"
1221
+ console.print(Panel(panel_content, title=f"Module: {module} | Symbol: {name}", border_style="cyan"))
1222
+
1223
+
1224
+ @app.command(name="devdocs", help="Download and index complete DevDocs offline documentation suite.")
1225
+ def devdocs_cmd(
1226
+ download: bool = typer.Option(True, "--download", "-d", help="Download and index all official DevDocs."),
1227
+ search: Optional[str] = typer.Option(None, "--search", "-s", help="Search offline DevDocs."),
1228
+ ):
1229
+ """Downloads all DevDocs standard libraries or searches offline docs."""
1230
+ retriever = DocRetriever()
1231
+ if search:
1232
+ results = retriever.search(search, limit=3)
1233
+ if not results:
1234
+ console.print(f"[yellow]No documentation found for '{search}'.[/yellow]")
1235
+ return
1236
+ for r in results:
1237
+ panel_content = f"[bold green]{r.get('signature')}[/bold green]\n\n[dim]{r.get('doc')}[/dim]"
1238
+ console.print(Panel(panel_content, title=f"Module: {r.get('module')} | Symbol: {r.get('name')}", border_style="cyan"))
1239
+ return
1240
+
1241
+ console.print("[bold cyan]📦 Indexing all standard libraries and frameworks into DevDocs SQLite database...[/bold cyan]")
1242
+ res = retriever.download_all_devdocs()
1243
+ console.print(f"[bold green]✔ Successfully indexed {res['total_database_symbols']} symbols in {res['duration_seconds']}s into {res['db_path']}![/bold green]")
1244
+
1245
+
1246
+
1247
+ @app.command(name="map", help="Display AST codebase repository map for the workspace.")
1248
+ def map_cmd(
1249
+ root_dir: Path = typer.Option(Path("."), "--dir", "-d", help="Workspace root directory."),
1250
+ max_tokens: int = typer.Option(400, "--max-tokens", "-t", help="Max tokens budget for map."),
1251
+ focus: Optional[List[str]] = typer.Option(None, "--focus", "-f", help="Files to prioritize."),
1252
+ ):
1253
+ """Generates and displays AST symbol tree for the workspace."""
1254
+ repo_map = RepoMap(root_dir=str(root_dir))
1255
+ tree_text = repo_map.get_repo_map(max_tokens=max_tokens, focus_files=focus)
1256
+ if not tree_text.strip():
1257
+ console.print("[yellow]Repository map is empty (no valid Python files found).[/yellow]")
1258
+ return
1259
+
1260
+ syntax = Syntax(tree_text, "python", theme="monokai", line_numbers=False)
1261
+ console.print(Panel(syntax, title="AST Codebase Repository Map", border_style="magenta"))
1262
+
1263
+
1264
+ @app.command(name="init", help="Initialize K-CLI environment, verify Ollama health, and bootstrap Bankai models.")
1265
+ def init_cmd(
1266
+ model: str = typer.Option("bankai-7b", "--model", "-m", help="Target Bankai model identifier (e.g. bankai-7b, bankai-10b)."),
1267
+ ollama_url: str = typer.Option("http://localhost:11434", "--ollama-url", help="Ollama daemon URL."),
1268
+ no_pull: bool = typer.Option(False, "--no-pull", help="Skip downloading/pulling model weights from Hugging Face Hub."),
1269
+ force: bool = typer.Option(False, "--force", "-f", help="Force re-download and re-creation even if cached."),
1270
+ mock: bool = typer.Option(False, "--mock", help="Force mock execution for offline testing."),
1271
+ ):
1272
+ """Initializes local K-CLI directory layout, checks Ollama status, and provisions default Bankai models."""
1273
+ print_banner()
1274
+ console.print("[bold cyan]⚡ Initializing K-CLI Environment & Bootstrapping Bankai Models...[/bold cyan]\n")
1275
+
1276
+ mock_mode = bool(_resolve_val(mock, False))
1277
+ if not mock_mode and ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM")):
1278
+ mock_mode = True
1279
+
1280
+ model_val = str(_resolve_val(model, "bankai-7b"))
1281
+ ollama_url_val = str(_resolve_val(ollama_url, "http://localhost:11434"))
1282
+ no_pull_val = bool(_resolve_val(no_pull, False))
1283
+ force_val = bool(_resolve_val(force, False))
1284
+
1285
+ manager = ModelManager(ollama_url=ollama_url_val, mock_mode=mock_mode) if ModelManager else None
1286
+ if manager is None:
1287
+ console.print("[bold red]Error:[/bold red] ModelManager module could not be loaded.")
1288
+ raise typer.Exit(code=1)
1289
+
1290
+ init_res = manager.init_environment(
1291
+ default_model=model_val,
1292
+ sync_model=not no_pull_val,
1293
+ force=force_val,
1294
+ )
1295
+
1296
+ # 1. Directory Hierarchy
1297
+ table = Table(title="K-CLI Environment Directory Layout", box=None)
1298
+ table.add_column("Directory", style="cyan")
1299
+ table.add_column("Status", style="green")
1300
+ for d in init_res.get("directories", []):
1301
+ table.add_row(d, "✔ Ready")
1302
+ console.print(table)
1303
+ console.print()
1304
+
1305
+ # 2. Ollama Diagnostics
1306
+ ollama_stat = init_res.get("ollama", {})
1307
+ ollama_ok = ollama_stat.get("healthy", False)
1308
+ ollama_table = Table(title="Local Ollama Inference Diagnostics", box=None)
1309
+ ollama_table.add_column("Property", style="cyan")
1310
+ ollama_table.add_column("Value", style="magenta")
1311
+ ollama_table.add_row("Ollama Host", ollama_stat.get("url", ollama_url_val))
1312
+ ollama_table.add_row("Daemon Status", "[bold green]ONLINE (Healthy)[/bold green]" if ollama_ok else "[bold yellow]OFFLINE / Unreachable[/bold yellow]")
1313
+ ollama_table.add_row("Ollama Version", str(ollama_stat.get("version", "unknown")))
1314
+ models_list = ", ".join(ollama_stat.get("models", [])) or "None loaded"
1315
+ ollama_table.add_row("Loaded Models", models_list)
1316
+ console.print(ollama_table)
1317
+ console.print()
1318
+
1319
+ # 3. Model Pull & Ollama Registration Status
1320
+ pull_info = init_res.get("model_pull")
1321
+ if pull_info:
1322
+ p_table = Table(title="Bankai Model Bootstrapper Status", box=None)
1323
+ p_table.add_column("Attribute", style="cyan")
1324
+ p_table.add_column("Details", style="bold white")
1325
+ p_table.add_row("Target Model", pull_info.get("model_name", model_val))
1326
+ p_table.add_row("Ollama Tag", pull_info.get("ollama_tag", model_val))
1327
+ p_table.add_row("Local GGUF Path", str(pull_info.get("gguf_path") or "None"))
1328
+ p_table.add_row("Modelfile Path", str(pull_info.get("modelfile_path") or "None"))
1329
+ sha_str = pull_info.get("sha256") or "N/A"
1330
+ sha_status = "[bold green]✔ Verified[/bold green]" if pull_info.get("sha256_verified") else "[yellow]Unverified[/yellow]"
1331
+ p_table.add_row("SHA256 Integrity", f"{sha_str[:20]}... ({sha_status})")
1332
+ ollama_created = "[bold green]✔ Registered in Ollama[/bold green]" if pull_info.get("ollama_created") else "[yellow]Pending (Ollama offline)[/yellow]"
1333
+ p_table.add_row("Ollama Deployment", ollama_created)
1334
+ console.print(p_table)
1335
+ console.print()
1336
+
1337
+ if init_res.get("ready"):
1338
+ console.print(Panel(
1339
+ f"[bold green]✔ Project Bankai Engine initialized successfully![/bold green]\n\n"
1340
+ f"• Active Model: [bold cyan]{model_val}[/bold cyan]\n"
1341
+ f"• Quick Run: [italic]k run 'write a binary search in python'[/italic]\n"
1342
+ f"• Interactive Shell: [italic]k[/italic]",
1343
+ title="[bold green]K-CLI Ready[/bold green]",
1344
+ border_style="green",
1345
+ ))
1346
+ else:
1347
+ console.print(Panel(
1348
+ "[yellow]⚠ K-CLI directories initialized. To run with local Ollama, start the Ollama daemon and run [bold]k pull-model[/bold].[/yellow]",
1349
+ title="[bold yellow]Setup Notice[/bold yellow]",
1350
+ border_style="yellow",
1351
+ ))
1352
+
1353
+
1354
+ @app.command(name="pull-model", help="Pull Bankai model from Hugging Face Hub into Ollama or local GGUF cache.")
1355
+ def pull_model_cmd(
1356
+ model: str = typer.Argument("bankai-7b", help="Model identifier (e.g. bankai-7b, bankai-10b, krishivjoshi/bankai-7b)."),
1357
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Ollama model tag to register (e.g. bankai:7b, bankai-7b)."),
1358
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Hugging Face repository ID override."),
1359
+ quant: str = typer.Option("q4_k_m", "--quant", "-q", help="Quantization format to target (default: q4_k_m)."),
1360
+ verify_sha: bool = typer.Option(True, "--verify-sha/--no-verify-sha", help="Cryptographically verify SHA256 integrity."),
1361
+ sha256: Optional[str] = typer.Option(None, "--sha256", help="Expected SHA256 checksum string."),
1362
+ ollama_url: str = typer.Option("http://localhost:11434", "--ollama-url", help="Ollama host URL."),
1363
+ no_ollama: bool = typer.Option(False, "--no-ollama", help="Skip Ollama model creation (cache GGUF only)."),
1364
+ force: bool = typer.Option(False, "--force", "-f", help="Force re-download even if cached."),
1365
+ mock: bool = typer.Option(False, "--mock", help="Force mock execution for offline testing."),
1366
+ ):
1367
+ """Pulls Bankai GGUF model directly from Hugging Face Hub, verifies SHA256 integrity, and registers in Ollama."""
1368
+ print_banner()
1369
+
1370
+ mock_mode = bool(_resolve_val(mock, False))
1371
+ if not mock_mode and ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM")):
1372
+ mock_mode = True
1373
+
1374
+ model_val = str(_resolve_val(model, "bankai-7b"))
1375
+ tag_val = _resolve_val(tag, None)
1376
+ repo_val = _resolve_val(repo, None)
1377
+ quant_val = str(_resolve_val(quant, "q4_k_m"))
1378
+ verify_sha_val = bool(_resolve_val(verify_sha, True))
1379
+ sha256_val = _resolve_val(sha256, None)
1380
+ ollama_url_val = str(_resolve_val(ollama_url, "http://localhost:11434"))
1381
+ no_ollama_val = bool(_resolve_val(no_ollama, False))
1382
+ force_val = bool(_resolve_val(force, False))
1383
+
1384
+ console.print(f"[bold cyan]🚀 Project Bankai Auto-Sync Engine: Pulling model '{model_val}'...[/bold cyan]\n")
1385
+
1386
+ manager = ModelManager(ollama_url=ollama_url_val, mock_mode=mock_mode) if ModelManager else None
1387
+ if manager is None:
1388
+ console.print("[bold red]Error:[/bold red] ModelManager module could not be loaded.")
1389
+ raise typer.Exit(code=1)
1390
+
1391
+ result = manager.pull_model(
1392
+ model_identifier=model_val,
1393
+ ollama_tag=tag_val,
1394
+ hf_repo=repo_val,
1395
+ force=force_val,
1396
+ verify_sha=verify_sha_val,
1397
+ create_in_ollama=not no_ollama_val,
1398
+ expected_sha256=sha256_val,
1399
+ quant=quant_val,
1400
+ )
1401
+
1402
+ # Render Result Table
1403
+ table = Table(title=f"Model Pull & Ollama Deployment Report: {model_val}", box=None)
1404
+ table.add_column("Property", style="cyan")
1405
+ table.add_column("Value", style="bold white")
1406
+ table.add_row("Model Identifier", result.model_name)
1407
+ table.add_row("Target Ollama Tag", result.ollama_tag)
1408
+ table.add_row("Hugging Face Source", result.details.get("repo_id", f"krishivjoshi/{model_val}"))
1409
+ table.add_row("Local GGUF Path", str(result.gguf_path) if result.gguf_path else "[red]None[/red]")
1410
+ table.add_row("Modelfile Generated", str(result.modelfile_path) if result.modelfile_path else "[yellow]None[/yellow]")
1411
+
1412
+ sha_text = result.sha256 or "N/A"
1413
+ if result.sha256_verified:
1414
+ sha_display = f"{sha_text[:20]}... [bold green]✔ SHA256 Verified[/bold green]"
1415
+ else:
1416
+ sha_display = f"{sha_text[:20]}... [bold red]✘ Verification Failed[/bold red]"
1417
+ table.add_row("SHA256 Integrity", sha_display)
1418
+
1419
+ if not no_ollama_val:
1420
+ if result.ollama_created:
1421
+ table.add_row("Ollama Registration", f"[bold green]✔ Created '{result.ollama_tag}'[/bold green]")
1422
+ elif not result.ollama_healthy:
1423
+ table.add_row("Ollama Registration", f"[yellow]⚠ Ollama daemon offline at {ollama_url_val}[/yellow]")
1424
+ else:
1425
+ table.add_row("Ollama Registration", f"[red]✘ Failed to create model in Ollama[/red]")
1426
+ else:
1427
+ table.add_row("Ollama Registration", "[dim]Skipped (--no-ollama)[/dim]")
1428
+
1429
+ console.print(table)
1430
+ console.print()
1431
+
1432
+ if result.success:
1433
+ console.print(f"[bold green]✔ SUCCESS: Model '{model_val}' is ready for local compiler-grounded inference.[/bold green]\n")
1434
+ else:
1435
+ console.print(f"[bold red]✘ PULL FAILED: {result.message}[/bold red]\n")
1436
+ raise typer.Exit(code=1)
1437
+
1438
+
1439
+ @app.command(name="pull", help="Alias for pull-model command.")
1440
+ def pull_cmd(
1441
+ model: str = typer.Argument("bankai-7b", help="Model identifier (e.g. bankai-7b, bankai-10b, krishivjoshi/bankai-7b)."),
1442
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Ollama model tag to register (e.g. bankai:7b, bankai-7b)."),
1443
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Hugging Face repository ID override."),
1444
+ quant: str = typer.Option("q4_k_m", "--quant", "-q", help="Quantization format to target (default: q4_k_m)."),
1445
+ verify_sha: bool = typer.Option(True, "--verify-sha/--no-verify-sha", help="Cryptographically verify SHA256 integrity."),
1446
+ sha256: Optional[str] = typer.Option(None, "--sha256", help="Expected SHA256 checksum string."),
1447
+ ollama_url: str = typer.Option("http://localhost:11434", "--ollama-url", help="Ollama host URL."),
1448
+ no_ollama: bool = typer.Option(False, "--no-ollama", help="Skip Ollama model creation (cache GGUF only)."),
1449
+ force: bool = typer.Option(False, "--force", "-f", help="Force re-download even if cached."),
1450
+ mock: bool = typer.Option(False, "--mock", help="Force mock execution for offline testing."),
1451
+ ):
1452
+ """Alias for pull-model command."""
1453
+ pull_model_cmd(
1454
+ model=model,
1455
+ tag=tag,
1456
+ repo=repo,
1457
+ quant=quant,
1458
+ verify_sha=verify_sha,
1459
+ sha256=sha256,
1460
+ ollama_url=ollama_url,
1461
+ no_ollama=no_ollama,
1462
+ force=force,
1463
+ mock=mock,
1464
+ )
1465
+
1466
+
1467
+ # ==============================================================================
1468
+ # Conflict Resolution Commands (k-cli conflict ...)
1469
+ # ==============================================================================
1470
+
1471
+ conflict_app = typer.Typer(
1472
+ name="conflict",
1473
+ help="Detect, inspect, and AI-resolve git merge conflicts.",
1474
+ add_completion=False,
1475
+ )
1476
+
1477
+
1478
+ @conflict_app.command(name="list", help="Detect and show conflicts in repo.")
1479
+ def conflict_list_cmd(
1480
+ dir: str = typer.Option(".", "--dir", "-d", help="Repository or workspace root directory."),
1481
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1482
+ ):
1483
+ target_dir = Path(dir).resolve()
1484
+ resolver = ConflictResolver() if ConflictResolver else None
1485
+ if resolver is None:
1486
+ if json_output:
1487
+ typer.echo(json.dumps({"error": "ConflictResolver module not available", "conflicts": []}))
1488
+ else:
1489
+ console.print("[bold red]Error:[/bold red] ConflictResolver module is not available.")
1490
+ raise typer.Exit(code=1)
1491
+
1492
+ conflicts = resolver.find_conflicts(repo_path=str(target_dir))
1493
+
1494
+ if json_output:
1495
+ out_data = {
1496
+ "repo_path": str(target_dir),
1497
+ "total_conflicts": len(conflicts),
1498
+ "conflicted_files_count": len({c.file_path for c in conflicts if c.file_path}),
1499
+ "conflicts": [c.to_dict() for c in conflicts],
1500
+ }
1501
+ typer.echo(json.dumps(out_data, indent=2))
1502
+ return
1503
+
1504
+ if not conflicts:
1505
+ console.print("[bold green]✔ Clean: No git merge conflicts detected in workspace.[/bold green]")
1506
+ return
1507
+
1508
+ table = Table(title=f"Git Merge Conflicts Detected ({len(conflicts)})", box=None)
1509
+ table.add_column("File", style="bold cyan")
1510
+ table.add_column("Lines", style="magenta")
1511
+ table.add_column("Type", style="yellow")
1512
+ table.add_column("Scope / Function", style="white")
1513
+ table.add_column("Ours Label", style="green")
1514
+ table.add_column("Theirs Label", style="red")
1515
+
1516
+ for c in conflicts:
1517
+ rel_p = str(Path(c.file_path).relative_to(target_dir)) if c.file_path.startswith(str(target_dir)) else c.file_path
1518
+ mtype = "3-Way (Diff3)" if c.is_3way() else "2-Way"
1519
+ table.add_row(
1520
+ rel_p,
1521
+ f"L{c.start_line}-{c.end_line}",
1522
+ mtype,
1523
+ c.scope_name or "(top-level)",
1524
+ c.ours_label,
1525
+ c.theirs_label,
1526
+ )
1527
+
1528
+ console.print(table)
1529
+ console.print(f"\n[dim]Run [bold]k-cli conflict resolve --file <path>[/bold] or [bold]k-cli conflict resolve[/bold] to resolve.[/dim]\n")
1530
+
1531
+
1532
+ @conflict_app.command(name="resolve", help="AI 3-way merge with verification.")
1533
+ def conflict_resolve_cmd(
1534
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific conflicted file path to resolve."),
1535
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model identifier to use."),
1536
+ auto_accept: bool = typer.Option(False, "--auto-accept", "-y", help="Automatically accept and stage resolved files."),
1537
+ dir: str = typer.Option(".", "--dir", "-d", help="Repository workspace root directory."),
1538
+ mock: bool = typer.Option(False, "--mock", help="Force mock execution for testing."),
1539
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1540
+ ):
1541
+ target_dir = Path(dir).resolve()
1542
+ resolver = ConflictResolver(default_model=model) if ConflictResolver else None
1543
+ if resolver is None:
1544
+ if json_output:
1545
+ typer.echo(json.dumps({"error": "ConflictResolver module not available", "success": False}))
1546
+ else:
1547
+ console.print("[bold red]Error:[/bold red] ConflictResolver module is not available.")
1548
+ raise typer.Exit(code=1)
1549
+
1550
+ is_mock = mock or os.getenv("KCLI_MOCK_MODE", "").lower() in ("true", "1") or ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM"))
1551
+ driver = LLMDriver(model_name=model or "qwen2.5-coder:1.5b", mock_mode=is_mock)
1552
+ verifier = Verifier()
1553
+
1554
+ if file:
1555
+ target_file = Path(file).resolve() if not Path(file).is_absolute() else Path(file)
1556
+ if not target_file.exists():
1557
+ if json_output:
1558
+ typer.echo(json.dumps({"error": f"File '{file}' not found", "success": False}))
1559
+ else:
1560
+ console.print(f"[bold red]Error:[/bold red] Conflicted file '{file}' not found.")
1561
+ raise typer.Exit(code=1)
1562
+
1563
+ res = resolver.resolve_file(
1564
+ file_path=str(target_file),
1565
+ llm_driver=driver,
1566
+ verifier=verifier,
1567
+ auto_stage=auto_accept,
1568
+ )
1569
+
1570
+ if json_output:
1571
+ typer.echo(json.dumps(res.to_dict(), indent=2))
1572
+ return
1573
+
1574
+ if res.success:
1575
+ console.print(f"[bold green]✔ Successfully resolved {res.resolved_conflicts}/{res.total_conflicts} conflict(s) in {file}.[/bold green]")
1576
+ if res.staged:
1577
+ console.print("[dim]✔ Automatically staged resolved file with git add.[/dim]")
1578
+ else:
1579
+ console.print(f"[bold red]✘ Failed to resolve conflicts in {file}: {res.error_message}[/bold red]")
1580
+ raise typer.Exit(code=1)
1581
+ else:
1582
+ summary = resolver.resolve_all_conflicts(
1583
+ repo_path=str(target_dir),
1584
+ llm_driver=driver,
1585
+ verifier=verifier,
1586
+ auto_stage=auto_accept,
1587
+ )
1588
+
1589
+ if json_output:
1590
+ typer.echo(json.dumps(summary.to_dict(), indent=2))
1591
+ return
1592
+
1593
+ if summary.total_files == 0:
1594
+ console.print("[bold green]✔ No merge conflicts detected in workspace.[/bold green]")
1595
+ return
1596
+
1597
+ console.print(f"[bold cyan]Conflict Resolution Summary:[/bold cyan] {summary.resolved_files}/{summary.total_files} files resolved successfully.")
1598
+ for fpath, f_res in summary.file_results.items():
1599
+ glyph = "[bold green]✔[/bold green]" if f_res.success else "[bold red]✘[/bold red]"
1600
+ console.print(f" {glyph} {os.path.basename(fpath)}: {f_res.resolved_conflicts}/{f_res.total_conflicts} resolved")
1601
+
1602
+ if not summary.success:
1603
+ raise typer.Exit(code=1)
1604
+
1605
+
1606
+ # ==============================================================================
1607
+ # Pull Request Lifecycle Commands (k-cli pr ...)
1608
+ # ==============================================================================
1609
+
1610
+ pr_app = typer.Typer(
1611
+ name="pr",
1612
+ help="Inspect, review, fix, and merge GitHub Pull Requests.",
1613
+ add_completion=False,
1614
+ )
1615
+
1616
+
1617
+ @pr_app.command(name="list", help="List GitHub pull requests.")
1618
+ def pr_list_cmd(
1619
+ state: str = typer.Option("open", "--state", "-s", help="Filter PRs by state: open, closed, all."),
1620
+ limit: int = typer.Option(30, "--limit", "-l", help="Max number of PRs to retrieve."),
1621
+ dir: str = typer.Option(".", "--dir", "-d", help="Repository workspace directory."),
1622
+ mock: bool = typer.Option(False, "--mock", help="Use mock GitHub client for offline testing."),
1623
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1624
+ ):
1625
+ target_dir = Path(dir).resolve()
1626
+ is_mock = mock or os.getenv("KCLI_MOCK_GITHUB", "0").lower() in ("1", "true") or ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("GITHUB_TOKEN"))
1627
+ client = GitHubClient(repo_dir=target_dir, mock_mode=is_mock) if GitHubClient else None
1628
+
1629
+ if client is None:
1630
+ if json_output:
1631
+ typer.echo(json.dumps({"error": "GitHubClient module not available"}))
1632
+ else:
1633
+ console.print("[bold red]Error:[/bold red] GitHubClient module is not available.")
1634
+ raise typer.Exit(code=1)
1635
+
1636
+ try:
1637
+ prs = client.list_pull_requests(state=state, limit=limit)
1638
+ except Exception as ex:
1639
+ if json_output:
1640
+ typer.echo(json.dumps({"error": str(ex), "pull_requests": []}))
1641
+ else:
1642
+ console.print(f"[bold yellow]⚠ Could not list pull requests:[/bold yellow] {ex}")
1643
+ return
1644
+
1645
+ if json_output:
1646
+ typer.echo(json.dumps([pr.to_dict() for pr in prs], indent=2))
1647
+ return
1648
+
1649
+ if not prs:
1650
+ console.print(f"[yellow]No {state} pull requests found.[/yellow]")
1651
+ return
1652
+
1653
+ table = Table(title=f"Pull Requests ({client.owner}/{client.repo}) [{state}]", box=None)
1654
+ table.add_column("#", style="bold cyan", justify="right")
1655
+ table.add_column("Title", style="bold white")
1656
+ table.add_column("Author", style="magenta")
1657
+ table.add_column("Branch", style="green")
1658
+ table.add_column("State", style="yellow")
1659
+ table.add_column("Created", style="dim")
1660
+
1661
+ for pr in prs:
1662
+ state_style = "green" if pr.state == "open" else ("magenta" if pr.merged else "red")
1663
+ table.add_row(
1664
+ str(pr.number),
1665
+ pr.title,
1666
+ pr.author or "unknown",
1667
+ f"{pr.head_branch} -> {pr.base_branch}",
1668
+ f"[{state_style}]{pr.state.upper()}[/{state_style}]",
1669
+ pr.created_at[:10] if pr.created_at else "",
1670
+ )
1671
+
1672
+ console.print(table)
1673
+
1674
+
1675
+ @pr_app.command(name="view", help="View pull request details and diff.")
1676
+ def pr_view_cmd(
1677
+ pr_num: int = typer.Argument(..., help="Pull request number."),
1678
+ dir: str = typer.Option(".", "--dir", "-d", help="Repository workspace directory."),
1679
+ mock: bool = typer.Option(False, "--mock", help="Use mock GitHub client for offline testing."),
1680
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1681
+ ):
1682
+ target_dir = Path(dir).resolve()
1683
+ is_mock = mock or os.getenv("KCLI_MOCK_GITHUB", "0").lower() in ("1", "true") or ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("GITHUB_TOKEN"))
1684
+ client = GitHubClient(repo_dir=target_dir, mock_mode=is_mock) if GitHubClient else None
1685
+
1686
+ if client is None:
1687
+ if json_output:
1688
+ typer.echo(json.dumps({"error": "GitHubClient module not available"}))
1689
+ else:
1690
+ console.print("[bold red]Error:[/bold red] GitHubClient module is not available.")
1691
+ raise typer.Exit(code=1)
1692
+
1693
+ try:
1694
+ pr = client.get_pull_request(pr_num)
1695
+ diff = client.get_pr_diff(pr_num)
1696
+ ci = client.get_ci_status(pr.head_sha or pr.head_branch)
1697
+ except Exception as ex:
1698
+ if json_output:
1699
+ typer.echo(json.dumps({"error": str(ex)}))
1700
+ else:
1701
+ console.print(f"[bold yellow]⚠ Could not view pull request #{pr_num}:[/bold yellow] {ex}")
1702
+ return
1703
+
1704
+ if json_output:
1705
+ data = pr.to_dict()
1706
+ data["diff"] = diff
1707
+ data["ci_status"] = ci.to_dict()
1708
+ typer.echo(json.dumps(data, indent=2))
1709
+ return
1710
+
1711
+ status_style = "bold green" if pr.state == "open" else ("bold magenta" if pr.merged else "bold red")
1712
+ ci_text = "[bold green]✔ Passing[/bold green]" if ci.is_passing else f"[bold red]✘ Failing ({ci.failed_count} failed)[/bold red]"
1713
+
1714
+ panel_content = (
1715
+ f"[bold white]{pr.title}[/bold white]\n\n"
1716
+ f"• [cyan]Author:[/cyan] {pr.author} • [cyan]State:[/cyan] [{status_style}]{pr.state.upper()}[/{status_style}] • [cyan]CI:[/cyan] {ci_text}\n"
1717
+ f"• [cyan]Branches:[/cyan] [green]{pr.head_branch}[/green] -> [blue]{pr.base_branch}[/blue] (HEAD: {pr.head_sha[:8] if pr.head_sha else 'N/A'})\n\n"
1718
+ f"[bold]Description:[/bold]\n{pr.body or '(No description provided)'}"
1719
+ )
1720
+ console.print(Panel(panel_content, title=f"Pull Request #{pr.number}", border_style="cyan"))
1721
+
1722
+ if diff.strip():
1723
+ console.print("\n[bold cyan]Diff Preview:[/bold cyan]")
1724
+ diff_lines = diff.splitlines()
1725
+ diff_preview = "\n".join(diff_lines[:30])
1726
+ if len(diff_lines) > 30:
1727
+ diff_preview += f"\n... ({len(diff_lines) - 30} more diff lines)"
1728
+ console.print(Syntax(diff_preview, "diff", theme="monokai", line_numbers=True))
1729
+
1730
+
1731
+ @pr_app.command(name="review", help="Perform compiler-grade AI code review on a pull request.")
1732
+ def pr_review_cmd(
1733
+ pr_num: int = typer.Argument(..., help="Pull request number."),
1734
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model identifier to use."),
1735
+ post_comment: bool = typer.Option(False, "--post-comment", help="Automatically post review comment to GitHub PR."),
1736
+ dir: str = typer.Option(".", "--dir", "-d", help="Repository workspace directory."),
1737
+ mock: bool = typer.Option(False, "--mock", help="Use mock GitHub client for offline testing."),
1738
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1739
+ ):
1740
+ target_dir = Path(dir).resolve()
1741
+ is_mock = mock or os.getenv("KCLI_MOCK_GITHUB", "0").lower() in ("1", "true") or ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("GITHUB_TOKEN"))
1742
+ client = GitHubClient(repo_dir=target_dir, mock_mode=is_mock) if GitHubClient else None
1743
+ mgr = PRLifecycleManager(client=client, repo_dir=target_dir) if PRLifecycleManager else None
1744
+
1745
+ if mgr is None:
1746
+ if json_output:
1747
+ typer.echo(json.dumps({"error": "PRLifecycleManager module not available"}))
1748
+ else:
1749
+ console.print("[bold red]Error:[/bold red] PRLifecycleManager module is not available.")
1750
+ raise typer.Exit(code=1)
1751
+
1752
+ driver = LLMDriver(model_name=model or "qwen2.5-coder:1.5b", mock_mode=is_mock)
1753
+ try:
1754
+ review = mgr.review_pr(
1755
+ pr_number=pr_num,
1756
+ llm_driver=driver,
1757
+ model=model,
1758
+ post_comment=post_comment,
1759
+ )
1760
+ except Exception as ex:
1761
+ if json_output:
1762
+ typer.echo(json.dumps({"error": f"Failed to review PR #{pr_num}: {ex}"}))
1763
+ else:
1764
+ console.print(f"[bold red]✘ Failed to review PR #{pr_num}:[/bold red] {ex}")
1765
+ raise typer.Exit(code=1)
1766
+
1767
+ if json_output:
1768
+ typer.echo(json.dumps(review.to_dict(), indent=2))
1769
+ return
1770
+
1771
+ verdict_color = "green" if review.verdict == "APPROVE" else ("red" if review.verdict == "REQUEST_CHANGES" else "yellow")
1772
+ console.print(Panel(
1773
+ f"[bold {verdict_color}]VERDICT: {review.verdict}[/bold {verdict_color}]\n\n"
1774
+ f"[bold]Summary:[/bold] {review.summary}\n\n"
1775
+ f"[bold red]Bugs Identified ({len(review.bugs)}):[/bold red]\n" + ("\n".join(f" • {b}" for b in review.bugs) if review.bugs else " None detected.") + "\n\n"
1776
+ f"[bold yellow]Security Issues ({len(review.security_issues)}):[/bold yellow]\n" + ("\n".join(f" • {s}" for s in review.security_issues) if review.security_issues else " None detected.") + "\n\n"
1777
+ f"[bold cyan]Performance Notes ({len(review.performance_notes)}):[/bold cyan]\n" + ("\n".join(f" • {p}" for p in review.performance_notes) if review.performance_notes else " None detected."),
1778
+ title=f"AI Code Review: PR #{pr_num}",
1779
+ border_style=verdict_color,
1780
+ ))
1781
+ if post_comment:
1782
+ console.print("[bold green]✔ Posted review comment to GitHub PR.[/bold green]")
1783
+
1784
+
1785
+ @pr_app.command(name="fix", help="Automatically generate surgical fixes for PR issues, verify tests, and commit.")
1786
+ def pr_fix_cmd(
1787
+ pr_num: int = typer.Argument(..., help="Pull request number."),
1788
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model identifier to use."),
1789
+ auto_push: bool = typer.Option(False, "--auto-push", help="Automatically push fixes to remote branch on passing verification."),
1790
+ dir: str = typer.Option(".", "--dir", "-d", help="Repository workspace directory."),
1791
+ mock: bool = typer.Option(False, "--mock", help="Use mock GitHub client for offline testing."),
1792
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1793
+ ):
1794
+ target_dir = Path(dir).resolve()
1795
+ is_mock = mock or os.getenv("KCLI_MOCK_GITHUB", "0").lower() in ("1", "true") or ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("GITHUB_TOKEN"))
1796
+ client = GitHubClient(repo_dir=target_dir, mock_mode=is_mock) if GitHubClient else None
1797
+ mgr = PRLifecycleManager(client=client, repo_dir=target_dir) if PRLifecycleManager else None
1798
+
1799
+ if mgr is None:
1800
+ if json_output:
1801
+ typer.echo(json.dumps({"error": "PRLifecycleManager module not available"}))
1802
+ else:
1803
+ console.print("[bold red]Error:[/bold red] PRLifecycleManager module is not available.")
1804
+ raise typer.Exit(code=1)
1805
+
1806
+ driver = LLMDriver(model_name=model or "qwen2.5-coder:1.5b", mock_mode=is_mock)
1807
+ try:
1808
+ res = mgr.fix_pr(
1809
+ pr_number=pr_num,
1810
+ llm_driver=driver,
1811
+ auto_push=auto_push,
1812
+ )
1813
+ except Exception as ex:
1814
+ if json_output:
1815
+ typer.echo(json.dumps({"error": f"Failed to fix PR #{pr_num}: {ex}"}))
1816
+ else:
1817
+ console.print(f"[bold red]✘ Failed to fix PR #{pr_num}:[/bold red] {ex}")
1818
+ raise typer.Exit(code=1)
1819
+
1820
+ if json_output:
1821
+ typer.echo(json.dumps(res.to_dict(), indent=2))
1822
+ return
1823
+
1824
+ if res.success:
1825
+ console.print(f"[bold green]✔ Fixed PR #{pr_num} successfully![/bold green]")
1826
+ console.print(f" • Branch: {res.branch}")
1827
+ console.print(f" • Files modified: {', '.join(res.fixes_applied) if res.fixes_applied else 'None'}")
1828
+ if res.commit_sha:
1829
+ console.print(f" • Commit: {res.commit_sha}")
1830
+ if res.pushed:
1831
+ console.print(" • Remote push: [bold green]✔ Pushed to origin[/bold green]")
1832
+ else:
1833
+ console.print(f"[bold red]✘ Failed to fix PR #{pr_num}: {res.error_message}[/bold red]")
1834
+ raise typer.Exit(code=1)
1835
+
1836
+
1837
+ @pr_app.command(name="merge", help="Merge pull request upon CI and verification checks passing.")
1838
+ def pr_merge_cmd(
1839
+ pr_num: int = typer.Argument(..., help="Pull request number."),
1840
+ method: str = typer.Option("squash", "--method", help="Merge strategy: squash, rebase, merge."),
1841
+ require_ci: bool = typer.Option(True, "--require-ci/--no-require-ci", help="Require CI check runs to pass before merging."),
1842
+ dir: str = typer.Option(".", "--dir", "-d", help="Repository workspace directory."),
1843
+ mock: bool = typer.Option(False, "--mock", help="Use mock GitHub client for offline testing."),
1844
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1845
+ ):
1846
+ target_dir = Path(dir).resolve()
1847
+ is_mock = mock or os.getenv("KCLI_MOCK_GITHUB", "0").lower() in ("1", "true") or ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("GITHUB_TOKEN"))
1848
+ client = GitHubClient(repo_dir=target_dir, mock_mode=is_mock) if GitHubClient else None
1849
+ mgr = PRLifecycleManager(client=client, repo_dir=target_dir) if PRLifecycleManager else None
1850
+
1851
+ if mgr is None:
1852
+ if json_output:
1853
+ typer.echo(json.dumps({"error": "PRLifecycleManager module not available"}))
1854
+ else:
1855
+ console.print("[bold red]Error:[/bold red] PRLifecycleManager module is not available.")
1856
+ raise typer.Exit(code=1)
1857
+
1858
+ try:
1859
+ ok = mgr.auto_merge_pr(
1860
+ pr_number=pr_num,
1861
+ require_ci_pass=require_ci,
1862
+ merge_method=method,
1863
+ )
1864
+ except Exception as ex:
1865
+ if json_output:
1866
+ typer.echo(json.dumps({"error": f"Failed to merge PR #{pr_num}: {ex}"}))
1867
+ else:
1868
+ console.print(f"[bold red]✘ Failed to merge PR #{pr_num}:[/bold red] {ex}")
1869
+ raise typer.Exit(code=1)
1870
+
1871
+ if json_output:
1872
+ typer.echo(json.dumps({"pr_number": pr_num, "merged": ok, "method": method}, indent=2))
1873
+ return
1874
+
1875
+ if ok:
1876
+ console.print(f"[bold green]✔ Successfully merged PR #{pr_num} using '{method}' strategy.[/bold green]")
1877
+ else:
1878
+ console.print(f"[bold red]✘ Failed to merge PR #{pr_num}. Ensure CI checks are passing and merge requirements are met.[/bold red]")
1879
+ raise typer.Exit(code=1)
1880
+
1881
+
1882
+ # ==============================================================================
1883
+ # Model Context Protocol Commands (k-cli mcp ...)
1884
+ # ==============================================================================
1885
+
1886
+ mcp_app = typer.Typer(
1887
+ name="mcp",
1888
+ help="Manage, inspect, and test Model Context Protocol (MCP) servers and tools.",
1889
+ add_completion=False,
1890
+ )
1891
+
1892
+
1893
+ @mcp_app.command(name="list", help="List configured MCP servers.")
1894
+ def mcp_list_subcmd(
1895
+ config: Optional[str] = typer.Option(None, "--config", help="Path to mcp.json config."),
1896
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1897
+ ):
1898
+ mgr = MCPManager(config_path=config) if MCPManager else None
1899
+ if mgr is None:
1900
+ if json_output:
1901
+ typer.echo(json.dumps({"error": "MCPManager not available", "servers": []}))
1902
+ else:
1903
+ console.print("[bold red]Error:[/bold red] MCPManager module is not available.")
1904
+ raise typer.Exit(code=1)
1905
+
1906
+ servers = mgr.list_servers()
1907
+ if json_output:
1908
+ typer.echo(json.dumps(servers, indent=2))
1909
+ return
1910
+
1911
+ table = Table(title="Configured Model Context Protocol (MCP) Servers", box=None)
1912
+ table.add_column("Server Name", style="bold cyan")
1913
+ table.add_column("Transport", style="magenta")
1914
+ table.add_column("Command / URL", style="white")
1915
+ table.add_column("Status", style="bold")
1916
+ table.add_column("Tools", justify="right")
1917
+ table.add_column("Resources", justify="right")
1918
+
1919
+ if not servers:
1920
+ console.print("[yellow]No MCP servers configured yet.[/yellow]")
1921
+ console.print("Add one using: [italic]k-cli mcp add github npx -a '-y @modelcontextprotocol/server-github'[/italic]\n")
1922
+ return
1923
+
1924
+ for s in servers:
1925
+ status_style = "green" if s["connected"] else ("yellow" if s["disabled"] else "dim")
1926
+ status_text = f"[{status_style}]{s['status']}[/{status_style}]"
1927
+ table.add_row(
1928
+ s["name"],
1929
+ s["transport"],
1930
+ s["command"],
1931
+ status_text,
1932
+ str(s["tool_count"]),
1933
+ str(s["resource_count"]),
1934
+ )
1935
+ console.print(table)
1936
+
1937
+
1938
+ @mcp_app.command(name="add", help="Add or update an MCP server configuration.")
1939
+ def mcp_add_subcmd(
1940
+ name: str = typer.Argument(..., help="Server identifier name."),
1941
+ command: str = typer.Argument(..., help="Command executable (e.g. npx, python, node)."),
1942
+ args: Optional[List[str]] = typer.Argument(None, help="Command arguments."),
1943
+ extra_args: Optional[str] = typer.Option(None, "--args", "-a", help="Arguments as a string or JSON list."),
1944
+ env: Optional[str] = typer.Option(None, "--env", "-e", help="JSON string of environment variables."),
1945
+ url: Optional[str] = typer.Option(None, "--url", "-u", help="URL for SSE/HTTP transport."),
1946
+ transport: str = typer.Option("stdio", "--transport", "-t", help="Transport type (stdio, sse, http)."),
1947
+ config: Optional[str] = typer.Option(None, "--config", help="Path to mcp.json config."),
1948
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1949
+ ):
1950
+ mgr = MCPManager(config_path=config, auto_load=True) if MCPManager else None
1951
+ if mgr is None:
1952
+ if json_output:
1953
+ typer.echo(json.dumps({"error": "MCPManager not available", "success": False}))
1954
+ else:
1955
+ console.print("[bold red]Error:[/bold red] MCPManager module is not available.")
1956
+ raise typer.Exit(code=1)
1957
+
1958
+ env_dict = {}
1959
+ if env:
1960
+ try:
1961
+ env_dict = json.loads(env)
1962
+ except Exception:
1963
+ pass
1964
+
1965
+ parsed_args = list(args) if args else []
1966
+ if extra_args:
1967
+ try:
1968
+ val = json.loads(extra_args)
1969
+ if isinstance(val, list):
1970
+ parsed_args.extend([str(x) for x in val])
1971
+ else:
1972
+ parsed_args.extend(shlex.split(str(extra_args)))
1973
+ except Exception:
1974
+ parsed_args.extend(shlex.split(str(extra_args)))
1975
+
1976
+ cfg = MCPServerConfig(
1977
+ name=name,
1978
+ command=command,
1979
+ args=parsed_args,
1980
+ env=env_dict,
1981
+ url=url,
1982
+ transport=transport,
1983
+ )
1984
+ mgr.add_server(name, cfg, save=True)
1985
+
1986
+ if json_output:
1987
+ typer.echo(json.dumps({"success": True, "name": name, "server": cfg.to_dict()}, indent=2))
1988
+ return
1989
+
1990
+ console.print(f"[bold green]✔ Server '{name}' successfully registered and saved to configuration.[/bold green]")
1991
+
1992
+
1993
+ @mcp_app.command(name="remove", help="Remove an MCP server from configuration.")
1994
+ def mcp_remove_subcmd(
1995
+ name: str = typer.Argument(..., help="Server identifier name to remove."),
1996
+ config: Optional[str] = typer.Option(None, "--config", help="Path to mcp.json config."),
1997
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
1998
+ ):
1999
+ mgr = MCPManager(config_path=config, auto_load=True) if MCPManager else None
2000
+ if mgr is None:
2001
+ if json_output:
2002
+ typer.echo(json.dumps({"error": "MCPManager not available", "success": False}))
2003
+ else:
2004
+ console.print("[bold red]Error:[/bold red] MCPManager module is not available.")
2005
+ raise typer.Exit(code=1)
2006
+
2007
+ ok = mgr.remove_server(name, save=True)
2008
+ if json_output:
2009
+ typer.echo(json.dumps({"success": ok, "name": name}, indent=2))
2010
+ return
2011
+
2012
+ if ok:
2013
+ console.print(f"[bold green]✔ Server '{name}' removed successfully.[/bold green]")
2014
+ else:
2015
+ console.print(f"[bold yellow]Server '{name}' was not found in configuration.[/bold yellow]")
2016
+
2017
+
2018
+ @mcp_app.command(name="tools", help="List discovered MCP tools.")
2019
+ def mcp_tools_subcmd(
2020
+ server: Optional[str] = typer.Option(None, "--server", "-s", help="Filter tools by server name."),
2021
+ config: Optional[str] = typer.Option(None, "--config", help="Path to mcp.json config."),
2022
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
2023
+ ):
2024
+ mgr = MCPManager(config_path=config) if MCPManager else None
2025
+ if mgr is None:
2026
+ if json_output:
2027
+ typer.echo(json.dumps({"error": "MCPManager not available", "tools": []}))
2028
+ else:
2029
+ console.print("[bold red]Error:[/bold red] MCPManager module is not available.")
2030
+ raise typer.Exit(code=1)
2031
+
2032
+ tools = mgr.list_tools(server_name=server)
2033
+ if json_output:
2034
+ typer.echo(json.dumps([t.to_dict() for t in tools], indent=2))
2035
+ return
2036
+
2037
+ if not tools:
2038
+ console.print(f"[yellow]No tools discovered on {'server ' + server if server else 'any active server'}.[/yellow]")
2039
+ return
2040
+
2041
+ table = Table(title=f"Discovered MCP Tools ({len(tools)})", box=None)
2042
+ table.add_column("Tool Name", style="bold cyan")
2043
+ table.add_column("Server", style="magenta")
2044
+ table.add_column("Description", style="white")
2045
+ for t in tools:
2046
+ table.add_row(t.name, t.server_name or "default", t.description)
2047
+ console.print(table)
2048
+
2049
+
2050
+ @mcp_app.command(name="call", help="Call an MCP tool with JSON arguments.")
2051
+ def mcp_call_subcmd(
2052
+ tool_name: str = typer.Argument(..., help="Tool name to execute."),
2053
+ json_args: str = typer.Argument("{}", help="JSON string arguments for tool call."),
2054
+ server: Optional[str] = typer.Option(None, "--server", "-s", help="Server name if tool is ambiguous."),
2055
+ config: Optional[str] = typer.Option(None, "--config", help="Path to mcp.json config."),
2056
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
2057
+ ):
2058
+ mgr = MCPManager(config_path=config) if MCPManager else None
2059
+ if mgr is None:
2060
+ if json_output:
2061
+ typer.echo(json.dumps({"error": "MCPManager not available"}))
2062
+ else:
2063
+ console.print("[bold red]Error:[/bold red] MCPManager module is not available.")
2064
+ raise typer.Exit(code=1)
2065
+
2066
+ parsed_args: Dict[str, Any] = {}
2067
+ if json_args:
2068
+ try:
2069
+ parsed_args = json.loads(json_args)
2070
+ except Exception as pe:
2071
+ if json_output:
2072
+ typer.echo(json.dumps({"error": f"Invalid JSON arguments: {pe}"}))
2073
+ else:
2074
+ console.print(f"[bold red]Error parsing JSON arguments:[/bold red] {pe}")
2075
+ raise typer.Exit(code=1)
2076
+
2077
+ try:
2078
+ result = mgr.call_tool(tool_name, arguments=parsed_args, server_name=server)
2079
+ if json_output:
2080
+ typer.echo(json.dumps(result.to_dict(), indent=2))
2081
+ return
2082
+
2083
+ console.print(f"[bold green]Tool '{tool_name}' Output:[/bold green]")
2084
+ console.print(result.text if result.text else json.dumps(result.raw, indent=2))
2085
+ except Exception as ce:
2086
+ if json_output:
2087
+ typer.echo(json.dumps({"error": str(ce), "tool": tool_name}))
2088
+ else:
2089
+ console.print(f"[bold red]Tool execution error:[/bold red] {ce}")
2090
+ raise typer.Exit(code=1)
2091
+
2092
+
2093
+ @mcp_app.command(name="test", help="Test connection to an MCP server.")
2094
+ def mcp_test_subcmd(
2095
+ name: Optional[str] = typer.Argument(None, help="Server identifier name to test."),
2096
+ config: Optional[str] = typer.Option(None, "--config", help="Path to mcp.json config."),
2097
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
2098
+ ):
2099
+ mgr = MCPManager(config_path=config) if MCPManager else None
2100
+ if mgr is None:
2101
+ if json_output:
2102
+ typer.echo(json.dumps({"error": "MCPManager not available", "success": False}))
2103
+ else:
2104
+ console.print("[bold red]Error:[/bold red] MCPManager module is not available.")
2105
+ raise typer.Exit(code=1)
2106
+
2107
+ target_name = name or (list(mgr.server_configs.keys())[0] if mgr.server_configs else None)
2108
+ if not target_name:
2109
+ if json_output:
2110
+ typer.echo(json.dumps({"error": "No MCP servers configured to test", "success": False}))
2111
+ else:
2112
+ console.print("[bold red]Error:[/bold red] No MCP servers configured to test.")
2113
+ raise typer.Exit(code=1)
2114
+
2115
+ res = mcp_test_connection(target_name, manager=mgr)
2116
+ if json_output:
2117
+ typer.echo(json.dumps(res, indent=2))
2118
+ return
2119
+
2120
+ if res["success"]:
2121
+ console.print(f"[bold green]✔ Connected to '{target_name}' in {res['duration_ms']}ms![/bold green]")
2122
+ console.print(f" • Tools Discovered: {len(res.get('tools', []))}")
2123
+ for t in res.get("tools", []):
2124
+ console.print(f" - [bold white]{t.get('name')}[/bold white]: {t.get('description', '')}")
2125
+ else:
2126
+ console.print(f"[bold red]✘ Connection to '{target_name}' failed: {res['error']}[/bold red]")
2127
+ raise typer.Exit(code=1)
2128
+
2129
+
2130
+ # ==============================================================================
2131
+ # Task Deduplication Commands (k-cli dedup ...)
2132
+ # ==============================================================================
2133
+
2134
+ dedup_app = typer.Typer(
2135
+ name="dedup",
2136
+ help="Check and detect duplicate issues, tasks, and existing code.",
2137
+ add_completion=False,
2138
+ )
2139
+
2140
+
2141
+ @dedup_app.command(name="check", help="Check if a task or query matches existing commits or symbols.")
2142
+ def dedup_check_cmd(
2143
+ query_or_issue: str = typer.Argument(..., help="Query string, issue title, or requested task."),
2144
+ dir: str = typer.Option(".", "--dir", "-d", help="Repository workspace root directory."),
2145
+ threshold: float = typer.Option(0.65, "--threshold", "-t", help="Confidence threshold (0.0 to 1.0) to mark as duplicate."),
2146
+ depth: int = typer.Option(50, "--depth", help="Git commit history depth to inspect."),
2147
+ json_output: bool = typer.Option(False, "--json", help="Output results in JSON format."),
2148
+ ):
2149
+ target_dir = Path(dir).resolve()
2150
+ engine = DedupEngine(repo_path=str(target_dir), duplicate_threshold=threshold, git_depth=depth) if DedupEngine else None
2151
+
2152
+ if engine is None:
2153
+ if json_output:
2154
+ typer.echo(json.dumps({"error": "DedupEngine module not available"}))
2155
+ else:
2156
+ console.print("[bold red]Error:[/bold red] DedupEngine module is not available.")
2157
+ raise typer.Exit(code=1)
2158
+
2159
+ match = engine.scan_for_duplicate(query=query_or_issue)
2160
+
2161
+ if json_output:
2162
+ typer.echo(json.dumps(match.to_dict() if match else {"is_duplicate": False, "confidence": 0.0}, indent=2))
2163
+ return
2164
+
2165
+ if match and match.is_duplicate:
2166
+ console.print(Panel(
2167
+ f"[bold yellow]⚠ POTENTIAL DUPLICATE DETECTED ({match.confidence:.1%} confidence)[/bold yellow]\n\n"
2168
+ f"[bold]Rationale:[/bold] {match.explanation}\n"
2169
+ f"• [cyan]Match Type:[/cyan] {match.match_type.upper()}\n"
2170
+ + (f"• [cyan]Existing Commit:[/cyan] {match.existing_commit[:10]}\n" if match.existing_commit else "")
2171
+ + (f"• [cyan]File Location:[/cyan] {match.file_path}" + (f" (lines {match.line_range[0]}-{match.line_range[1]})" if match.line_range else "") + "\n" if match.file_path else ""),
2172
+ title="Deduplication Warning",
2173
+ border_style="yellow",
2174
+ ))
2175
+ else:
2176
+ conf_str = f" ({match.confidence:.1%} max match)" if match else ""
2177
+ console.print(f"[bold green]✔ Unique: No duplicate commits or symbols found{conf_str}. Safe to proceed.[/bold green]")
2178
+
2179
+
2180
+ @app.command(name="commit", help="Generate AST-grounded conventional commit message and stage/commit changes.")
2181
+ def commit_command(
2182
+ push: bool = typer.Option(False, "--push", help="Push commit to remote branch after creating it."),
2183
+ all: bool = typer.Option(True, "--all", "-a", help="Stage all uncommitted working tree changes."),
2184
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Optional model identifier for AI-assisted refinement."),
2185
+ repo: str = typer.Option(".", "--repo", "-r", help="Repository path."),
2186
+ json_output: bool = typer.Option(False, "--json", help="Output proposal in JSON format without committing."),
2187
+ ):
2188
+ """Generates an intelligent conventional commit from AST diffs and stages/commits changes."""
2189
+ target_path = Path(repo).resolve()
2190
+ engine = SmartGitEngine(repo_path=str(target_path)) if SmartGitEngine else None
2191
+
2192
+ if engine is None:
2193
+ console.print("[bold red]Error:[/bold red] SmartGitEngine module is not available.")
2194
+ raise typer.Exit(code=1)
2195
+
2196
+ if not engine.is_git_repo():
2197
+ console.print(f"[bold red]Error:[/bold red] '{target_path}' is not a valid git repository.")
2198
+ raise typer.Exit(code=1)
2199
+
2200
+ proposal = engine.generate_smart_commit(staged_only=not all, model=model)
2201
+
2202
+ if json_output:
2203
+ typer.echo(json.dumps(proposal.to_dict(), indent=2))
2204
+ return
2205
+
2206
+ if not proposal.files_changed:
2207
+ console.print("[yellow]Working tree is clean. No uncommitted modifications found.[/yellow]")
2208
+ return
2209
+
2210
+ # Render proposal preview
2211
+ console.print(Panel(
2212
+ f"[bold cyan]Type:[/bold cyan] {proposal.commit_type.upper()}"
2213
+ + (f" | [magenta]Scope:[/magenta] {proposal.scope}" if proposal.scope else "")
2214
+ + f"\n[bold green]Subject:[/bold green] {proposal.subject}\n\n"
2215
+ f"[bold]Body:[/bold]\n{proposal.body}\n\n"
2216
+ f"[dim]{proposal.raw_diff_summary}[/dim]",
2217
+ title="✨ Smart Conventional Commit Proposal",
2218
+ border_style="cyan",
2219
+ ))
2220
+
2221
+ # Auto-stage and commit
2222
+ success = engine.auto_stage_and_commit(message=proposal.full_message, push=push, all_files=all)
2223
+ if success:
2224
+ console.print(f"[bold green]✔ Changes committed successfully![/bold green]" + (" Pushed to remote." if push else ""))
2225
+ else:
2226
+ console.print("[bold red]✘ Git commit failed.[/bold red]")
2227
+ raise typer.Exit(code=1)
2228
+
2229
+
2230
+ security_app = typer.Typer(
2231
+ name="security",
2232
+ help="Fast AST & Regex security scanner and surgical auto-healer.",
2233
+ add_completion=False,
2234
+ )
2235
+
2236
+
2237
+ @security_app.command(name="scan", help="Scan repository for hardcoded secrets, SQLi, ReDoS, and unsafe execution.")
2238
+ def security_scan_command(
2239
+ repo: str = typer.Option(".", "--repo", "-r", help="Repository path to scan."),
2240
+ json_output: bool = typer.Option(False, "--json", help="Output findings in JSON format."),
2241
+ ):
2242
+ """Scans codebase for security vulnerabilities with AST & regex analysis."""
2243
+ target_path = Path(repo).resolve()
2244
+ healer = SecurityHealer(repo_path=str(target_path)) if SecurityHealer else None
2245
+
2246
+ if healer is None:
2247
+ if json_output:
2248
+ typer.echo(json.dumps({"error": "SecurityHealer module is not available"}))
2249
+ else:
2250
+ console.print("[bold red]Error:[/bold red] SecurityHealer module is not available.")
2251
+ raise typer.Exit(code=1)
2252
+
2253
+ report = healer.scan_repository()
2254
+
2255
+ if json_output:
2256
+ typer.echo(report.to_json(indent=2))
2257
+ return
2258
+
2259
+ if not report.findings:
2260
+ console.print(f"[bold green]✔ Clean Workspace: 0 security vulnerabilities found across {report.scanned_files_count} files ({report.scan_duration_seconds:.2f}s).[/bold green]")
2261
+ return
2262
+
2263
+ table = Table(title=f"🛡️ Security Vulnerability Scan ({report.total_findings} findings)", box=None)
2264
+ table.add_column("ID", style="bold cyan")
2265
+ table.add_column("Severity", style="bold")
2266
+ table.add_column("Type", style="magenta")
2267
+ table.add_column("File:Line", style="white")
2268
+ table.add_column("CVSS", justify="right", style="yellow")
2269
+ table.add_column("CWE", style="dim")
2270
+ table.add_column("Description", style="white")
2271
+
2272
+ for f in report.findings:
2273
+ sev_style = "bold red" if f.severity in ("CRITICAL", "HIGH") else "bold yellow"
2274
+ table.add_row(
2275
+ f.id,
2276
+ f"[{sev_style}]{f.severity}[/{sev_style}]",
2277
+ f.vuln_type,
2278
+ f"{f.file_path}:{f.line_number}",
2279
+ str(f.cvss_score),
2280
+ f.cwe_id,
2281
+ f.description[:60] + ("..." if len(f.description) > 60 else ""),
2282
+ )
2283
+
2284
+ console.print(table)
2285
+ console.print(f"\n[dim]Files scanned: {report.scanned_files_count} | Duration: {report.scan_duration_seconds:.2f}s | Max CVSS: {report.max_cvss_score}[/dim]")
2286
+ console.print("[cyan]Run [bold]k-cli security heal --all[/bold] to automatically remediate detected vulnerabilities.[/cyan]\n")
2287
+
2288
+
2289
+ @security_app.command(name="heal", help="Auto-heal detected security vulnerabilities with AST & test verification.")
2290
+ def security_heal_command(
2291
+ vuln_id: Optional[str] = typer.Option(None, "--vuln-id", "-i", help="Specific vulnerability ID to heal."),
2292
+ heal_all: bool = typer.Option(False, "--all", "-a", help="Heal all detected vulnerabilities."),
2293
+ repo: str = typer.Option(".", "--repo", "-r", help="Repository path."),
2294
+ json_output: bool = typer.Option(False, "--json", help="Output healing results in JSON format."),
2295
+ ):
2296
+ """Surgically remediates detected vulnerabilities with ground-truth verification."""
2297
+ target_path = Path(repo).resolve()
2298
+ healer = SecurityHealer(repo_path=str(target_path)) if SecurityHealer else None
2299
+
2300
+ if healer is None:
2301
+ if json_output:
2302
+ typer.echo(json.dumps({"error": "SecurityHealer module is not available"}))
2303
+ else:
2304
+ console.print("[bold red]Error:[/bold red] SecurityHealer module is not available.")
2305
+ raise typer.Exit(code=1)
2306
+
2307
+ if not vuln_id and not heal_all:
2308
+ console.print("[bold red]Error:[/bold red] Please specify either [bold]--vuln-id <id>[/bold] or [bold]--all[/bold].")
2309
+ raise typer.Exit(code=1)
2310
+
2311
+ results: List[VulnerabilityHealResult] = []
2312
+ if vuln_id:
2313
+ res = healer.auto_heal_vulnerability(vuln_id=vuln_id)
2314
+ results.append(res)
2315
+ elif heal_all:
2316
+ results = healer.heal_all_vulnerabilities()
2317
+
2318
+ if json_output:
2319
+ typer.echo(json.dumps([r.to_dict() for r in results], indent=2))
2320
+ return
2321
+
2322
+ if not results:
2323
+ console.print("[yellow]No vulnerabilities were targeted or found for healing.[/yellow]")
2324
+ return
2325
+
2326
+ for r in results:
2327
+ if r.success:
2328
+ console.print(Panel(
2329
+ f"[bold green]✔ Successfully Healed {r.vuln_id}[/bold green] in [cyan]{r.file_path}[/cyan]\n\n"
2330
+ f"• AST Syntax Verified: [green]✔[/green]\n"
2331
+ f"• Test Suite Passed: [green]✔[/green]\n"
2332
+ f"• Re-scan Clean: [green]✔[/green]\n\n"
2333
+ + (f"[dim]Applied Diff:\n{r.diff}[/dim]" if r.diff else ""),
2334
+ title=f"Remediation Success: {r.vuln_id}",
2335
+ border_style="green",
2336
+ ))
2337
+ else:
2338
+ console.print(Panel(
2339
+ f"[bold red]✘ Failed to heal {r.vuln_id}[/bold red] in [cyan]{r.file_path or 'unknown'}[/cyan]\n\n"
2340
+ f"[bold]Reason:[/bold] {r.error_message}",
2341
+ title=f"Remediation Failed: {r.vuln_id}",
2342
+ border_style="red",
2343
+ ))
2344
+
2345
+
2346
+ # =============================================================================
2347
+ # Universal AI Models Hub Commands (`k-cli models`)
2348
+ # =============================================================================
2349
+ models_app = typer.Typer(help="Universal AI Model Hub, local SLMs, and benchmarks.")
2350
+
2351
+
2352
+ @models_app.command("list")
2353
+ def models_list(
2354
+ provider: Optional[str] = typer.Option(None, "--provider", "-p", help="Filter by provider (ollama, gemini, anthropic, openai, groq, etc.)."),
2355
+ local_only: bool = typer.Option(False, "--local", "-l", help="List only local models."),
2356
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2357
+ ):
2358
+ """Lists available local and cloud AI models in the Universal Model Hub."""
2359
+ hub = ModelHub()
2360
+ models = hub.list_models(provider=provider, local_only=local_only)
2361
+
2362
+ if json_output:
2363
+ print(json.dumps([m.to_dict() for m in models], indent=2))
2364
+ return
2365
+
2366
+ table = Table(title="🤖 K-CLI Universal Model Hub", border_style="cyan", header_style="bold magenta")
2367
+ table.add_column("Model ID", style="bold cyan")
2368
+ table.add_column("Provider", style="yellow")
2369
+ table.add_column("Type", style="green")
2370
+ table.add_column("Context", justify="right")
2371
+ table.add_column("Status / Installed", justify="center")
2372
+ table.add_column("Description", style="dim")
2373
+
2374
+ for m in models:
2375
+ type_str = "Local SLM" if m.is_local else "Cloud LLM"
2376
+ status_str = "[bold green]✔ Installed[/bold green]" if m.is_installed else ("[dim]Cloud Available[/dim]" if not m.is_local else "[yellow]Pull Available[/yellow]")
2377
+ table.add_row(
2378
+ m.id,
2379
+ m.provider.value.upper(),
2380
+ type_str,
2381
+ f"{m.context_window // 1024}k",
2382
+ status_str,
2383
+ m.description[:45] + ("..." if len(m.description) > 45 else ""),
2384
+ )
2385
+
2386
+ console.print(table)
2387
+
2388
+
2389
+ @models_app.command("test")
2390
+ def models_test(
2391
+ model: str = typer.Argument(..., help="Model identifier to test (e.g. qwen2.5-coder:1.5b, gemini-2.0-flash)."),
2392
+ prompt: str = typer.Option("Write a Python function to compute fibonacci numbers iteratively.", "--prompt", "-p"),
2393
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2394
+ ):
2395
+ """Benchmarks model latency, throughput (tok/s), and memory RSS consumption."""
2396
+ hub = ModelHub()
2397
+ console.print(f"[bold cyan]Running benchmark for model [yellow]{model}[/yellow]...[/bold cyan]")
2398
+ res = hub.benchmark_model(model_name=model, prompt=prompt)
2399
+
2400
+ if json_output:
2401
+ print(json.dumps(res.to_dict(), indent=2))
2402
+ return
2403
+
2404
+ if res.success:
2405
+ console.print(Panel(
2406
+ f"[bold green]✔ Benchmark Succeeded[/bold green]\n\n"
2407
+ f"• [bold]Model:[/bold] {res.model_id} ({res.provider})\n"
2408
+ f"• [bold]Throughput:[/bold] [bold cyan]{res.tokens_per_second:.1f} tok/s[/bold cyan]\n"
2409
+ f"• [bold]Time to First Token (TTFT):[/bold] {res.time_to_first_token:.3f}s\n"
2410
+ f"• [bold]Total Duration:[/bold] {res.duration_seconds:.3f}s ({res.tokens_generated} tokens)\n"
2411
+ f"• [bold]Process RAM RSS:[/bold] {res.ram_rss_mb:.1f} MB\n\n"
2412
+ f"[dim]Output Preview:\n{res.sample_output}[/dim]",
2413
+ title="Model Benchmark Telemetry",
2414
+ border_style="green",
2415
+ ))
2416
+ else:
2417
+ console.print(Panel(
2418
+ f"[bold red]✘ Benchmark Failed[/bold red]\n\n[bold]Error:[/bold] {res.error_message}",
2419
+ title="Benchmark Error",
2420
+ border_style="red",
2421
+ ))
2422
+
2423
+
2424
+ @models_app.command("pull")
2425
+ def models_pull(
2426
+ model: str = typer.Argument(..., help="Local model name to pull via Ollama (e.g. qwen2.5-coder:7b)."),
2427
+ ):
2428
+ """Pulls a local model onto the local machine via Ollama daemon."""
2429
+ hub = ModelHub()
2430
+ console.print(f"[bold cyan]Pulling local model [yellow]{model}[/yellow]...[/bold cyan]")
2431
+ success = hub.pull_model(model_name=model, stream_callback=lambda msg: console.print(msg, end=""))
2432
+ if success:
2433
+ console.print(f"\n[bold green]✔ Model {model} pulled and ready for local inference.[/bold green]")
2434
+ else:
2435
+ console.print(f"\n[bold red]✘ Failed pulling model {model}. Ensure Ollama daemon is running at http://localhost:11434[/bold red]")
2436
+
2437
+
2438
+ @models_app.command("providers")
2439
+ def models_providers(
2440
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2441
+ ):
2442
+ """Inspects configuration and active API credentials across all supported providers."""
2443
+ hub = ModelHub()
2444
+ statuses = {}
2445
+ for p in ModelProvider:
2446
+ statuses[p.value] = hub.is_provider_configured(p)
2447
+
2448
+ if json_output:
2449
+ print(json.dumps(statuses, indent=2))
2450
+ return
2451
+
2452
+ table = Table(title="⚡ AI Model Provider Status", border_style="cyan", header_style="bold magenta")
2453
+ table.add_column("Provider", style="bold yellow")
2454
+ table.add_column("Type", style="cyan")
2455
+ table.add_column("Status", justify="center")
2456
+ table.add_column("Configuration Requirement", style="dim")
2457
+
2458
+ for p in ModelProvider:
2459
+ is_ready = statuses[p.value]
2460
+ status_str = "[bold green]✔ Ready[/bold green]" if is_ready else "[dim red]Not Configured[/dim red]"
2461
+ prov_type = "Local SLM" if p in (ModelProvider.OLLAMA, ModelProvider.LLAMACPP, ModelProvider.NATIVE) else "Cloud API"
2462
+ req_str = "http://localhost:11434" if p == ModelProvider.OLLAMA else (f"export {p.value.upper()}_API_KEY" if p != ModelProvider.MOCK else "None (Built-in)")
2463
+ table.add_row(p.value.upper(), prov_type, status_str, req_str)
2464
+
2465
+ console.print(table)
2466
+
2467
+
2468
+ @models_app.command("set-default", help="Set and persist the default AI model (e.g. 'k-cli models set-default claude-3-5-sonnet' or 'auto').")
2469
+ def models_set_default(
2470
+ model_name: str = typer.Argument(..., help="Model identifier to set as default (or 'auto' for adaptive intent routing)."),
2471
+ ):
2472
+ """Sets and saves the developer's default preferred AI model."""
2473
+ DevPreferencesManager.set_default_model(model_name)
2474
+ console.print(f"[bold green]✔ Default model successfully set to:[/bold green] [bold cyan]{model_name}[/bold cyan]")
2475
+ console.print("[dim]K-CLI will automatically route to this model when in default mode.[/dim]")
2476
+
2477
+
2478
+ @models_app.command("get-default", help="Display the currently active default AI model.")
2479
+ def models_get_default():
2480
+ """Prints the currently configured default AI model."""
2481
+ current = DevPreferencesManager.get_default_model()
2482
+ console.print(f"[bold cyan]Current Default Model:[/bold cyan] [bold green]{current}[/bold green]")
2483
+
2484
+
2485
+ # =============================================================================
2486
+ # GitHub Ecosystem Commands (`k-cli gh` / `k-cli issue` / `k-cli release`)
2487
+ # =============================================================================
2488
+ gh_app = typer.Typer(help="Complete GitHub ecosystem & autonomous issue solver.")
2489
+ issue_app = typer.Typer(help="Manage and autonomously solve GitHub issues.")
2490
+ release_app = typer.Typer(help="Manage GitHub releases & automated changelogs.")
2491
+ action_app = typer.Typer(help="Inspect GitHub Actions CI/CD runs & logs.")
2492
+ gist_app = typer.Typer(help="Create and manage GitHub Gists.")
2493
+
2494
+
2495
+ @issue_app.command("list")
2496
+ @gh_app.command("issues")
2497
+ def gh_issue_list(
2498
+ state: str = typer.Option("open", "--state", "-s", help="Issue state (open, closed, all)."),
2499
+ limit: int = typer.Option(30, "--limit", "-n", help="Max issues to return."),
2500
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2501
+ ):
2502
+ """Lists repository issues."""
2503
+ engine = GitHubEngine()
2504
+ issues = engine.list_issues(state=state, limit=limit)
2505
+
2506
+ if json_output:
2507
+ print(json.dumps([i.to_dict() for i in issues], indent=2))
2508
+ return
2509
+
2510
+ table = Table(title=f"🐙 GitHub Issues ({engine.owner}/{engine.repo})", border_style="cyan", header_style="bold magenta")
2511
+ table.add_column("#", justify="right", style="bold cyan")
2512
+ table.add_column("State", justify="center")
2513
+ table.add_column("Title", style="white")
2514
+ table.add_column("Author", style="dim yellow")
2515
+ table.add_column("Labels", style="dim green")
2516
+ table.add_column("Comments", justify="right")
2517
+
2518
+ for i in issues:
2519
+ state_badge = "[bold green]open[/bold green]" if i.state == "open" else "[dim red]closed[/dim red]"
2520
+ table.add_row(
2521
+ str(i.number),
2522
+ state_badge,
2523
+ i.title[:50],
2524
+ f"@{i.author}",
2525
+ ", ".join(i.labels[:3]),
2526
+ str(i.comments_count),
2527
+ )
2528
+ console.print(table)
2529
+
2530
+
2531
+ @issue_app.command("solve")
2532
+ @gh_app.command("solve")
2533
+ def gh_issue_solve(
2534
+ issue_number: int = typer.Argument(..., help="GitHub issue number to autonomously solve."),
2535
+ auto_pr: bool = typer.Option(True, "--auto-pr/--no-pr", help="Automatically create Pull Request once tests pass."),
2536
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2537
+ ):
2538
+ """Autonomously investigates, writes surgical fixes, verifies tests, and opens PR for an issue."""
2539
+ engine = GitHubEngine()
2540
+ console.print(f"[bold cyan]Autonomously solving GitHub issue [yellow]#{issue_number}[/yellow]...[/bold cyan]")
2541
+ res = engine.solve_issue(issue_number=issue_number, auto_pr=auto_pr)
2542
+
2543
+ if json_output:
2544
+ print(json.dumps(res.to_dict(), indent=2))
2545
+ return
2546
+
2547
+ if res.success:
2548
+ console.print(Panel(
2549
+ f"[bold green]✔ Successfully Solved Issue #{issue_number}[/bold green]\n\n"
2550
+ f"• [bold]Branch Created:[/bold] [cyan]{res.branch_name}[/cyan]\n"
2551
+ + (f"• [bold]Pull Request Opened:[/bold] [link={res.pr_url}]{res.pr_url}[/link]\n" if res.pr_url else "") +
2552
+ f"• [bold]Status:[/bold] {res.summary}",
2553
+ title=f"Issue #{issue_number} Resolved",
2554
+ border_style="green",
2555
+ ))
2556
+ else:
2557
+ console.print(Panel(
2558
+ f"[bold red]✘ Failed solving issue #{issue_number}[/bold red]\n\n[bold]Reason:[/bold] {res.error_message}",
2559
+ title=f"Issue #{issue_number} Unresolved",
2560
+ border_style="red",
2561
+ ))
2562
+
2563
+
2564
+ @release_app.command("list")
2565
+ @gh_app.command("releases")
2566
+ def gh_release_list(
2567
+ limit: int = typer.Option(10, "--limit", "-n", help="Max releases to return."),
2568
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2569
+ ):
2570
+ """Lists repository releases."""
2571
+ engine = GitHubEngine()
2572
+ releases = engine.list_releases(limit=limit)
2573
+
2574
+ if json_output:
2575
+ print(json.dumps([r.to_dict() for r in releases], indent=2))
2576
+ return
2577
+
2578
+ table = Table(title=f"🚀 GitHub Releases ({engine.owner}/{engine.repo})", border_style="cyan", header_style="bold magenta")
2579
+ table.add_column("Tag", style="bold cyan")
2580
+ table.add_column("Release Name", style="white")
2581
+ table.add_column("Type", justify="center")
2582
+ table.add_column("Published", style="dim")
2583
+ table.add_column("Assets", justify="right")
2584
+
2585
+ for r in releases:
2586
+ type_badge = "[yellow]pre-release[/yellow]" if r.prerelease else ("[dim]draft[/dim]" if r.draft else "[green]release[/green]")
2587
+ table.add_row(r.tag_name, r.name, type_badge, r.published_at[:10], str(len(r.assets)))
2588
+ console.print(table)
2589
+
2590
+
2591
+ @release_app.command("create")
2592
+ def gh_release_create(
2593
+ tag: str = typer.Argument(..., help="Release tag name (e.g. v1.0.0)."),
2594
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Release title name."),
2595
+ draft: bool = typer.Option(False, "--draft", help="Create as draft release."),
2596
+ prerelease: bool = typer.Option(False, "--prerelease", help="Create as prerelease."),
2597
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2598
+ ):
2599
+ """Creates a new GitHub release with automatically generated AST Conventional Changelog."""
2600
+ engine = GitHubEngine()
2601
+ console.print(f"[bold cyan]Synthesizing changelog and creating release [yellow]{tag}[/yellow]...[/bold cyan]")
2602
+ rel = engine.create_release(tag_name=tag, name=name, draft=draft, prerelease=prerelease)
2603
+
2604
+ if json_output:
2605
+ print(json.dumps(rel.to_dict(), indent=2))
2606
+ return
2607
+
2608
+ console.print(Panel(
2609
+ f"[bold green]✔ Created Release {rel.tag_name}[/bold green]\n\n"
2610
+ f"• [bold]Title:[/bold] {rel.name}\n"
2611
+ f"• [bold]URL:[/bold] [link={rel.html_url}]{rel.html_url}[/link]\n\n"
2612
+ f"[dim]Changelog Preview:\n{rel.body[:300]}...[/dim]",
2613
+ title=f"Release {tag} Published",
2614
+ border_style="green",
2615
+ ))
2616
+
2617
+
2618
+ @action_app.command("runs")
2619
+ @gh_app.command("actions")
2620
+ def gh_action_runs(
2621
+ limit: int = typer.Option(15, "--limit", "-n", help="Max runs to return."),
2622
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2623
+ ):
2624
+ """Lists GitHub Actions CI/CD workflow runs."""
2625
+ engine = GitHubEngine()
2626
+ runs = engine.list_workflow_runs(limit=limit)
2627
+
2628
+ if json_output:
2629
+ print(json.dumps([r.to_dict() for r in runs], indent=2))
2630
+ return
2631
+
2632
+ table = Table(title=f"⚡ GitHub Actions CI/CD Runs ({engine.owner}/{engine.repo})", border_style="cyan", header_style="bold magenta")
2633
+ table.add_column("Run ID", style="bold cyan")
2634
+ table.add_column("Workflow", style="white")
2635
+ table.add_column("Branch", style="yellow")
2636
+ table.add_column("Status", justify="center")
2637
+ table.add_column("Conclusion", justify="center")
2638
+ table.add_column("Created", style="dim")
2639
+
2640
+ for r in runs:
2641
+ conclusion_badge = "[bold green]success[/bold green]" if r.conclusion == "success" else (
2642
+ "[bold red]failure[/bold red]" if r.conclusion == "failure" else f"[dim]{r.conclusion or 'running'}[/dim]"
2643
+ )
2644
+ table.add_row(str(r.id), r.name, r.head_branch, r.status, conclusion_badge, r.created_at[:10])
2645
+ console.print(table)
2646
+
2647
+
2648
+ @gist_app.command("create")
2649
+ def gh_gist_create(
2650
+ file_path: str = typer.Argument(..., help="File path to publish as Gist."),
2651
+ description: str = typer.Option("Created via K-CLI", "--description", "-d"),
2652
+ public: bool = typer.Option(False, "--public", help="Make gist public."),
2653
+ ):
2654
+ """Creates a GitHub Gist snippet."""
2655
+ engine = GitHubEngine()
2656
+ p = Path(file_path).resolve()
2657
+ if not p.exists():
2658
+ console.print(f"[bold red]File not found: {file_path}[/bold red]")
2659
+ return
2660
+
2661
+ content = p.read_text(encoding="utf-8", errors="replace")
2662
+ url = engine.create_gist(files={p.name: content}, description=description, public=public)
2663
+ console.print(f"[bold green]✔ Gist created successfully:[/bold green] [link={url}]{url}[/link]")
2664
+
2665
+
2666
+ # =============================================================================
2667
+ # Local GitHub Hub & Trending Commands (`k-cli hub` / `k-cli trending`)
2668
+ # =============================================================================
2669
+
2670
+ @app.command(name="hub", help="Display local GitHub workstation summary, commits, and activity feed.")
2671
+ def hub_cmd(
2672
+ repo: str = typer.Option(".", "--repo", "-r", help="Repository path."),
2673
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2674
+ ):
2675
+ """Displays local repository workstation analytics and commit streams."""
2676
+ hub = LocalGitHubHub(repo_path=repo)
2677
+ summary = hub.get_summary()
2678
+
2679
+ if json_output:
2680
+ sys.stdout.write(json.dumps(summary.to_dict(), indent=2) + "\n")
2681
+ return
2682
+
2683
+ table = Table(title=f"🐙 Local GitHub Workstation ({summary.repo_name})", box=None)
2684
+ table.add_column("Property", style="cyan")
2685
+ table.add_column("Value", style="bold white")
2686
+ table.add_row("Branch Name", f"[bold green]{summary.branch_name}[/bold green]")
2687
+ table.add_row("Total Commits", str(summary.total_commits))
2688
+ table.add_row("Uncommitted Changes", f"[yellow]{summary.uncommitted_changes}[/yellow]" if summary.uncommitted_changes else "[green]0 (clean)[/green]")
2689
+ table.add_row("Contributors", str(summary.contributors_count))
2690
+ table.add_row("Repository Health", f"[bold green]{summary.health_score:.1f} / 100[/bold green]")
2691
+ console.print(table)
2692
+ console.print()
2693
+
2694
+ commits = hub.get_recent_commits(limit=5)
2695
+ if commits:
2696
+ c_table = Table(title="Recent Git Commit History", box=None)
2697
+ c_table.add_column("SHA", style="bold cyan")
2698
+ c_table.add_column("Author", style="magenta")
2699
+ c_table.add_column("Date", style="dim")
2700
+ c_table.add_column("Subject", style="white")
2701
+ for c in commits:
2702
+ c_table.add_row(c.short_sha, c.author, c.date, c.subject[:50])
2703
+ console.print(c_table)
2704
+
2705
+
2706
+ @app.command(name="trending", help="Discover trending GitHub repositories, AI agents, and developer tools.")
2707
+ def trending_cmd(
2708
+ language: Optional[str] = typer.Option(None, "--language", "-l", help="Filter by programming language (python, rust, go, etc.)."),
2709
+ query: Optional[str] = typer.Option(None, "--query", "-q", help="Filter by topic or query (ai-agent, tui, llm, etc.)."),
2710
+ limit: int = typer.Option(10, "--limit", "-n", help="Max repositories to show."),
2711
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2712
+ ):
2713
+ """Discovers trending GitHub repositories and AI agent frameworks."""
2714
+ engine = TrendingEngine()
2715
+ repos = engine.get_trending(language=language, query=query, limit=limit)
2716
+
2717
+ if json_output:
2718
+ sys.stdout.write(json.dumps([r.to_dict() for r in repos], indent=2) + "\n")
2719
+ return
2720
+
2721
+ table = Table(title="🔥 Trending on GitHub (Developer Workstation)", box=None)
2722
+ table.add_column("Repository", style="bold cyan")
2723
+ table.add_column("Language", style="magenta")
2724
+ table.add_column("Stars", style="yellow", justify="right")
2725
+ table.add_column("Today", style="bold green", justify="right")
2726
+ table.add_column("Description", style="white")
2727
+
2728
+ for r in repos:
2729
+ table.add_row(
2730
+ r.full_name,
2731
+ r.language,
2732
+ f"★ {r.stars:,}",
2733
+ f"+{r.stars_today}",
2734
+ r.description[:50] + ("..." if len(r.description) > 50 else ""),
2735
+ )
2736
+ console.print(table)
2737
+
2738
+
2739
+ rules_app = typer.Typer(name="rules", help="Manage custom developer instructions & workspace rules (.kclirules).")
2740
+
2741
+
2742
+ @rules_app.command(name="init", help="Create a .kclirules template in the current workspace.")
2743
+ def rules_init(
2744
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing rules file."),
2745
+ ):
2746
+ """Initializes a starter .kclirules template in the workspace root."""
2747
+ from k_cli.tools.rules import create_default_rules_file
2748
+ path = create_default_rules_file(force=force)
2749
+ console.print(f"[bold green]✔ Initialized custom rules template at:[/bold green] [cyan]{path}[/cyan]")
2750
+
2751
+
2752
+ @rules_app.command(name="get", help="Display currently active developer instructions & rules.")
2753
+ def rules_get():
2754
+ """Displays active developer rules from workspace or global settings."""
2755
+ from k_cli.tools.rules import load_project_rules
2756
+ rules_text = load_project_rules(".")
2757
+ if rules_text:
2758
+ console.print(Panel(rules_text, title="[bold green]Active Developer Rules & Instructions[/bold green]", border_style="green"))
2759
+ else:
2760
+ console.print("[yellow]No custom rules found in workspace. Run 'k-cli rules init' to create a .kclirules file.[/yellow]")
2761
+
2762
+
2763
+ @rules_app.command(name="set", help="Set custom global developer instructions.")
2764
+ def rules_set(
2765
+ instructions: str = typer.Argument(..., help="Custom system prompt instructions for the AI."),
2766
+ ):
2767
+ """Sets global developer instructions saved to ~/.kcli/rules.md."""
2768
+ from k_cli.tools.rules import set_global_rules
2769
+ path = set_global_rules(instructions)
2770
+ console.print(f"[bold green]✔ Successfully saved global developer instructions to:[/bold green] [cyan]{path}[/cyan]")
2771
+
2772
+
2773
+ # Mount sub-applications onto root CLI app
2774
+ app.add_typer(conflict_app, name="conflict")
2775
+ app.add_typer(pr_app, name="pr")
2776
+ app.add_typer(mcp_app, name="mcp")
2777
+ app.add_typer(dedup_app, name="dedup")
2778
+ app.add_typer(security_app, name="security")
2779
+ app.add_typer(models_app, name="models")
2780
+ app.add_typer(rules_app, name="rules")
2781
+ app.add_typer(gh_app, name="gh")
2782
+ app.add_typer(issue_app, name="issue")
2783
+ app.add_typer(release_app, name="release")
2784
+ app.add_typer(action_app, name="action")
2785
+ app.add_typer(gist_app, name="gist")
2786
+
2787
+ # =============================================================================
2788
+ # Credentials & API Keys Management
2789
+ # =============================================================================
2790
+ keys_app = typer.Typer(help="🔑 Manage, configure, test, and store API keys for all AI model providers.")
2791
+
2792
+ @keys_app.callback(invoke_without_command=True)
2793
+ def keys_main(ctx: typer.Context):
2794
+ """List all API key statuses and provide quick interactive setup."""
2795
+ if ctx.invoked_subcommand is None:
2796
+ from k_cli.core.credentials import CredentialsManager
2797
+ from rich.table import Table
2798
+
2799
+ statuses = CredentialsManager.get_key_statuses()
2800
+ table = Table(title="🔑 K-CLI API Credentials Vault", border_style="cyan")
2801
+ table.add_column("Provider / Key", style="bold cyan")
2802
+ table.add_column("Environment Variable", style="dim")
2803
+ table.add_column("Status", style="bold")
2804
+ table.add_column("Active Key", style="dim white")
2805
+
2806
+ for s in statuses:
2807
+ status_text = "[green]✔ Active[/green]" if s["active"] else "[yellow]○ Missing[/yellow]"
2808
+ masked_val = s["masked"] or "[dim]None[/dim]"
2809
+ table.add_row(s["label"], s["key"], status_text, masked_val)
2810
+
2811
+ console.print(table)
2812
+ console.print("\n[dim]To set a key: [/dim][bold cyan]k-cli keys set <KEY_NAME> <VALUE>[/bold cyan]")
2813
+ console.print("[dim]To test connections: [/dim][bold cyan]k-cli keys test[/bold cyan]\n")
2814
+
2815
+
2816
+ @keys_app.command(name="set", help="Set and store an API key (e.g. 'k-cli keys set GEMINI_API_KEY AIzaSy...').")
2817
+ def keys_set_cmd(
2818
+ key_name: str = typer.Argument(..., help="Environment variable name (e.g. GEMINI_API_KEY, OPENAI_API_KEY, GITHUB_TOKEN)."),
2819
+ key_val: str = typer.Argument(..., help="Secret API key value."),
2820
+ ):
2821
+ if not key_name.strip() or not key_val.strip():
2822
+ console.print("[bold red]✘ Key name and value cannot be empty.[/bold red]")
2823
+ raise typer.Exit(code=1)
2824
+ from k_cli.core.credentials import CredentialsManager
2825
+ CredentialsManager.set_key(key_name, key_val)
2826
+ console.print(f"[bold green]✔ Successfully saved and activated {key_name.upper()}![/bold green]")
2827
+ console.print(f"[dim]Stored persistently in ~/.kcli/credentials.env[/dim]")
2828
+
2829
+
2830
+ @keys_app.command(name="test", help="Test live connectivity for all configured provider keys.")
2831
+ def keys_test_cmd():
2832
+ from k_cli.core.credentials import CredentialsManager, SUPPORTED_KEYS
2833
+ from rich.table import Table
2834
+
2835
+ table = Table(title="⚡ Provider Connectivity Test", border_style="cyan")
2836
+ table.add_column("Provider", style="bold cyan")
2837
+ table.add_column("Status", style="bold")
2838
+ table.add_column("Latency / Message", style="dim")
2839
+
2840
+ for key_name, label, _ in SUPPORTED_KEYS:
2841
+ ok, msg = CredentialsManager.test_key_connectivity(key_name)
2842
+ status_text = "[green]✔ Connected[/green]" if ok else "[red]✘ Offline / Missing[/red]"
2843
+ table.add_row(label, status_text, msg)
2844
+
2845
+ console.print(table)
2846
+
2847
+
2848
+ @keys_app.command(name="import", help="Import API keys from an existing .env or key.json file.")
2849
+ def keys_import_cmd(
2850
+ file_path: str = typer.Argument(..., help="Path to .env or key.json file to import."),
2851
+ ):
2852
+ from k_cli.core.credentials import CredentialsManager, SUPPORTED_KEYS
2853
+ p = Path(file_path).resolve()
2854
+ if not p.exists():
2855
+ console.print(f"[bold red]File not found: {file_path}[/bold red]")
2856
+ raise typer.Exit(code=1)
2857
+
2858
+ imported_count = 0
2859
+ if p.suffix == ".json":
2860
+ data = json.loads(p.read_text(encoding="utf-8"))
2861
+ for k, v in data.items():
2862
+ if isinstance(v, str) and v.strip() and k.upper() in [sk[0] for sk in SUPPORTED_KEYS]:
2863
+ CredentialsManager.set_key(k.upper(), v.strip())
2864
+ imported_count += 1
2865
+ else:
2866
+ for line in p.read_text(encoding="utf-8").splitlines():
2867
+ line = line.strip()
2868
+ if line and not line.startswith("#") and "=" in line:
2869
+ k, v = line.split("=", 1)
2870
+ k, v = k.strip().upper(), v.strip()
2871
+ if k in [sk[0] for sk in SUPPORTED_KEYS] and v:
2872
+ CredentialsManager.set_key(k, v)
2873
+ imported_count += 1
2874
+
2875
+ console.print(f"[bold green]✔ Successfully imported {imported_count} key(s) from {file_path}![/bold green]")
2876
+
2877
+ app.add_typer(keys_app, name="keys")
2878
+ app.add_typer(keys_app, name="auth")
2879
+
2880
+
2881
+
2882
+ # =============================================================================
2883
+ # 10 Killer Agentic CLI Commands
2884
+ # =============================================================================
2885
+
2886
+ @app.command(name="watch", help="Feature 1: Autonomous PR Review & Watcher Daemon.")
2887
+ def watch_cmd(
2888
+ interval: int = typer.Option(30, "--interval", "-i", help="Polling interval in seconds."),
2889
+ auto_merge: bool = typer.Option(False, "--auto-merge", help="Auto-merge approved PRs when CI passes."),
2890
+ once: bool = typer.Option(False, "--once", help="Run a single review cycle and exit."),
2891
+ ):
2892
+ from k_cli.github.pr_watcher import PRWatcherDaemon
2893
+ daemon = PRWatcherDaemon(auto_merge_approved=auto_merge)
2894
+ console.print(f"[bold cyan]👁️ K-CLI PR Watcher Daemon active...[/bold cyan]")
2895
+ events = daemon.run_loop(interval_seconds=interval, max_iterations=1 if once else None, callback=lambda ev: console.print(f"[green]✔ PR #{ev.pr_number}: {ev.review_status} ({ev.action_taken})[/green]"))
2896
+ console.print(f"[dim]Processed {len(events)} PR review event(s).[/dim]")
2897
+
2898
+
2899
+ @app.command(name="bisect", help="Feature 2: AI-Powered Git Bisect & Regression Hunter.")
2900
+ def bisect_cmd(
2901
+ test_cmd: str = typer.Argument(..., help="Test command to evaluate regressions (e.g. 'pytest tests/ -q')."),
2902
+ good: str = typer.Option("HEAD~5", "--good", help="Known good commit SHA."),
2903
+ bad: str = typer.Option("HEAD", "--bad", help="Known bad commit SHA."),
2904
+ ):
2905
+ from k_cli.git.ai_bisect import AIBisectEngine
2906
+ try:
2907
+ engine = AIBisectEngine()
2908
+ console.print(f"[bold magenta]🎯 Starting AI Git Bisect between {good} and {bad}...[/bold magenta]")
2909
+ res = engine.run_bisect(test_command=test_cmd, good_commit=good, bad_commit=bad)
2910
+ console.print(Markdown(res.render_markdown()))
2911
+ except Exception as ex:
2912
+ console.print(f"[bold red]✘ Git Bisect failed:[/bold red] {ex}")
2913
+ raise typer.Exit(code=1)
2914
+
2915
+
2916
+ @app.command(name="route", help="Feature 3: Cost & Latency Smart Model Router.")
2917
+ def route_cmd(
2918
+ task: str = typer.Argument("Analyze, architect, and optimize repository codebase", help="Task prompt to analyze and route."),
2919
+ ):
2920
+ from k_cli.core.smart_router import SmartModelRouter
2921
+ try:
2922
+ router = SmartModelRouter()
2923
+ dec = router.route(task_prompt=task or "default task")
2924
+ console.print(Panel(
2925
+ f"[bold cyan]Selected Model:[/bold cyan] {dec.selected_model} ({dec.selected_provider})\n"
2926
+ f"[bold yellow]Task Tier:[/bold yellow] {dec.tier.value.upper()}\n"
2927
+ f"[bold green]Estimated Cost:[/bold green] ${dec.estimated_cost_usd:.4f} USD\n"
2928
+ f"[bold blue]Savings vs GPT-4:[/bold blue] ${dec.savings_usd:.4f} USD ({(dec.savings_usd/dec.baseline_gpt4_cost_usd):.1%})\n"
2929
+ f"[dim]Rationale: {dec.reasoning}[/dim]",
2930
+ title="⚡ Smart Model Router Decision",
2931
+ border_style="cyan",
2932
+ ))
2933
+ except Exception as ex:
2934
+ console.print(f"[bold red]✘ Smart Router failed:[/bold red] {ex}")
2935
+ raise typer.Exit(code=1)
2936
+
2937
+
2938
+ @app.command(name="garden", help="Feature 4: Nightly Autonomous Repo Maintenance & Health Sweep.")
2939
+ def garden_cmd(
2940
+ as_json: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
2941
+ ):
2942
+ from k_cli.tools.repo_gardener import RepoGardener
2943
+ try:
2944
+ gardener = RepoGardener()
2945
+ rep = gardener.run_garden_sweep()
2946
+ if as_json:
2947
+ import json
2948
+ console.print(json.dumps({"health_score": rep.health_score, "findings": len(rep.findings), "dead_code": rep.dead_code_count}))
2949
+ else:
2950
+ console.print(Markdown(rep.render_markdown()))
2951
+ except Exception as ex:
2952
+ console.print(f"[bold red]✘ Repo Gardener failed:[/bold red] {ex}")
2953
+ raise typer.Exit(code=1)
2954
+
2955
+
2956
+ @app.command(name="explain", help="Feature 5: Codebase Natural Language Search & Semantic Q&A.")
2957
+ def explain_cmd(
2958
+ query: str = typer.Argument("Explain high level architecture and entrypoints", help="Question to ask about the codebase architecture."),
2959
+ ):
2960
+ from k_cli.tools.codebase_qa import CodebaseQAEngine
2961
+ if not query.strip():
2962
+ console.print("[bold yellow]Please provide a question to search the codebase.[/bold yellow]")
2963
+ return
2964
+ try:
2965
+ qa = CodebaseQAEngine()
2966
+ res = qa.ask(query=query)
2967
+ console.print(Markdown(res.render_markdown()))
2968
+ except Exception as ex:
2969
+ console.print(f"[bold red]✘ Codebase Q&A failed:[/bold red] {ex}")
2970
+ raise typer.Exit(code=1)
2971
+
2972
+
2973
+ @app.command(name="ghost", help="Feature 6: Ghost Terminal Autopilot & Error Healer.")
2974
+ def ghost_cmd(
2975
+ command: str = typer.Argument(..., help="Dev server or test command to wrap (e.g. 'pytest')."),
2976
+ ):
2977
+ from k_cli.tools.ghost_daemon import GhostTerminalDaemon
2978
+ try:
2979
+ daemon = GhostTerminalDaemon()
2980
+ console.print(f"[bold cyan]👻 K-CLI Ghost Terminal Autopilot attached to: '{command}'[/bold cyan]\n")
2981
+ code = daemon.run_wrapped_command(command_str=command, on_heal_prompt=lambda p: True)
2982
+ raise typer.Exit(code=code)
2983
+ except Exception as ex:
2984
+ console.print(f"[bold red]✘ Ghost Daemon encountered an error:[/bold red] {ex}")
2985
+ raise typer.Exit(code=1)
2986
+
2987
+
2988
+ @app.command(name="swarm", help="Feature 7: Adversarial Red Team / Blue Team Consensus Loop.")
2989
+ def swarm_cmd(
2990
+ task: str = typer.Argument("Implement verified zero-defect algorithms", help="Coding task to execute through adversarial consensus."),
2991
+ rounds: int = typer.Option(3, "--rounds", "-r", help="Maximum adversarial attack rounds."),
2992
+ ):
2993
+ from k_cli.agents.adversarial_swarm import AdversarialConsensusSwarm
2994
+ try:
2995
+ swarm = AdversarialConsensusSwarm(max_rounds=rounds)
2996
+ console.print(f"[bold magenta]🐝 Running Adversarial Consensus Swarm for: '{task}'...[/bold magenta]")
2997
+ res = swarm.run_consensus(task_prompt=task or "consensus task")
2998
+ console.print(Markdown(res.render_markdown()))
2999
+ except Exception as ex:
3000
+ console.print(f"[bold red]✘ Adversarial Swarm failed:[/bold red] {ex}")
3001
+ raise typer.Exit(code=1)
3002
+
3003
+
3004
+ @app.command(name="synapse", help="Feature 8: AST Neural Code Graph & Context Compressor.")
3005
+ def synapse_cmd(
3006
+ query: str = typer.Argument("core architecture components", help="Task or keyword to extract minimal AST subgraph for."),
3007
+ ):
3008
+ from k_cli.tools.synapse_graph import SynapseCodeGraph
3009
+ try:
3010
+ graph = SynapseCodeGraph()
3011
+ res = graph.extract_subgraph_slice(query=query or "core")
3012
+ console.print(Markdown(res.render_context()))
3013
+ except Exception as ex:
3014
+ console.print(f"[bold red]✘ Synapse Graph extraction failed:[/bold red] {ex}")
3015
+ raise typer.Exit(code=1)
3016
+
3017
+
3018
+ @app.command(name="airgap", help="Feature 9: Sovereign Air-Gapped Offline Engine.")
3019
+ def airgap_cmd():
3020
+ from k_cli.core.airgap import AirgapManager
3021
+ try:
3022
+ mgr = AirgapManager()
3023
+ rep = mgr.audit_environment()
3024
+ console.print(Markdown(rep.render_markdown()))
3025
+ except Exception as ex:
3026
+ console.print(f"[bold red]✘ Airgap audit failed:[/bold red] {ex}")
3027
+ raise typer.Exit(code=1)
3028
+
3029
+
3030
+ @app.command(name="scaffold", help="Feature 10: Natural Language Full-Stack Scaffolder.")
3031
+ def scaffold_cmd(
3032
+ spec: str = typer.Argument(..., help="Natural language description of application to scaffold."),
3033
+ target: str = typer.Option("./scaffolded_app", "--dir", "-d", help="Target output directory."),
3034
+ write: bool = typer.Option(False, "--write", "-w", help="Write scaffolded files to disk."),
3035
+ ):
3036
+ from k_cli.agents.scaffold_engine import FullStackScaffolder
3037
+ if not spec.strip():
3038
+ console.print("[bold yellow]Please provide an application specification to scaffold.[/bold yellow]")
3039
+ return
3040
+ try:
3041
+ scaffolder = FullStackScaffolder()
3042
+ console.print(f"[bold cyan]🏗️ Scaffolding full-stack application for: '{spec}'...[/bold cyan]")
3043
+ res = scaffolder.scaffold(spec_prompt=spec, target_dir=target, write_to_disk=write)
3044
+ console.print(Markdown(res.render_markdown()))
3045
+ except Exception as ex:
3046
+ console.print(f"[bold red]✘ Scaffolding failed:[/bold red] {ex}")
3047
+ raise typer.Exit(code=1)
3048
+
3049
+
3050
+ @app.command(name="strands", help="Feature 11: AWS Strands Autonomous Agent Runner (Agents for Humans).")
3051
+ def strands_cmd(
3052
+ goal: str = typer.Argument(..., help="High-level engineering or triage goal for the Strands agent."),
3053
+ provider: str = typer.Option("auto", "--provider", "-p", help="Model provider ('bedrock', 'gemini', 'anthropic', 'openai', 'ollama', or 'auto')."),
3054
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Specific model ID (e.g. 'anthropic.claude-3-5-sonnet-20241022-v2:0' or 'gemini-2.0-flash')."),
3055
+ region: Optional[str] = typer.Option(None, "--region", "-r", help="AWS Region for Amazon Bedrock (e.g. 'us-east-1')."),
3056
+ ):
3057
+ """Executes an autonomous goal using the AWS Strands Agents SDK and registered deterministic tools."""
3058
+ from k_cli.agents.strands_agent import create_strands_agent
3059
+ try:
3060
+ console.print(f"[bold cyan]⚡ Initializing AWS Strands Autonomous Agent (Provider: {provider})...[/bold cyan]")
3061
+ agent = create_strands_agent(provider=provider, model_name=model, aws_region=region)
3062
+ console.print(f"[bold green]▶ Running Goal:[/bold green] [white]{goal}[/white]\n")
3063
+ response = agent.run(goal)
3064
+ console.print(Markdown(response))
3065
+ except Exception as ex:
3066
+ console.print(f"[bold red]✘ Strands Agent execution failed:[/bold red] {ex}")
3067
+ raise typer.Exit(code=1)
3068
+
3069
+
3070
+ @app.command(name="agent", help="Alias for strands autonomous developer agent.")
3071
+ def agent_cmd(
3072
+ goal: str = typer.Argument(..., help="High-level engineering or triage goal for the Strands agent."),
3073
+ provider: str = typer.Option("auto", "--provider", "-p", help="Model provider ('bedrock', 'gemini', 'anthropic', 'openai', 'ollama', or 'auto')."),
3074
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Specific model ID."),
3075
+ region: Optional[str] = typer.Option(None, "--region", "-r", help="AWS Region for Amazon Bedrock."),
3076
+ ):
3077
+ strands_cmd(goal=goal, provider=provider, model=model, region=region)
3078
+
3079
+
3080
+ @app.command(name="auto-heal", help="Feature 12: Strands Deep Crash Triage & Closed-Loop Auto-Heal.")
3081
+ def auto_heal_cmd(
3082
+ log_source: Optional[str] = typer.Argument(None, help="Path to crash log file, or raw error string. If omitted, reads from stdin."),
3083
+ repo: str = typer.Option(".", "--repo", "-r", help="Target repository root directory."),
3084
+ ):
3085
+ """Parses raw crash traces across 7 environments and executes an autonomous verified heal loop."""
3086
+ from k_cli.agents.strands_agent import triage_and_heal_incident
3087
+ try:
3088
+ if log_source and os.path.exists(log_source):
3089
+ raw_log = Path(log_source).read_text(encoding="utf-8", errors="replace")
3090
+ elif log_source:
3091
+ raw_log = log_source
3092
+ elif not sys.stdin.isatty():
3093
+ raw_log = sys.stdin.read()
3094
+ else:
3095
+ console.print("[bold yellow]Please provide a log file, error string, or pipe logs via stdin.[/bold yellow]")
3096
+ return
3097
+
3098
+ console.print("[bold cyan]🔍 Executing Strands Multi-Language Crash Triage & Auto-Heal...[/bold cyan]\n")
3099
+ report_json = triage_and_heal_incident(raw_log, repo_path=repo)
3100
+ console.print(Syntax(report_json, "json", theme="monokai", line_numbers=True))
3101
+ except Exception as ex:
3102
+ console.print(f"[bold red]✘ Auto-heal failed:[/bold red] {ex}")
3103
+ raise typer.Exit(code=1)
3104
+
3105
+
3106
+ @app.command(name="immune", help="Feature 13: Autonomous Chaos Immunity & Edge-Case Self-Healing Engine.")
3107
+ def immune_cmd(
3108
+ target_file: Optional[str] = typer.Argument(None, help="Target Python source file to probe and inoculate. If omitted, scans repository."),
3109
+ repo: str = typer.Option(".", "--repo", "-r", help="Target repository root directory."),
3110
+ apply_patches: bool = typer.Option(True, "--patch/--no-patch", help="Automatically apply verified defensive inoculation patches."),
3111
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
3112
+ ):
3113
+ """Probes brittle AST patterns (KeyError, None dereference, timeout hangs), synthesizes adversarial tests, and inoculates codebase."""
3114
+ from k_cli.tools.chaos_immunity import ChaosImmunityEngine
3115
+ try:
3116
+ engine = ChaosImmunityEngine(repo_path=repo)
3117
+ if target_file and os.path.exists(target_file):
3118
+ console.print(f"[bold cyan]🛡️ Running Chaos Immunity Inoculation on '{target_file}'...[/bold cyan]\n")
3119
+ report = engine.inoculate_file(target_file, auto_apply_patches=apply_patches)
3120
+ if json_output:
3121
+ import json
3122
+ console.print(json.dumps({
3123
+ "target_file": report.target_file,
3124
+ "patterns_detected": len(report.patterns_detected),
3125
+ "generated_tests_count": report.generated_tests_count,
3126
+ "patches_applied_count": report.patches_applied_count,
3127
+ "verification_passed": report.verification_passed,
3128
+ "summary": report.summary,
3129
+ }, indent=2))
3130
+ else:
3131
+ console.print(Markdown(report.render_markdown()))
3132
+ else:
3133
+ console.print("[bold cyan]🛡️ Scanning workspace for brittle edge cases across core modules...[/bold cyan]\n")
3134
+ reports = engine.scan_and_inoculate_repo(max_files=10)
3135
+ total_patterns = sum(len(r.patterns_detected) for r in reports)
3136
+ total_tests = sum(r.generated_tests_count for r in reports)
3137
+ console.print(Panel(
3138
+ f"[bold green]✔ Chaos Immunity Sweep Completed[/bold green]\n\n"
3139
+ f"• [cyan]Modules Inoculated:[/cyan] {len(reports)}\n"
3140
+ f"• [yellow]Brittle Edge Cases Probed:[/yellow] {total_patterns}\n"
3141
+ f"• [magenta]Adversarial Immunity Tests Synthesized:[/magenta] {total_tests}\n"
3142
+ f"• [green]AST Ground-Truth Integrity:[/green] 100% VERIFIED\n\n"
3143
+ f"[dim]Generated test suites stored in `tests/chaos/`[/dim]",
3144
+ title="🛡️ K-CLI Chaos Immunity Shield",
3145
+ border_style="green",
3146
+ ))
3147
+ except Exception as ex:
3148
+ console.print(f"[bold red]✘ Chaos Immunity Engine failed:[/bold red] {ex}")
3149
+ raise typer.Exit(code=1)
3150
+
3151
+
3152
+ @app.command(name="chaos", help="Alias for k-cli immune.")
3153
+ def chaos_cmd(
3154
+ target_file: Optional[str] = typer.Argument(None, help="Target Python source file to probe and inoculate."),
3155
+ repo: str = typer.Option(".", "--repo", "-r", help="Target repository root directory."),
3156
+ ):
3157
+ immune_cmd(target_file=target_file, repo=repo, apply_patches=True, json_output=False)
3158
+
3159
+
3160
+ # =============================================================================
3161
+ # Amazon Bedrock AgentCore Deployment & Integration (`k-cli bedrock`)
3162
+ # =============================================================================
3163
+ bedrock_app = typer.Typer(name="bedrock", help="Deploy and manage Amazon Bedrock AgentCore for K-CLI Strands Agent.")
3164
+
3165
+
3166
+ @bedrock_app.command(name="export", help="Export Amazon Bedrock AgentCore OpenAPI schema and CloudFormation bundle.")
3167
+ def bedrock_export_cmd(
3168
+ output_dir: str = typer.Option(".kcli/agent_core_bundle", "--output", "-o", help="Output directory for AgentCore bundle."),
3169
+ ):
3170
+ """Exports Bedrock AgentCore OpenAPI 3.0 schemas and CloudFormation SAM templates."""
3171
+ from k_cli.agents.agent_core import BedrockAgentCoreEngine
3172
+ engine = BedrockAgentCoreEngine()
3173
+ bundle_path = engine.export_deployment_bundle(output_dir=output_dir)
3174
+ console.print(Panel(
3175
+ f"[bold green]✔ Amazon Bedrock AgentCore Bundle Exported[/bold green]\n\n"
3176
+ f"• [cyan]Bundle Directory:[/cyan] {bundle_path}\n"
3177
+ f"• [yellow]OpenAPI Action Group:[/yellow] {bundle_path / 'openapi_schema.json'}\n"
3178
+ f"• [magenta]CloudFormation SAM Template:[/magenta] {bundle_path / 'template.yaml'}\n"
3179
+ f"• [green]Agent Configuration:[/green] {bundle_path / 'agent_config.json'}\n\n"
3180
+ f"[dim]Ready to deploy with AWS CLI or SAM: `sam deploy --guided`[/dim]",
3181
+ title="⚡ Amazon Bedrock AgentCore",
3182
+ border_style="green",
3183
+ ))
3184
+
3185
+
3186
+ @bedrock_app.command(name="deploy", help="Deploy K-CLI Strands Agent to Amazon Bedrock AgentCore.")
3187
+ def bedrock_deploy_cmd():
3188
+ """Deploys K-CLI Strands Agent directly to Amazon Bedrock."""
3189
+ from k_cli.agents.agent_core import BedrockAgentCoreEngine
3190
+ engine = BedrockAgentCoreEngine()
3191
+ res = engine.deploy_to_bedrock()
3192
+ console.print(Panel(
3193
+ f"[bold green]✔ Amazon Bedrock AgentCore Deployment Status: {res['status']}[/bold green]\n\n"
3194
+ f"• [cyan]Agent Name:[/cyan] {res['agent_name']}\n"
3195
+ f"• [yellow]Foundation Model:[/yellow] {res['model_id']}\n"
3196
+ f"• [magenta]AWS Region:[/magenta] {res['region']}\n"
3197
+ f"• [white]Summary:[/white] {res['message']}\n",
3198
+ title="⚡ Amazon Bedrock AgentCore Deployment",
3199
+ border_style="cyan",
3200
+ ))
3201
+
3202
+
3203
+ app.add_typer(bedrock_app, name="bedrock")
3204
+
3205
+
3206
+ # =============================================================================
3207
+ # Autonomous Background Healing Daemon (`k-cli daemon` / `k-cli watch`)
3208
+ # =============================================================================
3209
+ @app.command(name="daemon", help="Run K-CLI autonomous self-healing daemon in the background.")
3210
+ @app.command(name="watch", help="Continuously monitor repository and auto-heal broken builds in the background.")
3211
+ def daemon_cmd(
3212
+ repo: str = typer.Option(".", "--repo", "-r", help="Repository directory to monitor."),
3213
+ interval: float = typer.Option(10.0, "--interval", "-i", help="Poll interval in seconds."),
3214
+ ):
3215
+ """Runs autonomous developer daemon quietly in the background; surfaces only on critical decisions."""
3216
+ from k_cli.agents.background_daemon import BackgroundHealerDaemon
3217
+ import asyncio
3218
+ console.print(f"[bold cyan]⚡ K-CLI Background Healer Daemon starting on '{repo}' (interval: {interval}s)...[/bold cyan]")
3219
+ console.print("[dim]Runs quietly in the background and surfaces only when a decision is needed. Press Ctrl+C to stop.[/dim]\n")
3220
+
3221
+ def on_decision(dec):
3222
+ console.print(f"\n[bold green]🚨 [DAEMON NOTICE][/bold green] [yellow]{dec['summary']}[/yellow]")
3223
+
3224
+ daemon = BackgroundHealerDaemon(workspace_dir=repo, poll_interval_seconds=interval, decision_callback=on_decision)
3225
+ try:
3226
+ asyncio.run(daemon.start())
3227
+ except KeyboardInterrupt:
3228
+ console.print("\n[yellow]Daemon stopped by user.[/yellow]")
3229
+
3230
+
3231
+ # =============================================================================
3232
+ # Cinematic 5-Minute Interactive Demo & AI Voiceover (`k-cli demo`)
3233
+ # =============================================================================
3234
+ @app.command(name="demo", help="Run the cinematic 5-minute interactive demo with AI voiceover cues.")
3235
+ def demo_cmd(
3236
+ speed: float = typer.Option(1.0, "--speed", "-s", help="Playback speed multiplier (e.g. 1.5 for fast demo)."),
3237
+ act: Optional[int] = typer.Option(None, "--act", "-a", help="Run a specific act (1 to 5). If omitted, runs all 5 acts."),
3238
+ ):
3239
+ """Executes the ultra-cinematic 5-minute production demo with live agent telemetry."""
3240
+ from k_cli.demo.demo_runner import start_cinematic_demo
3241
+ start_cinematic_demo(speed=speed, act=act)
3242
+
3243
+
3244
+ def interactive_mode(model: str = "qwen2.5-coder:1.5b", mock: bool = False, continue_session: bool = False):
3245
+ """Interactive multi-turn prompt shell when typing 'k' without arguments."""
3246
+ if hasattr(console, "is_terminal") and console.is_terminal:
3247
+ console.clear()
3248
+
3249
+ if continue_session:
3250
+ session = SessionManager.load_latest(workspace_dir=".", mock_mode=mock) or SessionManager(workspace_dir=".", model_name=model, mock_mode=mock)
3251
+ else:
3252
+ session = SessionManager(workspace_dir=".", model_name=model, mock_mode=mock)
3253
+
3254
+ print_banner()
3255
+ if continue_session and session.history:
3256
+ console.print(f"[bold green]✔ Resumed previous session ({len(session.history)} turn(s), model: {session.model_name}) from ~/.kcli/sessions/[/bold green]\n")
3257
+ else:
3258
+ console.print("[bold cyan]K-CLI Interactive Shell ready. Type /help for slash commands or /exit to quit.[/bold cyan]\n")
3259
+
3260
+ shell = InteractiveShell(session=session, console=console)
3261
+ shell.run()
3262
+
3263
+
3264
+ def version_callback(value: bool):
3265
+ if value:
3266
+ console.print("[bold cyan]K-CLI[/bold cyan] [bold bright_white]v1.0.0[/bold bright_white] [dim](Project Bankai Flagship Edition)[/dim]")
3267
+ raise typer.Exit()
3268
+
3269
+
3270
+ @app.callback(
3271
+ invoke_without_command=True,
3272
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
3273
+ )
3274
+ def main(
3275
+ ctx: typer.Context,
3276
+ version: Optional[bool] = typer.Option(None, "--version", "-v", help="Show K-CLI version and exit.", callback=version_callback, is_eager=True),
3277
+ prompt: Optional[str] = typer.Option(None, "--prompt", "-p", help="Prompt text if running main entrypoint directly."),
3278
+ continue_session: bool = typer.Option(False, "--continue", "-c", help="Continue previous multi-turn session from local storage."),
3279
+ demo_ui: bool = typer.Option(False, "--demo-ui", help="Launch the TUI in pure Zero-AI demo mode without needing any API key."),
3280
+ ):
3281
+ if ctx.invoked_subcommand is None:
3282
+ if demo_ui:
3283
+ ui_cmd(mock=True, demo=True, continue_session=continue_session)
3284
+ raise typer.Exit()
3285
+ elif prompt:
3286
+ execute_run(prompt=prompt, show_banner=True)
3287
+ raise typer.Exit()
3288
+ elif ctx.args:
3289
+ prompt_arg = " ".join(ctx.args)
3290
+ execute_run(prompt=prompt_arg, show_banner=True)
3291
+ raise typer.Exit()
3292
+ else:
3293
+ interactive_mode(continue_session=continue_session)
3294
+
3295
+
3296
+ if __name__ == "__main__":
3297
+ app()