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/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
|
+
|