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.
- k_cli/__init__.py +77 -0
- k_cli/agents/__init__.py +0 -0
- k_cli/agents/adversarial_swarm.py +338 -0
- k_cli/agents/agent_core.py +255 -0
- k_cli/agents/background_daemon.py +141 -0
- k_cli/agents/orchestrator.py +376 -0
- k_cli/agents/persona.py +649 -0
- k_cli/agents/scaffold_engine.py +121 -0
- k_cli/agents/strands_agent.py +832 -0
- k_cli/agents/subagents.py +1496 -0
- k_cli/cli.py +3297 -0
- k_cli/core/__init__.py +0 -0
- k_cli/core/airgap.py +95 -0
- k_cli/core/credentials.py +548 -0
- k_cli/core/intent_sensor.py +177 -0
- k_cli/core/llm_driver.py +1028 -0
- k_cli/core/model_manager.py +1109 -0
- k_cli/core/models_hub.py +913 -0
- k_cli/core/prompting.py +41 -0
- k_cli/core/sdk.py +322 -0
- k_cli/core/session.py +826 -0
- k_cli/core/smart_router.py +230 -0
- k_cli/core/storage_manager.py +176 -0
- k_cli/core/viewport_engine.py +117 -0
- k_cli/demo/demo_runner.py +579 -0
- k_cli/git/__init__.py +0 -0
- k_cli/git/ai_bisect.py +208 -0
- k_cli/git/conflict_resolver.py +1039 -0
- k_cli/git/git_guard.py +417 -0
- k_cli/git/patcher.py +1175 -0
- k_cli/git/repo_map.py +1780 -0
- k_cli/git/smart_git.py +928 -0
- k_cli/git/verifier.py +969 -0
- k_cli/github/__init__.py +0 -0
- k_cli/github/dedup_engine.py +787 -0
- k_cli/github/github_client.py +1702 -0
- k_cli/github/github_engine.py +641 -0
- k_cli/github/local_hub.py +209 -0
- k_cli/github/pr_watcher.py +129 -0
- k_cli/github/trending.py +205 -0
- k_cli/tools/__init__.py +0 -0
- k_cli/tools/audit.py +79 -0
- k_cli/tools/chaos_immunity.py +377 -0
- k_cli/tools/codebase_qa.py +106 -0
- k_cli/tools/command_runner.py +256 -0
- k_cli/tools/diagram_generator.py +547 -0
- k_cli/tools/doc_retriever.py +1332 -0
- k_cli/tools/feature.py +105 -0
- k_cli/tools/ghost_daemon.py +122 -0
- k_cli/tools/incident_triage.py +1365 -0
- k_cli/tools/mcp_client.py +1846 -0
- k_cli/tools/repo_gardener.py +142 -0
- k_cli/tools/rules.py +109 -0
- k_cli/tools/security.py +52 -0
- k_cli/tools/security_healer.py +999 -0
- k_cli/tools/synapse_graph.py +155 -0
- k_cli/tui/__init__.py +0 -0
- k_cli/tui/diff_viewer.py +223 -0
- k_cli/tui/tui.py +1145 -0
- k_cli/tui/tui_animations.py +648 -0
- k_cli/tui/tui_app.py +2788 -0
- k_cli/ui/__init__.py +10 -0
- k_cli/ui/simple_repl.py +315 -0
- k_cli/web/__init__.py +7 -0
- k_cli/web/server.py +624 -0
- k_cli/web/static/app.js +830 -0
- k_cli/web/static/index.html +495 -0
- k_cli/web/static/monitor.html +189 -0
- k_cli/web/static/style.css +838 -0
- k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
- k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
- k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
- k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
- k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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()
|