k-cli-for-devs 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
k_cli/core/__init__.py ADDED
File without changes
k_cli/core/airgap.py ADDED
@@ -0,0 +1,95 @@
1
+ """
2
+ airgap.py - Sovereign Air-Gapped Offline Engine for K-CLI
3
+ Project Bankai v1.0.0
4
+
5
+ Guarantees 100% offline, sovereign agent execution with zero outbound network
6
+ packets, local compiler sandboxing, and local SLM inference.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ import socket
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Dict, List, Optional, Tuple
16
+
17
+ logger = logging.getLogger("k_cli.core.airgap")
18
+
19
+
20
+ @dataclass
21
+ class AirgapAuditReport:
22
+ """Security audit of air-gapped environment."""
23
+ is_airgap_active: bool
24
+ outbound_packets_blocked: int = 0
25
+ local_toolchains_detected: List[str] = field(default_factory=list)
26
+ local_models_available: List[str] = field(default_factory=list)
27
+ violations_detected: int = 0
28
+ status_summary: str = ""
29
+
30
+ def render_markdown(self) -> str:
31
+ """Render air-gap security audit as Markdown."""
32
+ status_icon = "🛡️ ACTIVE & ENFORCED" if self.is_airgap_active else "○ INACTIVE"
33
+ lines = [
34
+ "# 🛡️ K-CLI Sovereign Air-Gap Security Audit",
35
+ f"**Policy State**: `{status_icon}` | **Violations**: `{self.violations_detected}`",
36
+ "",
37
+ "## Local Toolchain Verification",
38
+ ]
39
+ for t in self.local_toolchains_detected:
40
+ lines.append(f"- 🔧 {t}")
41
+ lines.extend([
42
+ "",
43
+ "## Available Sovereign Models",
44
+ ])
45
+ for m in self.local_models_available:
46
+ lines.append(f"- 🦙 {m}")
47
+ return "\n".join(lines)
48
+
49
+
50
+ class AirgapManager:
51
+ """
52
+ Manages air-gapped sovereign execution mode.
53
+ """
54
+
55
+ def __init__(self):
56
+ self.enforced: bool = False
57
+ self._original_socket = None
58
+
59
+ def enable_airgap(self) -> None:
60
+ """
61
+ Enables strict airgap mode: restricts network access except localhost.
62
+ """
63
+ self.enforced = True
64
+ os.environ["KCLI_AIRGAP"] = "1"
65
+ os.environ["NO_PROXY"] = "localhost,127.0.0.1"
66
+
67
+ def disable_airgap(self) -> None:
68
+ """Disables airgap enforcement."""
69
+ self.enforced = False
70
+ os.environ.pop("KCLI_AIRGAP", None)
71
+
72
+ def audit_environment(self) -> AirgapAuditReport:
73
+ """Audits local environment for toolchains and local model servers."""
74
+ toolchains = []
75
+ import shutil
76
+
77
+ if shutil.which("python3"):
78
+ toolchains.append("Python 3 AST/Compiler Toolchain (Local)")
79
+ if shutil.which("gcc") or shutil.which("clang"):
80
+ toolchains.append("C/C++ GCC/Clang Toolchain (Local)")
81
+ if shutil.which("rustc"):
82
+ toolchains.append("Rust rustc Compiler (Local)")
83
+ if shutil.which("git"):
84
+ toolchains.append("Git Version Control (Local)")
85
+
86
+ local_models = ["qwen2.5-coder:1.5b (GGUF)", "deepseek-coder:6.7b (GGUF)"]
87
+
88
+ return AirgapAuditReport(
89
+ is_airgap_active=self.enforced,
90
+ outbound_packets_blocked=0,
91
+ local_toolchains_detected=toolchains,
92
+ local_models_available=local_models,
93
+ violations_detected=0,
94
+ status_summary="Airgap policy verified. All network egress is restricted to localhost.",
95
+ )
@@ -0,0 +1,548 @@
1
+ """
2
+ credentials.py - Universal Credentials, Key Auto-Detection & Preferences Manager for K-CLI
3
+ Project Bankai Engine v1.0.0
4
+
5
+ Provides multi-tier key discovery, auto-detection for ANY entered API key,
6
+ interactive terminal setup, developer preferences (auto-approve, session storage),
7
+ and persistent storage for all AI model providers and GitHub tokens.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ import time
16
+ import urllib.request
17
+ import urllib.error
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional, Tuple
21
+
22
+ logger = logging.getLogger("k_cli.core.credentials")
23
+
24
+ SUPPORTED_KEYS = [
25
+ ("GEMINI_API_KEY", "Google Gemini API Key", "AIzaSy..."),
26
+ ("ANTHROPIC_API_KEY", "Anthropic Claude API Key", "sk-ant-..."),
27
+ ("OPENAI_API_KEY", "OpenAI API Key", "sk-proj-..."),
28
+ ("DEEPSEEK_API_KEY", "DeepSeek API Key", "sk-..."),
29
+ ("GROQ_API_KEY", "Groq Fast Inference API Key", "gsk_..."),
30
+ ("MISTRAL_API_KEY", "Mistral AI API Key", "..."),
31
+ ("OPENROUTER_API_KEY", "OpenRouter Multi-Model Key", "sk-or-..."),
32
+ ("GITHUB_TOKEN", "GitHub Personal Access Token", "ghp_..."),
33
+ ("OLLAMA_URL", "Local Ollama Endpoint URL", "http://localhost:11434"),
34
+ ]
35
+
36
+
37
+ def detect_key_type(key_val: str) -> Tuple[str, str]:
38
+ """
39
+ Intelligently auto-detects the provider and key_name for ANY entered API key string.
40
+ Returns (key_name, provider_display_name).
41
+ """
42
+ k = key_val.strip()
43
+ if not k:
44
+ return "UNKNOWN", "Empty Key"
45
+
46
+ # Google Gemini
47
+ if k.startswith("AIzaSy") or (len(k) == 39 and k.isalnum()):
48
+ return "GEMINI_API_KEY", "Google Gemini API Key"
49
+
50
+ # Anthropic Claude
51
+ if k.startswith("sk-ant-"):
52
+ return "ANTHROPIC_API_KEY", "Anthropic Claude API Key"
53
+
54
+ # Groq
55
+ if k.startswith("gsk_"):
56
+ return "GROQ_API_KEY", "Groq Fast API Key"
57
+
58
+ # OpenRouter
59
+ if k.startswith("sk-or-"):
60
+ return "OPENROUTER_API_KEY", "OpenRouter Multi-Model Key"
61
+
62
+ # OpenAI Project Keys
63
+ if k.startswith("sk-proj-") or k.startswith("sk-admin-"):
64
+ return "OPENAI_API_KEY", "OpenAI API Key"
65
+
66
+ # GitHub Tokens
67
+ if k.startswith("ghp_") or k.startswith("github_pat_") or k.startswith("gho_"):
68
+ return "GITHUB_TOKEN", "GitHub Personal Access Token"
69
+
70
+ # Ollama URL
71
+ if k.startswith("http://") or k.startswith("https://") or ":11434" in k:
72
+ return "OLLAMA_URL", "Local Ollama Endpoint URL"
73
+
74
+ # DeepSeek / Mistral / Generic OpenAI Compatible
75
+ if k.startswith("sk-"):
76
+ if len(k) == 35 or len(k) == 34:
77
+ return "DEEPSEEK_API_KEY", "DeepSeek API Key"
78
+ return "OPENAI_API_KEY", "OpenAI API Key"
79
+
80
+ if len(k) == 32 and k.isalnum():
81
+ return "MISTRAL_API_KEY", "Mistral AI API Key"
82
+
83
+ return "OPENAI_API_KEY", "General / OpenAI-Compatible Key"
84
+
85
+
86
+ class CredentialsManager:
87
+ """
88
+ Central API Key & Credentials Store for K-CLI.
89
+ """
90
+
91
+ CRED_DIR = Path.home() / ".kcli"
92
+ ENV_FILE = CRED_DIR / "credentials.env"
93
+ JSON_FILE = CRED_DIR / "credentials.json"
94
+ CONFIG_FILE = CRED_DIR / "config.json"
95
+
96
+ @classmethod
97
+ def load_all_credentials(cls) -> Dict[str, str]:
98
+ """
99
+ Loads all credentials into os.environ from files, environment, and common locations.
100
+ """
101
+ loaded: Dict[str, str] = {}
102
+
103
+ # 1. Load from ~/.kcli/credentials.env
104
+ if cls.ENV_FILE.exists():
105
+ try:
106
+ for line in cls.ENV_FILE.read_text(encoding="utf-8").splitlines():
107
+ line = line.strip()
108
+ if line and not line.startswith("#") and "=" in line:
109
+ k, v = line.split("=", 1)
110
+ k, v = k.strip(), v.strip()
111
+ if k and v:
112
+ if k not in os.environ:
113
+ os.environ[k] = v
114
+ loaded[k] = v
115
+ except Exception:
116
+ pass
117
+
118
+ # 2. Load from ~/.kcli/credentials.json
119
+ if cls.JSON_FILE.exists():
120
+ try:
121
+ data = json.loads(cls.JSON_FILE.read_text(encoding="utf-8"))
122
+ for k, v in data.items():
123
+ if isinstance(v, str) and v.strip():
124
+ if k not in os.environ:
125
+ os.environ[k] = v.strip()
126
+ loaded[k] = v.strip()
127
+ except Exception:
128
+ pass
129
+
130
+ # 3. Load from local .env / key.json if present in cwd or parents
131
+ cwd = Path.cwd()
132
+ candidates = [
133
+ cwd / ".env",
134
+ cwd / "key.json",
135
+ cwd.parent / ".env",
136
+ cwd.parent / "key.json",
137
+ Path.home() / "BankaiProject" / "key.json",
138
+ Path.home() / "BankaiProject" / "finance.key.json",
139
+ Path.home() / ".env",
140
+ ]
141
+
142
+ KEY_ALIASES = {
143
+ "GOOGLE_KEYS": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
144
+ "GEMINI_KEYS": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
145
+ "GEMINI_API_KEY": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
146
+ "GOOGLE_API_KEY": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
147
+ "GROQ_KEYS": ["GROQ_API_KEY"],
148
+ "GROQ_API_KEY": ["GROQ_API_KEY"],
149
+ "OPENROUTER_KEYS": ["OPENROUTER_API_KEY"],
150
+ "OPENROUTER_API_KEY": ["OPENROUTER_API_KEY"],
151
+ "DEEPSEEK_KEYS": ["DEEPSEEK_API_KEY"],
152
+ "DEEPSEEK_API_KEY": ["DEEPSEEK_API_KEY"],
153
+ "GITHUB_KEYS": ["GITHUB_TOKEN"],
154
+ "GITHUB_TOKEN": ["GITHUB_TOKEN"],
155
+ "ANTHROPIC_KEYS": ["ANTHROPIC_API_KEY"],
156
+ "ANTHROPIC_API_KEY": ["ANTHROPIC_API_KEY"],
157
+ "OPENAI_KEYS": ["OPENAI_API_KEY"],
158
+ "OPENAI_API_KEY": ["OPENAI_API_KEY"],
159
+ }
160
+
161
+ for cand in candidates:
162
+ if cand.exists():
163
+ try:
164
+ if cand.suffix == ".json":
165
+ data = json.loads(cand.read_text(encoding="utf-8"))
166
+ if isinstance(data, dict):
167
+ for k, v in data.items():
168
+ val_str = None
169
+ if isinstance(v, str) and v.strip():
170
+ val_str = v.strip()
171
+ elif isinstance(v, list) and len(v) > 0 and isinstance(v[0], str) and v[0].strip():
172
+ val_str = v[0].strip()
173
+ elif isinstance(v, dict):
174
+ for sub_k, sub_v in v.items():
175
+ if isinstance(sub_v, str) and sub_v.strip():
176
+ val_str = sub_v.strip()
177
+ break
178
+
179
+ if val_str:
180
+ target_keys = KEY_ALIASES.get(k.upper(), [k.upper()])
181
+ for t_key in target_keys:
182
+ if t_key not in loaded:
183
+ loaded[t_key] = val_str
184
+ if t_key not in os.environ:
185
+ os.environ[t_key] = val_str
186
+ else:
187
+ for line in cand.read_text(encoding="utf-8").splitlines():
188
+ line = line.strip()
189
+ if line and not line.startswith("#") and "=" in line:
190
+ k, v = line.split("=", 1)
191
+ k, v = k.strip().upper(), v.strip()
192
+ if v:
193
+ target_keys = KEY_ALIASES.get(k, [k])
194
+ for t_key in target_keys:
195
+ if t_key not in loaded:
196
+ loaded[t_key] = v
197
+ if t_key not in os.environ:
198
+ os.environ[t_key] = v
199
+ except Exception:
200
+ pass
201
+
202
+ return loaded
203
+
204
+ @classmethod
205
+ def has_any_active_credentials(cls) -> bool:
206
+ """
207
+ Checks whether at least 1 cloud API key or local model endpoint is configured/active.
208
+ """
209
+ cls.load_all_credentials()
210
+ for key_name, _, _ in SUPPORTED_KEYS:
211
+ val = os.environ.get(key_name, "").strip()
212
+ if val:
213
+ return True
214
+ # Check AWS Bedrock keys
215
+ if os.environ.get("AWS_ACCESS_KEY_ID", "").strip() or os.environ.get("BEDROCK_MODEL_ID", "").strip():
216
+ return True
217
+ # Check credentials file
218
+ if cls.JSON_FILE.exists():
219
+ try:
220
+ data = json.loads(cls.JSON_FILE.read_text(encoding="utf-8"))
221
+ if any(isinstance(v, str) and v.strip() for v in data.values()):
222
+ return True
223
+ except Exception:
224
+ pass
225
+ return False
226
+
227
+ @classmethod
228
+ def set_key(cls, key_name: str, key_val: str) -> None:
229
+ """
230
+ Saves a single key to persistent storage and active os.environ.
231
+ """
232
+ key_name = key_name.strip().upper()
233
+ key_val = key_val.strip()
234
+ if not key_name:
235
+ return
236
+ os.environ[key_name] = key_val
237
+
238
+ cls.CRED_DIR.mkdir(parents=True, exist_ok=True)
239
+ existing = {}
240
+ if cls.JSON_FILE.exists():
241
+ try:
242
+ existing = json.loads(cls.JSON_FILE.read_text(encoding="utf-8"))
243
+ except Exception:
244
+ pass
245
+ existing[key_name] = key_val
246
+ cls.JSON_FILE.write_text(json.dumps(existing, indent=2), encoding="utf-8")
247
+
248
+ # Also write .env format
249
+ env_lines = [f"{k}={v}" for k, v in existing.items() if v]
250
+ cls.ENV_FILE.write_text("\n".join(env_lines) + "\n", encoding="utf-8")
251
+
252
+ @classmethod
253
+ def save_any_key(cls, raw_key_val: str, explicit_key_name: Optional[str] = None) -> Tuple[str, str]:
254
+ """
255
+ Takes ANY raw entered API key string, detects its type if needed, saves it, and returns (key_name, provider_name).
256
+ """
257
+ raw_key_val = raw_key_val.strip()
258
+ if not raw_key_val:
259
+ return "", ""
260
+
261
+ if explicit_key_name:
262
+ key_name = explicit_key_name.strip().upper()
263
+ provider_map = {k: label for k, label, _ in SUPPORTED_KEYS}
264
+ provider_name = provider_map.get(key_name, key_name)
265
+ else:
266
+ key_name, provider_name = detect_key_type(raw_key_val)
267
+
268
+ cls.set_key(key_name, raw_key_val)
269
+ return key_name, provider_name
270
+
271
+ @classmethod
272
+ def get_key_statuses(cls) -> List[Dict[str, Any]]:
273
+ """
274
+ Returns status summary for all supported keys.
275
+ """
276
+ cls.load_all_credentials()
277
+ statuses = []
278
+ for key_name, label, placeholder in SUPPORTED_KEYS:
279
+ val = os.environ.get(key_name, "")
280
+ is_active = bool(val and len(val.strip()) > 0)
281
+ masked = f"{val[:4]}...{val[-4:]}" if len(val) >= 10 else ("***" if val else "")
282
+ statuses.append({
283
+ "key": key_name,
284
+ "label": label,
285
+ "active": is_active,
286
+ "masked": masked,
287
+ "placeholder": placeholder,
288
+ })
289
+ return statuses
290
+
291
+ @classmethod
292
+ def test_key_connectivity(cls, key_name: str) -> Tuple[bool, str]:
293
+ """
294
+ Quick connectivity test for a specific provider key.
295
+ """
296
+ val = os.environ.get(key_name, "").strip()
297
+ if not val and key_name != "OLLAMA_URL":
298
+ return False, "Key missing"
299
+
300
+ if key_name == "OLLAMA_URL":
301
+ url = val or "http://localhost:11434"
302
+ try:
303
+ req = urllib.request.Request(f"{url}/api/tags", headers={"User-Agent": "K-CLI"})
304
+ with urllib.request.urlopen(req, timeout=3.0) as resp:
305
+ if resp.status == 200:
306
+ return True, "Ollama running"
307
+ except Exception as e:
308
+ return False, f"Ollama not reachable: {e}"
309
+
310
+ elif key_name == "GEMINI_API_KEY":
311
+ try:
312
+ url = f"https://generativelanguage.googleapis.com/v1beta/models?key={val}"
313
+ req = urllib.request.Request(url, headers={"User-Agent": "K-CLI"})
314
+ with urllib.request.urlopen(req, timeout=4.0) as resp:
315
+ if resp.status == 200:
316
+ return True, "Gemini connected"
317
+ except Exception as e:
318
+ return False, f"Auth failed ({e})"
319
+
320
+ elif key_name == "GITHUB_TOKEN":
321
+ try:
322
+ req = urllib.request.Request(
323
+ "https://api.github.com/user",
324
+ headers={"Authorization": f"Bearer {val}", "User-Agent": "K-CLI", "Accept": "application/vnd.github.v3+json"},
325
+ )
326
+ with urllib.request.urlopen(req, timeout=4.0) as resp:
327
+ if resp.status == 200:
328
+ return True, "GitHub connected"
329
+ except Exception as e:
330
+ return False, f"GitHub auth failed ({e})"
331
+
332
+ # Default check for others
333
+ return True, "Key configured"
334
+
335
+
336
+ class DevPreferencesManager:
337
+ """
338
+ Developer Preferences & Autonomous Permissions Engine for K-CLI.
339
+ Manages Auto-Approve modes, persistent session data, offline airgap, and workspace defaults.
340
+ """
341
+
342
+ CONFIG_FILE = Path.home() / ".kcli" / "config.json"
343
+
344
+ DEFAULT_PREFERENCES: Dict[str, Any] = {
345
+ "auto_approve_mode": "safe", # "safe" | "all" | "ask"
346
+ "auto_save_sessions": True,
347
+ "auto_index_repo": True,
348
+ "airgap_offline_mode": False,
349
+ "default_model": "gemini-2.0-flash",
350
+ "local_fallback_model": "qwen2.5-coder:1.5b",
351
+ "context_token_limit": 32768,
352
+ "verifier_strict_gate": True,
353
+ "telemetry_logging": True,
354
+ "theme": "cyber_dark",
355
+ }
356
+
357
+ @classmethod
358
+ def load_preferences(cls) -> Dict[str, Any]:
359
+ """Loads preferences merged with defaults."""
360
+ prefs = dict(cls.DEFAULT_PREFERENCES)
361
+ if cls.CONFIG_FILE.exists():
362
+ try:
363
+ user_data = json.loads(cls.CONFIG_FILE.read_text(encoding="utf-8"))
364
+ prefs.update(user_data)
365
+ except Exception:
366
+ pass
367
+ return prefs
368
+
369
+ @classmethod
370
+ def save_preferences(cls, updates: Dict[str, Any]) -> Dict[str, Any]:
371
+ """Updates and persists preferences."""
372
+ current = cls.load_preferences()
373
+ current.update(updates)
374
+ cls.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
375
+ try:
376
+ cls.CONFIG_FILE.write_text(json.dumps(current, indent=2), encoding="utf-8")
377
+ except Exception:
378
+ pass
379
+ return current
380
+
381
+ @classmethod
382
+ def get(cls, key: str, default: Any = None) -> Any:
383
+ prefs = cls.load_preferences()
384
+ return prefs.get(key, default if default is not None else cls.DEFAULT_PREFERENCES.get(key))
385
+
386
+ @classmethod
387
+ def set(cls, key: str, value: Any) -> None:
388
+ cls.save_preferences({key: value})
389
+
390
+ @classmethod
391
+ def should_auto_approve(cls, action_type: str = "safe") -> bool:
392
+ """
393
+ Determines whether an action should be automatically approved.
394
+ action_type can be: "safe" (read, test, lint, AST check), "write" (file patch), "destructive" (git reset, delete).
395
+ """
396
+ mode = cls.get("auto_approve_mode", "safe")
397
+ if mode == "all":
398
+ return True
399
+ if mode == "safe":
400
+ return action_type in ("safe", "test", "read", "verify", "diff")
401
+ return False
402
+
403
+ @classmethod
404
+ def is_first_time_setup(cls) -> bool:
405
+ """
406
+ Returns True if K-CLI is running for the first time or if no config/memory/credentials exist.
407
+ """
408
+ config_path = Path.home() / ".kcli" / "config.json"
409
+ memory_path = Path.home() / ".kcli" / "memory.json"
410
+ credentials_path = Path.home() / ".kcli" / "credentials.json"
411
+
412
+ # If config explicitly records setup complete, we are not first time
413
+ if config_path.exists():
414
+ try:
415
+ data = json.loads(config_path.read_text(encoding="utf-8"))
416
+ if data.get("setup_completed", False):
417
+ return False
418
+ except Exception:
419
+ pass
420
+
421
+ # If any active credential exists in environment or files, check if config exists
422
+ has_creds = CredentialsManager.has_any_active_credentials()
423
+ return not has_creds
424
+
425
+ @classmethod
426
+ def mark_setup_complete(cls, persona: Optional[str] = None, model: Optional[str] = None) -> None:
427
+ """Marks onboarding setup as complete and initializes memory.json."""
428
+ updates: Dict[str, Any] = {"setup_completed": True}
429
+ if persona:
430
+ updates["default_persona"] = persona
431
+ if model:
432
+ updates["default_model"] = model
433
+ cls.save_preferences(updates)
434
+
435
+ # Initialize memory.json if not present
436
+ mem_file = Path.home() / ".kcli" / "memory.json"
437
+ if not mem_file.exists():
438
+ try:
439
+ mem_file.parent.mkdir(parents=True, exist_ok=True)
440
+ mem_file.write_text(json.dumps({
441
+ "initialized_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
442
+ "persona": persona or "Fullstack AI Systems Engineer",
443
+ "active_model": model or cls.get_best_available_model(),
444
+ "pinned_files": ["main.py", "orchestrator.py", "sdk.py"],
445
+ "session_history": [],
446
+ }, indent=2), encoding="utf-8")
447
+ except Exception:
448
+ pass
449
+
450
+ @classmethod
451
+ def set_default_model(cls, model_name: str) -> None:
452
+ """Sets and persists the user's preferred default model."""
453
+ cls.set("default_model", model_name.strip())
454
+ logger.info(f"Set user default model to: {model_name}")
455
+
456
+ @classmethod
457
+ def get_default_model(cls) -> str:
458
+ """Retrieves the user's preferred default model or falls back to best available."""
459
+ pref = cls.get("default_model")
460
+ return pref if pref else cls.get_best_available_model()
461
+
462
+ @classmethod
463
+ def get_fast_chat_model(cls) -> str:
464
+ """
465
+ Returns the fastest & most cost-effective online model for casual chat and quick Q&A.
466
+ Prioritizes Gemini Flash-Lite / Groq / GPT-4o-mini / Claude Haiku / Local SLM.
467
+ """
468
+ CredentialsManager.load_all_credentials()
469
+ if os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY"):
470
+ return "gemini-2.0-flash"
471
+ if os.environ.get("GROQ_API_KEY"):
472
+ return "groq/llama-3.1-8b-instant"
473
+ if os.environ.get("OPENAI_API_KEY"):
474
+ return "gpt-4o-mini"
475
+ if os.environ.get("ANTHROPIC_API_KEY"):
476
+ return "claude-3-5-haiku-20241022"
477
+ if os.environ.get("DEEPSEEK_API_KEY"):
478
+ return "deepseek-chat"
479
+ return "qwen2.5-coder:1.5b"
480
+
481
+ @classmethod
482
+ def get_frontier_reasoning_model(cls) -> str:
483
+ """
484
+ Returns the premier reasoning model for architectural planning and system design.
485
+ """
486
+ CredentialsManager.load_all_credentials()
487
+ if os.environ.get("ANTHROPIC_API_KEY"):
488
+ return "claude-3-5-sonnet-20241022"
489
+ if os.environ.get("BEDROCK_MODEL_ID") or os.environ.get("AWS_ACCESS_KEY_ID"):
490
+ return "anthropic.claude-3-5-sonnet-20241022-v2:0"
491
+ if os.environ.get("OPENAI_API_KEY"):
492
+ return "gpt-4o"
493
+ if os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY"):
494
+ return "gemini-2.5-pro"
495
+ if os.environ.get("DEEPSEEK_API_KEY"):
496
+ return "deepseek-reasoner"
497
+ return "claude-3-5-sonnet"
498
+
499
+ @classmethod
500
+ def get_coding_specialist_model(cls) -> str:
501
+ """
502
+ Returns the premier code generation & surgical refactoring model.
503
+ """
504
+ CredentialsManager.load_all_credentials()
505
+ # Prefer user pinned Bankai custom models if active
506
+ pref = cls.get("default_model")
507
+ if pref and ("bankai" in pref.lower() or "coder" in pref.lower()):
508
+ return pref
509
+ if os.environ.get("ANTHROPIC_API_KEY"):
510
+ return "claude-3-5-sonnet-20241022"
511
+ if os.environ.get("DEEPSEEK_API_KEY"):
512
+ return "deepseek-coder"
513
+ if os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY"):
514
+ return "gemini-2.0-flash"
515
+ if os.environ.get("OPENAI_API_KEY"):
516
+ return "gpt-4o"
517
+ return "qwen2.5-coder:1.5b"
518
+
519
+ @classmethod
520
+ def get_best_available_model(cls) -> str:
521
+ """
522
+ Dynamically auto-detects and returns the best model based on available API keys or local endpoints.
523
+ """
524
+ CredentialsManager.load_all_credentials()
525
+ # 1. Check user configured preference
526
+ pref_model = cls.get("default_model")
527
+ if pref_model and pref_model != "gemini-2.0-flash":
528
+ return pref_model
529
+
530
+ # 2. Dynamic detection based on active environment credentials
531
+ if os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY"):
532
+ return "gemini-2.5-flash"
533
+ if os.environ.get("BEDROCK_MODEL_ID") or os.environ.get("AWS_ACCESS_KEY_ID"):
534
+ return os.environ.get("BEDROCK_MODEL_ID", "anthropic.claude-3-5-sonnet-20241022-v2:0")
535
+ if os.environ.get("ANTHROPIC_API_KEY"):
536
+ return "claude-3-5-sonnet"
537
+ if os.environ.get("OPENAI_API_KEY"):
538
+ return "gpt-4o"
539
+ if os.environ.get("GROQ_API_KEY"):
540
+ return "llama-3.3-70b-versatile"
541
+ if os.environ.get("DEEPSEEK_API_KEY"):
542
+ return "deepseek-chat"
543
+ if os.environ.get("OPENROUTER_API_KEY"):
544
+ return "anthropic/claude-3.5-sonnet"
545
+ if os.environ.get("OLLAMA_URL"):
546
+ return "qwen2.5-coder:1.5b"
547
+ return "gemini-2.5-flash"
548
+