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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
smart_router.py - Cost, Latency & Intent-Adaptive Smart Model Router for K-CLI
|
|
3
|
+
Project Bankai v1.0.0
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
1. Adaptive Intent Routing: Automatically routes casual chat to ultra-fast & cheap models (Gemini Flash, Claude Haiku, GPT-4o-mini, Groq)
|
|
7
|
+
and routes architectural planning & heavy coding to premier frontier models (Claude 3.5 Sonnet, Bankai-14B, DeepSeek Coder).
|
|
8
|
+
2. Pinned Default Model: Respects user-pinned default model preferences when 'default' mode is active.
|
|
9
|
+
3. Cost & Latency Financial Optimization: Estimates cumulative cost savings against frontier baselines.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from k_cli.core.credentials import CredentialsManager, DevPreferencesManager
|
|
22
|
+
from k_cli.core.intent_sensor import IntentSensor, UserIntent
|
|
23
|
+
from k_cli.core.llm_driver import LLMDriver, ProviderType
|
|
24
|
+
from k_cli.core.models_hub import ModelHub, ModelProvider, ModelSpec
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("k_cli.core.smart_router")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TaskTier(str, Enum):
|
|
30
|
+
TRIVIAL = "trivial" # Docstrings, typos, formatting, greetings -> Fast / Cheap (Gemini Flash, Local SLM)
|
|
31
|
+
STANDARD = "standard" # Standard functions, bug fixes -> High Throughput (DeepSeek, Groq)
|
|
32
|
+
COMPLEX = "complex" # Multi-file refactors, AST verification, planning -> Claude Sonnet, Bankai-14B, GPT-4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class RouteDecision:
|
|
37
|
+
"""Routing outcome with rationale and cost analysis."""
|
|
38
|
+
task: str
|
|
39
|
+
tier: TaskTier
|
|
40
|
+
selected_model: str
|
|
41
|
+
selected_provider: str
|
|
42
|
+
estimated_cost_usd: float
|
|
43
|
+
baseline_gpt4_cost_usd: float
|
|
44
|
+
savings_usd: float
|
|
45
|
+
reasoning: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AdaptiveIntentRouter:
|
|
49
|
+
"""
|
|
50
|
+
Real-time dynamic model selector based on sensed user intent and active API keys.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def is_model_live(cls, model_name: str) -> bool:
|
|
55
|
+
"""Checks if a model is actually available (cloud key present or local model installed)."""
|
|
56
|
+
if not model_name:
|
|
57
|
+
return False
|
|
58
|
+
m_lower = model_name.lower().strip()
|
|
59
|
+
if m_lower in ("mock", "offline", "deterministic", "local_deterministic"):
|
|
60
|
+
return True
|
|
61
|
+
# Cloud check
|
|
62
|
+
if "gemini" in m_lower and (os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")):
|
|
63
|
+
return True
|
|
64
|
+
if "claude" in m_lower and os.environ.get("ANTHROPIC_API_KEY"):
|
|
65
|
+
return True
|
|
66
|
+
if ("gpt" in m_lower or "o1" in m_lower or "o3" in m_lower) and os.environ.get("OPENAI_API_KEY"):
|
|
67
|
+
return True
|
|
68
|
+
if "deepseek" in m_lower and os.environ.get("DEEPSEEK_API_KEY"):
|
|
69
|
+
return True
|
|
70
|
+
if "groq" in m_lower and os.environ.get("GROQ_API_KEY"):
|
|
71
|
+
return True
|
|
72
|
+
if ("bedrock" in m_lower or "anthropic.claude" in m_lower) and (os.environ.get("AWS_ACCESS_KEY_ID") or os.environ.get("AWS_PROFILE")):
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# Local Ollama / GGUF check
|
|
76
|
+
driver = LLMDriver(model_name=model_name)
|
|
77
|
+
if driver.is_ollama_available():
|
|
78
|
+
mm = driver.get_model_manager()
|
|
79
|
+
if mm and mm.has_ollama_model(model_name):
|
|
80
|
+
return True
|
|
81
|
+
mm = driver.get_model_manager()
|
|
82
|
+
if mm:
|
|
83
|
+
gguf_path = mm.find_local_gguf(model_name)
|
|
84
|
+
if gguf_path and Path(gguf_path).exists():
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def get_fallback_live_model(cls) -> Tuple[str, str]:
|
|
90
|
+
"""Finds the best active live model or falls back to sovereign verified model."""
|
|
91
|
+
CredentialsManager.load_all_credentials()
|
|
92
|
+
if os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY"):
|
|
93
|
+
return "gemini-2.5-flash", "Active Google Gemini API key ('gemini-2.5-flash')"
|
|
94
|
+
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
95
|
+
return "claude-3-5-sonnet-20241022", "Active Anthropic Claude API key"
|
|
96
|
+
if os.environ.get("OPENAI_API_KEY"):
|
|
97
|
+
return "gpt-4o-mini", "Active OpenAI API key"
|
|
98
|
+
if os.environ.get("DEEPSEEK_API_KEY"):
|
|
99
|
+
return "deepseek-chat", "Active DeepSeek API key"
|
|
100
|
+
if os.environ.get("GROQ_API_KEY"):
|
|
101
|
+
return "groq/llama-3.1-8b-instant", "Active Groq API key"
|
|
102
|
+
|
|
103
|
+
driver = LLMDriver()
|
|
104
|
+
if driver.is_ollama_available():
|
|
105
|
+
mm = driver.get_model_manager()
|
|
106
|
+
if mm:
|
|
107
|
+
models = mm.list_installed_models()
|
|
108
|
+
if models:
|
|
109
|
+
return models[0], f"Installed local model '{models[0]}'"
|
|
110
|
+
return "qwen2.5-coder:1.5b", "Local Ollama server"
|
|
111
|
+
|
|
112
|
+
return "qwen2.5-coder:1.5b", "Sovereign local engine"
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def resolve_model_for_prompt(cls, prompt: str, requested_model: Optional[str] = None) -> Tuple[str, str]:
|
|
116
|
+
"""
|
|
117
|
+
Determines the optimal model for a given prompt:
|
|
118
|
+
- If requested_model is specific, verifies availability. If inactive/missing, auto-falls back.
|
|
119
|
+
- If requested_model is 'default' or 'auto', dynamically senses intent and chooses active model.
|
|
120
|
+
"""
|
|
121
|
+
CredentialsManager.load_all_credentials()
|
|
122
|
+
|
|
123
|
+
if requested_model and requested_model.lower() not in ("auto", "dynamic", "none", "default"):
|
|
124
|
+
return requested_model.strip(), f"User-selected model: {requested_model.strip()}"
|
|
125
|
+
|
|
126
|
+
if requested_model and requested_model.lower() == "default":
|
|
127
|
+
def_m = DevPreferencesManager.get_default_model()
|
|
128
|
+
return def_m, f"Using user-pinned default model: {def_m}"
|
|
129
|
+
|
|
130
|
+
# Sensed Intent Routing
|
|
131
|
+
intent_res = IntentSensor.sense(prompt)
|
|
132
|
+
|
|
133
|
+
if intent_res.intent == UserIntent.CHAT:
|
|
134
|
+
model = DevPreferencesManager.get_fast_chat_model()
|
|
135
|
+
if cls.is_model_live(model):
|
|
136
|
+
return model, f"⚡ Fast Chat Path ({intent_res.mode_label}): Routed to low-latency model '{model}'"
|
|
137
|
+
|
|
138
|
+
elif intent_res.intent == UserIntent.PLAN:
|
|
139
|
+
model = DevPreferencesManager.get_frontier_reasoning_model()
|
|
140
|
+
if cls.is_model_live(model):
|
|
141
|
+
return model, f"📐 Architectural Planner ({intent_res.mode_label}): Routed to frontier reasoning model '{model}'"
|
|
142
|
+
|
|
143
|
+
elif intent_res.intent in (UserIntent.BUILD, UserIntent.TRIAGE, UserIntent.IMMUNITY):
|
|
144
|
+
model = DevPreferencesManager.get_coding_specialist_model()
|
|
145
|
+
if cls.is_model_live(model):
|
|
146
|
+
return model, f"🔨 Autonomous Coding ({intent_res.mode_label}): Routed to compiler-grounded specialist '{model}'"
|
|
147
|
+
|
|
148
|
+
# Fallback to verified live model
|
|
149
|
+
live_m, live_reason = cls.get_fallback_live_model()
|
|
150
|
+
return live_m, f"Standard route to {live_reason} ('{live_m}')"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class SmartModelRouter:
|
|
154
|
+
"""
|
|
155
|
+
Dynamic Cost & Latency Optimizer Model Router.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, hub: Optional[ModelHub] = None):
|
|
159
|
+
self.hub = hub or ModelHub()
|
|
160
|
+
self.total_queries_routed: int = 0
|
|
161
|
+
self.total_saved_usd: float = 0.0
|
|
162
|
+
|
|
163
|
+
def analyze_complexity(self, task_prompt: str, context_length: int = 0) -> Tuple[TaskTier, int, str]:
|
|
164
|
+
"""
|
|
165
|
+
Calculates complexity score (0-100) and returns TaskTier.
|
|
166
|
+
"""
|
|
167
|
+
text = task_prompt.lower()
|
|
168
|
+
score = 20 # base standard
|
|
169
|
+
|
|
170
|
+
words = set(re.findall(r"[\w-]+", text))
|
|
171
|
+
|
|
172
|
+
# Complexity boosters
|
|
173
|
+
if any(w in text for w in ("architect", "architecture", "refactor", "security", "distributed", "consensus", "concurrency", "lock-free", "ast", "compiler", "multi-thread")):
|
|
174
|
+
score += 45
|
|
175
|
+
if any(w in text for w in ("red-team", "adversarial", "cryptographic", "memory leak", "race condition")):
|
|
176
|
+
score += 30
|
|
177
|
+
if context_length > 20000:
|
|
178
|
+
score += 25
|
|
179
|
+
|
|
180
|
+
# Simplicity reducers (word boundary safe)
|
|
181
|
+
if any(w in words for w in ("typo", "docstring", "comment", "format", "rename", "hello", "hi", "hey")) or "add log" in text:
|
|
182
|
+
score -= 30
|
|
183
|
+
if len(task_prompt.split()) < 8 and "fix" not in text and score <= 20:
|
|
184
|
+
score -= 15
|
|
185
|
+
|
|
186
|
+
score = max(0, min(100, score))
|
|
187
|
+
|
|
188
|
+
if score < 30:
|
|
189
|
+
return TaskTier.TRIVIAL, score, "Low-complexity conversational/trivial task suitable for fast/local SLM."
|
|
190
|
+
elif score < 70:
|
|
191
|
+
return TaskTier.STANDARD, score, "Moderate complexity suitable for high-throughput inference."
|
|
192
|
+
else:
|
|
193
|
+
return TaskTier.COMPLEX, score, "High-complexity architectural task requiring frontier reasoning."
|
|
194
|
+
|
|
195
|
+
def route(self, task_prompt: str, context_length: int = 0, force_local: bool = False) -> RouteDecision:
|
|
196
|
+
"""
|
|
197
|
+
Decides the optimal model and estimates costs.
|
|
198
|
+
"""
|
|
199
|
+
tier, score, reason = self.analyze_complexity(task_prompt, context_length)
|
|
200
|
+
|
|
201
|
+
if force_local or tier == TaskTier.TRIVIAL:
|
|
202
|
+
selected_model = DevPreferencesManager.get_fast_chat_model()
|
|
203
|
+
provider = "fast-tier"
|
|
204
|
+
cost = 0.0000
|
|
205
|
+
elif tier == TaskTier.STANDARD:
|
|
206
|
+
selected_model = "deepseek-coder"
|
|
207
|
+
provider = "deepseek"
|
|
208
|
+
cost = 0.0002
|
|
209
|
+
else:
|
|
210
|
+
selected_model = DevPreferencesManager.get_frontier_reasoning_model()
|
|
211
|
+
provider = "frontier"
|
|
212
|
+
cost = 0.0030
|
|
213
|
+
|
|
214
|
+
# Compare against GPT-4 baseline ($0.030 per 1k input/output avg)
|
|
215
|
+
baseline_cost = 0.0300
|
|
216
|
+
savings = max(0.0, baseline_cost - cost)
|
|
217
|
+
|
|
218
|
+
self.total_queries_routed += 1
|
|
219
|
+
self.total_saved_usd += savings
|
|
220
|
+
|
|
221
|
+
return RouteDecision(
|
|
222
|
+
task=task_prompt,
|
|
223
|
+
tier=tier,
|
|
224
|
+
selected_model=selected_model,
|
|
225
|
+
selected_provider=provider,
|
|
226
|
+
estimated_cost_usd=cost,
|
|
227
|
+
baseline_gpt4_cost_usd=baseline_cost,
|
|
228
|
+
savings_usd=savings,
|
|
229
|
+
reasoning=f"Score: {score}/100. {reason}",
|
|
230
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
storage_manager.py - Persistent Local Storage & Session Resumption Engine for K-CLI
|
|
3
|
+
Project Bankai v1.0.0 — Built for AWS "Agents for Humans" Hackathon
|
|
4
|
+
Developer: Krishiv Joshi (@krishivjoshi)
|
|
5
|
+
|
|
6
|
+
Manages persistent local storage in ~/.kcli/:
|
|
7
|
+
- Multi-turn conversation sessions & checkpoints (~/.kcli/sessions/)
|
|
8
|
+
- API Keys & credentials (~/.kcli/credentials.env)
|
|
9
|
+
- Developer preferences & default models (~/.kcli/preferences.json)
|
|
10
|
+
- Enables 1-flag session continuation: `k-cli -c` or `k-cli --continue`
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import asdict, dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
HOME_DIR = Path.home()
|
|
23
|
+
STORAGE_DIR = Path(os.getenv("KCLI_HOME", HOME_DIR / ".kcli")).resolve()
|
|
24
|
+
SESSIONS_DIR = STORAGE_DIR / "sessions"
|
|
25
|
+
CREDS_FILE = STORAGE_DIR / "credentials.env"
|
|
26
|
+
PREFS_FILE = STORAGE_DIR / "preferences.json"
|
|
27
|
+
LATEST_SESSION_FILE = SESSIONS_DIR / "latest_session.json"
|
|
28
|
+
AUDIT_JOURNAL_FILE = STORAGE_DIR / "audit_journal.jsonl"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SessionTurn:
|
|
33
|
+
prompt: str
|
|
34
|
+
response: str
|
|
35
|
+
code: str = ""
|
|
36
|
+
success: bool = True
|
|
37
|
+
attempts: int = 1
|
|
38
|
+
patches_applied: bool = False
|
|
39
|
+
timestamp: float = field(default_factory=time.time)
|
|
40
|
+
routed_model: str = "auto"
|
|
41
|
+
persona: str = "CODER"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SessionCheckpoint:
|
|
46
|
+
session_id: str
|
|
47
|
+
workspace_dir: str
|
|
48
|
+
active_model: str
|
|
49
|
+
active_persona: str
|
|
50
|
+
context_files: List[str] = field(default_factory=list)
|
|
51
|
+
history: List[Dict[str, Any]] = field(default_factory=list)
|
|
52
|
+
git_branch: str = "main"
|
|
53
|
+
created_at: float = field(default_factory=time.time)
|
|
54
|
+
updated_at: float = field(default_factory=time.time)
|
|
55
|
+
total_tokens: int = 0
|
|
56
|
+
total_cost_saved: float = 0.0
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
59
|
+
return asdict(self)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_dict(cls, data: Dict[str, Any]) -> SessionCheckpoint:
|
|
63
|
+
return cls(
|
|
64
|
+
session_id=data.get("session_id", f"session_{int(time.time())}"),
|
|
65
|
+
workspace_dir=data.get("workspace_dir", "."),
|
|
66
|
+
active_model=data.get("active_model", "qwen2.5-coder:1.5b"),
|
|
67
|
+
active_persona=data.get("active_persona", "Fullstack AI Systems Engineer"),
|
|
68
|
+
context_files=data.get("context_files", []),
|
|
69
|
+
history=data.get("history", []),
|
|
70
|
+
git_branch=data.get("git_branch", "main"),
|
|
71
|
+
created_at=data.get("created_at", time.time()),
|
|
72
|
+
updated_at=data.get("updated_at", time.time()),
|
|
73
|
+
total_tokens=data.get("total_tokens", 0),
|
|
74
|
+
total_cost_saved=data.get("total_cost_saved", 0.0),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class LocalStorageManager:
|
|
79
|
+
"""Enterprise-grade local storage engine for persistent session state and zero-config resume."""
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def ensure_dirs(cls) -> None:
|
|
83
|
+
"""Ensures ~/.kcli and ~/.kcli/sessions directories exist with proper permissions."""
|
|
84
|
+
STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def save_session(
|
|
89
|
+
cls,
|
|
90
|
+
session_id: str,
|
|
91
|
+
workspace_dir: str,
|
|
92
|
+
active_model: str,
|
|
93
|
+
active_persona: str,
|
|
94
|
+
context_files: List[str],
|
|
95
|
+
history: List[Dict[str, Any]],
|
|
96
|
+
git_branch: str = "main",
|
|
97
|
+
total_tokens: int = 0,
|
|
98
|
+
total_cost_saved: float = 0.0,
|
|
99
|
+
) -> Path:
|
|
100
|
+
"""Persists a session checkpoint to ~/.kcli/sessions/ and updates latest_session.json."""
|
|
101
|
+
cls.ensure_dirs()
|
|
102
|
+
checkpoint = SessionCheckpoint(
|
|
103
|
+
session_id=session_id,
|
|
104
|
+
workspace_dir=str(Path(workspace_dir).resolve()),
|
|
105
|
+
active_model=active_model,
|
|
106
|
+
active_persona=active_persona,
|
|
107
|
+
context_files=context_files,
|
|
108
|
+
history=history,
|
|
109
|
+
git_branch=git_branch,
|
|
110
|
+
updated_at=time.time(),
|
|
111
|
+
total_tokens=total_tokens,
|
|
112
|
+
total_cost_saved=total_cost_saved,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Write timestamped session file
|
|
116
|
+
session_file = SESSIONS_DIR / f"{session_id}.json"
|
|
117
|
+
data_json = json.dumps(checkpoint.to_dict(), indent=2)
|
|
118
|
+
session_file.write_text(data_json, encoding="utf-8")
|
|
119
|
+
|
|
120
|
+
# Write pointer to latest session
|
|
121
|
+
LATEST_SESSION_FILE.write_text(data_json, encoding="utf-8")
|
|
122
|
+
|
|
123
|
+
return session_file
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def load_latest_session(cls) -> Optional[SessionCheckpoint]:
|
|
127
|
+
"""Loads the most recently saved session checkpoint for -c / --continue flag."""
|
|
128
|
+
cls.ensure_dirs()
|
|
129
|
+
if not LATEST_SESSION_FILE.exists():
|
|
130
|
+
# Check if any session files exist in sessions dir
|
|
131
|
+
session_files = sorted(SESSIONS_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
132
|
+
if not session_files:
|
|
133
|
+
return None
|
|
134
|
+
target_file = session_files[0]
|
|
135
|
+
else:
|
|
136
|
+
target_file = LATEST_SESSION_FILE
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
data = json.loads(target_file.read_text(encoding="utf-8"))
|
|
140
|
+
return SessionCheckpoint.from_dict(data)
|
|
141
|
+
except Exception:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def list_saved_sessions(cls) -> List[Dict[str, Any]]:
|
|
146
|
+
"""Returns metadata for all persisted sessions."""
|
|
147
|
+
cls.ensure_dirs()
|
|
148
|
+
results = []
|
|
149
|
+
for p in sorted(SESSIONS_DIR.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True):
|
|
150
|
+
if p.name == "latest_session.json":
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
154
|
+
results.append({
|
|
155
|
+
"session_id": data.get("session_id", p.stem),
|
|
156
|
+
"file": str(p),
|
|
157
|
+
"model": data.get("active_model", "unknown"),
|
|
158
|
+
"turns": len(data.get("history", [])),
|
|
159
|
+
"workspace": data.get("workspace_dir", "."),
|
|
160
|
+
"updated_at": data.get("updated_at", p.stat().st_mtime),
|
|
161
|
+
})
|
|
162
|
+
except Exception:
|
|
163
|
+
continue
|
|
164
|
+
return results
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def record_activity(cls, event_type: str, details: Dict[str, Any]) -> None:
|
|
168
|
+
"""Appends an immutable event record to ~/.kcli/audit_journal.jsonl."""
|
|
169
|
+
cls.ensure_dirs()
|
|
170
|
+
record = {
|
|
171
|
+
"timestamp": time.time(),
|
|
172
|
+
"event": event_type,
|
|
173
|
+
**details,
|
|
174
|
+
}
|
|
175
|
+
with open(AUDIT_JOURNAL_FILE, "a", encoding="utf-8") as f:
|
|
176
|
+
f.write(json.dumps(record) + "\n")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
viewport_engine.py - Hyper-Responsive Auto-Adjusting Viewport & Layout Engine for K-CLI
|
|
3
|
+
Project Bankai v1.0.0
|
|
4
|
+
|
|
5
|
+
Calculates optimal UI density, pane visibility, modal scaling, and typography
|
|
6
|
+
across all terminal sizes and displays (Compact <90cols, Standard 90-140cols, UltraWide >140cols).
|
|
7
|
+
Ensures zero visual clipping, zero clutter, and automatic responsive adaptation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Tuple, Dict, Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ViewportMode(str, Enum):
|
|
20
|
+
COMPACT = "compact" # < 95 cols: 1-pane focus mode, sidebars collapsed
|
|
21
|
+
STANDARD = "standard" # 95 - 140 cols: 2-pane mode (Sidebar + Canvas)
|
|
22
|
+
ULTRAWIDE = "ultrawide" # > 140 cols: 3-pane mode (Sidebar + Canvas + Drawer)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ViewportGeometry:
|
|
27
|
+
width: int
|
|
28
|
+
height: int
|
|
29
|
+
mode: ViewportMode
|
|
30
|
+
show_left_sidebar: bool
|
|
31
|
+
show_right_drawer: bool
|
|
32
|
+
sidebar_width: int
|
|
33
|
+
drawer_width: int
|
|
34
|
+
chat_width_fraction: float
|
|
35
|
+
hud_density: str
|
|
36
|
+
is_height_constrained: bool
|
|
37
|
+
|
|
38
|
+
def to_css_classes(self) -> list[str]:
|
|
39
|
+
classes = [f"viewport-{self.mode.value}"]
|
|
40
|
+
if not self.show_left_sidebar:
|
|
41
|
+
classes.append("hide-sidebar")
|
|
42
|
+
if not self.show_right_drawer:
|
|
43
|
+
classes.append("hide-drawer")
|
|
44
|
+
if self.is_height_constrained:
|
|
45
|
+
classes.append("height-compact")
|
|
46
|
+
return classes
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ViewportEngine:
|
|
50
|
+
"""
|
|
51
|
+
Real-time screen geometry and auto-adjusting layout calculator (<0.01ms).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def get_current_terminal_size(cls) -> Tuple[int, int]:
|
|
56
|
+
"""Gets (columns, rows) of current terminal with graceful fallback."""
|
|
57
|
+
try:
|
|
58
|
+
size = shutil.get_terminal_size(fallback=(120, 35))
|
|
59
|
+
return size.columns, size.lines
|
|
60
|
+
except Exception:
|
|
61
|
+
return 120, 35
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def compute_geometry(cls, width: Optional[int] = None, height: Optional[int] = None) -> ViewportGeometry:
|
|
65
|
+
"""
|
|
66
|
+
Calculates optimal layout dimensions and visibility flags based on viewport width & height.
|
|
67
|
+
"""
|
|
68
|
+
if width is None or height is None:
|
|
69
|
+
cur_w, cur_h = cls.get_current_terminal_size()
|
|
70
|
+
width = width if width is not None else cur_w
|
|
71
|
+
height = height if height is not None else cur_h
|
|
72
|
+
|
|
73
|
+
# Prevent degenerate dimensions
|
|
74
|
+
w = max(40, width)
|
|
75
|
+
h = max(15, height)
|
|
76
|
+
|
|
77
|
+
is_height_compact = h < 26
|
|
78
|
+
|
|
79
|
+
if w >= 140:
|
|
80
|
+
mode = ViewportMode.ULTRAWIDE
|
|
81
|
+
show_left = True
|
|
82
|
+
show_right = True
|
|
83
|
+
left_w = 26
|
|
84
|
+
right_w = 30
|
|
85
|
+
density = "full"
|
|
86
|
+
elif w >= 95:
|
|
87
|
+
mode = ViewportMode.STANDARD
|
|
88
|
+
show_left = True
|
|
89
|
+
show_right = False # Right drawer hidden to give maximum space to canvas
|
|
90
|
+
left_w = 22
|
|
91
|
+
right_w = 0
|
|
92
|
+
density = "standard"
|
|
93
|
+
else:
|
|
94
|
+
mode = ViewportMode.COMPACT
|
|
95
|
+
show_left = False
|
|
96
|
+
show_right = False # Pure 1-pane streamlined focus mode
|
|
97
|
+
left_w = 0
|
|
98
|
+
right_w = 0
|
|
99
|
+
density = "minimal"
|
|
100
|
+
|
|
101
|
+
# Calculate chat width ratio
|
|
102
|
+
occupied = (left_w if show_left else 0) + (right_w if show_right else 0)
|
|
103
|
+
remaining = max(30, w - occupied)
|
|
104
|
+
chat_fraction = remaining / w
|
|
105
|
+
|
|
106
|
+
return ViewportGeometry(
|
|
107
|
+
width=w,
|
|
108
|
+
height=h,
|
|
109
|
+
mode=mode,
|
|
110
|
+
show_left_sidebar=show_left,
|
|
111
|
+
show_right_drawer=show_right,
|
|
112
|
+
sidebar_width=left_w,
|
|
113
|
+
drawer_width=right_w,
|
|
114
|
+
chat_width_fraction=chat_fraction,
|
|
115
|
+
hud_density=density,
|
|
116
|
+
is_height_constrained=is_height_compact,
|
|
117
|
+
)
|