local-coding-agent 1.1.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.
- local_coding_agent/__init__.py +314 -0
- local_coding_agent/__main__.py +5 -0
- local_coding_agent/acp_server/__init__.py +38 -0
- local_coding_agent/acp_server/_codec.py +95 -0
- local_coding_agent/acp_server/_protocol.py +13 -0
- local_coding_agent/acp_server/_server.py +663 -0
- local_coding_agent/acp_server/_session.py +24 -0
- local_coding_agent/acp_server/_tools.py +126 -0
- local_coding_agent/ast_compactor.py +130 -0
- local_coding_agent/atomizer.py +77 -0
- local_coding_agent/benchmark/__init__.py +57 -0
- local_coding_agent/benchmark/_cases.py +307 -0
- local_coding_agent/benchmark/_judge.py +175 -0
- local_coding_agent/benchmark/_oracles.py +157 -0
- local_coding_agent/benchmark/_runner.py +259 -0
- local_coding_agent/benchmark/_stats.py +22 -0
- local_coding_agent/benchmark/_summary.py +143 -0
- local_coding_agent/benchmark_oracle_worker.py +169 -0
- local_coding_agent/calibration.py +154 -0
- local_coding_agent/capability.py +450 -0
- local_coding_agent/chat_compaction.py +276 -0
- local_coding_agent/cli/__init__.py +12 -0
- local_coding_agent/cli/_dispatch.py +377 -0
- local_coding_agent/cli/_handlers.py +320 -0
- local_coding_agent/cli/_handlers2.py +725 -0
- local_coding_agent/cli/_input.py +61 -0
- local_coding_agent/cli/_main.py +159 -0
- local_coding_agent/cli/_parser.py +327 -0
- local_coding_agent/context_manager.py +155 -0
- local_coding_agent/controller/__init__.py +18 -0
- local_coding_agent/controller/_constants.py +108 -0
- local_coding_agent/controller/_controller.py +1008 -0
- local_coding_agent/controller/_post_apply.py +216 -0
- local_coding_agent/delegator.py +180 -0
- local_coding_agent/desktop/__init__.py +8 -0
- local_coding_agent/desktop/app.py +100 -0
- local_coding_agent/desktop/assets/lucide.min.js +12 -0
- local_coding_agent/desktop/assets/tailwind.css +1 -0
- local_coding_agent/desktop/client_js.py +1271 -0
- local_coding_agent/desktop/components.py +555 -0
- local_coding_agent/desktop/server/__init__.py +37 -0
- local_coding_agent/desktop/server/_handlers.py +2178 -0
- local_coding_agent/desktop/server/_models.py +204 -0
- local_coding_agent/desktop/server/_server.py +347 -0
- local_coding_agent/desktop/server/_telemetry.py +87 -0
- local_coding_agent/desktop/styles.py +76 -0
- local_coding_agent/desktop/ui.py +53 -0
- local_coding_agent/doctor.py +413 -0
- local_coding_agent/hooks/__init__.py +19 -0
- local_coding_agent/hooks/_adapters.py +458 -0
- local_coding_agent/hooks/_bridge.py +346 -0
- local_coding_agent/hooks/_decision.py +26 -0
- local_coding_agent/lsp/__init__.py +60 -0
- local_coding_agent/lsp/_client.py +322 -0
- local_coding_agent/lsp/_fallback.py +297 -0
- local_coding_agent/lsp/_manager.py +173 -0
- local_coding_agent/lsp/_mock.py +200 -0
- local_coding_agent/lsp/_types.py +398 -0
- local_coding_agent/mcp_config.py +398 -0
- local_coding_agent/mcp_server.py +377 -0
- local_coding_agent/memory.py +140 -0
- local_coding_agent/mode_router.py +200 -0
- local_coding_agent/model_scanner.py +525 -0
- local_coding_agent/monitor.py +635 -0
- local_coding_agent/observation_policy.py +184 -0
- local_coding_agent/ollama_adapter.py +736 -0
- local_coding_agent/plan_mode.py +653 -0
- local_coding_agent/prescriptions.py +278 -0
- local_coding_agent/profiles.py +239 -0
- local_coding_agent/repository_tools.py +782 -0
- local_coding_agent/ripgrep.py +326 -0
- local_coding_agent/semantic_linter.py +122 -0
- local_coding_agent/service.py +405 -0
- local_coding_agent/session_events.py +684 -0
- local_coding_agent/session_query.py +578 -0
- local_coding_agent/skill_config.py +426 -0
- local_coding_agent/smoke.py +268 -0
- local_coding_agent/speculative_racing.py +85 -0
- local_coding_agent/spill.py +258 -0
- local_coding_agent/stats.py +255 -0
- local_coding_agent/stdio.py +132 -0
- local_coding_agent/subagent.py +516 -0
- local_coding_agent/task.py +90 -0
- local_coding_agent/task_store.py +142 -0
- local_coding_agent/tasks.py +240 -0
- local_coding_agent/terminal/__init__.py +35 -0
- local_coding_agent/terminal/_manager.py +156 -0
- local_coding_agent/terminal/_process.py +148 -0
- local_coding_agent/terminal/_session.py +452 -0
- local_coding_agent/terminal/_tools.py +233 -0
- local_coding_agent/terminal/_types.py +60 -0
- local_coding_agent/tool_call_parser.py +178 -0
- local_coding_agent/validators.py +563 -0
- local_coding_agent/vram_fit.py +240 -0
- local_coding_agent/worker_pool.py +516 -0
- local_coding_agent-1.1.0.dist-info/METADATA +202 -0
- local_coding_agent-1.1.0.dist-info/RECORD +101 -0
- local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
- local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
- local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
- local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""CSS stylesheets, Tailwind configuration, and design tokens for Desktop AI Coding Harness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
DESKTOP_CSS = """
|
|
6
|
+
* { box-sizing: border-box; }
|
|
7
|
+
|
|
8
|
+
:root.dark {
|
|
9
|
+
--bg-app: #09090b;
|
|
10
|
+
--bg-header: #0e0e11;
|
|
11
|
+
--bg-sidebar: #0e0e11;
|
|
12
|
+
--bg-card: #131316;
|
|
13
|
+
--bg-card-subtle: #18181b;
|
|
14
|
+
--bg-input: #09090b;
|
|
15
|
+
--border-main: #27272a;
|
|
16
|
+
--border-subtle: #1f1f23;
|
|
17
|
+
--text-main: #f4f4f5;
|
|
18
|
+
--text-muted: #a1a1aa;
|
|
19
|
+
--text-subtle: #71717a;
|
|
20
|
+
--diff-add-bg: rgba(16, 185, 129, 0.12);
|
|
21
|
+
--diff-add-text: #86efac;
|
|
22
|
+
--diff-del-bg: rgba(239, 68, 68, 0.12);
|
|
23
|
+
--diff-del-text: #fca5a5;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:root:not(.dark) {
|
|
27
|
+
--bg-app: #fbfbfb;
|
|
28
|
+
--bg-header: #ffffff;
|
|
29
|
+
--bg-sidebar: #f4f4f5;
|
|
30
|
+
--bg-card: #ffffff;
|
|
31
|
+
--bg-card-subtle: #f4f4f5;
|
|
32
|
+
--bg-input: #ffffff;
|
|
33
|
+
--border-main: #e4e4e7;
|
|
34
|
+
--border-subtle: #ebebef;
|
|
35
|
+
--text-main: #09090b;
|
|
36
|
+
--text-muted: #52525b;
|
|
37
|
+
--text-subtle: #71717a;
|
|
38
|
+
--diff-add-bg: rgba(16, 185, 129, 0.10);
|
|
39
|
+
--diff-add-text: #059669;
|
|
40
|
+
--diff-del-bg: rgba(239, 68, 68, 0.10);
|
|
41
|
+
--diff-del-text: #dc2626;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
body {
|
|
45
|
+
background-color: var(--bg-app);
|
|
46
|
+
color: var(--text-main);
|
|
47
|
+
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
|
48
|
+
-webkit-font-smoothing: antialiased;
|
|
49
|
+
-moz-osx-font-smoothing: grayscale;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
::-webkit-scrollbar { width: 4px; height: 4px; }
|
|
53
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
54
|
+
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 2px; }
|
|
55
|
+
:root:not(.dark) ::-webkit-scrollbar-thumb { background: #d4d4d8; }
|
|
56
|
+
|
|
57
|
+
.diff-del-bg { background-color: var(--diff-del-bg); color: var(--diff-del-text); }
|
|
58
|
+
.diff-add-bg { background-color: var(--diff-add-bg); color: var(--diff-add-text); }
|
|
59
|
+
.num-tabular { font-variant-numeric: tabular-nums; }
|
|
60
|
+
|
|
61
|
+
@keyframes spin {
|
|
62
|
+
from { transform: rotate(0deg); }
|
|
63
|
+
to { transform: rotate(360deg); }
|
|
64
|
+
}
|
|
65
|
+
.animate-spin-fast {
|
|
66
|
+
animation: spin 0.6s linear infinite;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@keyframes pulse-subtle {
|
|
70
|
+
0%, 100% { opacity: 1; }
|
|
71
|
+
50% { opacity: 0.5; }
|
|
72
|
+
}
|
|
73
|
+
.animate-pulse-subtle {
|
|
74
|
+
animation: pulse-subtle 1.8s ease-in-out infinite;
|
|
75
|
+
}
|
|
76
|
+
"""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Embedded production-grade HTML template and dynamic assembler for Desktop AI Coding Harness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from .client_js import DESKTOP_CLIENT_JS
|
|
8
|
+
from .components import (
|
|
9
|
+
render_chat_panel,
|
|
10
|
+
render_delegated_panel,
|
|
11
|
+
render_header,
|
|
12
|
+
render_modals,
|
|
13
|
+
render_sidebar,
|
|
14
|
+
render_toast,
|
|
15
|
+
)
|
|
16
|
+
from .styles import DESKTOP_CSS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def render_desktop_html(mutation_token: str = "") -> str:
|
|
20
|
+
"""Assemble complete standalone single-page HTML application from modular components."""
|
|
21
|
+
parts = [
|
|
22
|
+
"<!DOCTYPE html>",
|
|
23
|
+
'<html lang="en" class="dark">',
|
|
24
|
+
"<head>",
|
|
25
|
+
' <meta charset="UTF-8">',
|
|
26
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
27
|
+
" <title>Local AI Coding Harness</title>",
|
|
28
|
+
' <link rel="stylesheet" href="/assets/tailwind.css">',
|
|
29
|
+
' <script src="/assets/lucide.min.js"></script>',
|
|
30
|
+
" <style>",
|
|
31
|
+
DESKTOP_CSS,
|
|
32
|
+
" </style>",
|
|
33
|
+
"</head>",
|
|
34
|
+
'<body class="h-screen flex flex-col font-sans tracking-tight overflow-hidden">',
|
|
35
|
+
render_header(),
|
|
36
|
+
' <main class="flex-1 flex overflow-hidden relative">',
|
|
37
|
+
render_sidebar(),
|
|
38
|
+
render_chat_panel(),
|
|
39
|
+
render_delegated_panel(),
|
|
40
|
+
" </main>",
|
|
41
|
+
render_modals(),
|
|
42
|
+
render_toast(),
|
|
43
|
+
f" <script>window.DESKTOP_MUTATION_TOKEN = {json.dumps(mutation_token)};</script>",
|
|
44
|
+
" <script>",
|
|
45
|
+
DESKTOP_CLIENT_JS,
|
|
46
|
+
" </script>",
|
|
47
|
+
"</body>",
|
|
48
|
+
"</html>",
|
|
49
|
+
]
|
|
50
|
+
return "\n".join(parts)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
DESKTOP_HTML_TEMPLATE = render_desktop_html()
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Diagnostic wizard and environment validator for local-coding-agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import asdict, dataclass, field
|
|
13
|
+
from typing import Any, Sequence
|
|
14
|
+
|
|
15
|
+
from .ollama_adapter import OllamaClient
|
|
16
|
+
from .profiles import list_profiles
|
|
17
|
+
|
|
18
|
+
RECOMMENDED_PROFILES = [
|
|
19
|
+
{
|
|
20
|
+
"name": "qwen3-8b-q6k",
|
|
21
|
+
"model": "qwen3-8b-q6k:latest",
|
|
22
|
+
"description": "Recommended for 8-12 GB VRAM (Ultra-fast, high precision coding)",
|
|
23
|
+
"pull_cmd": "ollama run hf.co/unsloth/Qwen3-8B-GGUF:Q6_K",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "qwen3.8-27b-q4",
|
|
27
|
+
"model": "qwen3.8-27b-q4:latest",
|
|
28
|
+
"description": "Recommended for 16-24 GB VRAM (Maximum capability & architecture reasoning)",
|
|
29
|
+
"pull_cmd": "ollama run hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "qwen2.5-coder",
|
|
33
|
+
"model": "qwen2.5-coder:latest",
|
|
34
|
+
"description": "Standard coding workhorse (7B / 14B)",
|
|
35
|
+
"pull_cmd": "ollama pull qwen2.5-coder",
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CheckResult:
|
|
42
|
+
name: str
|
|
43
|
+
status: str # "ok" | "warn" | "fail"
|
|
44
|
+
message: str
|
|
45
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_ok(self) -> bool:
|
|
49
|
+
return self.status == "ok"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_warn(self) -> bool:
|
|
53
|
+
return self.status == "warn"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_fail(self) -> bool:
|
|
57
|
+
return self.status == "fail"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class DoctorReport:
|
|
62
|
+
healthy: bool
|
|
63
|
+
checks: list[CheckResult]
|
|
64
|
+
models: dict[str, Any]
|
|
65
|
+
system_info: dict[str, Any]
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_healthy(self) -> bool:
|
|
69
|
+
return self.healthy
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"healthy": self.healthy,
|
|
74
|
+
"checks": [asdict(c) for c in self.checks],
|
|
75
|
+
"models": self.models,
|
|
76
|
+
"system_info": self.system_info,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def render_text(self) -> str:
|
|
80
|
+
lines = []
|
|
81
|
+
lines.append("=" * 60)
|
|
82
|
+
lines.append(" Local Coding Agent — System Diagnostic Wizard")
|
|
83
|
+
lines.append("=" * 60)
|
|
84
|
+
lines.append("")
|
|
85
|
+
|
|
86
|
+
for check in self.checks:
|
|
87
|
+
tag = "[OK] " if check.status == "ok" else ("[WARN]" if check.status == "warn" else "[FAIL]")
|
|
88
|
+
lines.append(f"{tag} {check.name}: {check.message}")
|
|
89
|
+
if check.details:
|
|
90
|
+
for k, v in check.details.items():
|
|
91
|
+
lines.append(f" - {k}: {v}")
|
|
92
|
+
|
|
93
|
+
lines.append("")
|
|
94
|
+
lines.append("-" * 60)
|
|
95
|
+
lines.append(" Installed vs Recommended Models")
|
|
96
|
+
lines.append("-" * 60)
|
|
97
|
+
|
|
98
|
+
installed = self.models.get("installed", [])
|
|
99
|
+
if installed:
|
|
100
|
+
lines.append(f"Installed Ollama Models ({len(installed)}):")
|
|
101
|
+
for m in installed:
|
|
102
|
+
lines.append(f" • {m.get('name', 'unknown')}")
|
|
103
|
+
else:
|
|
104
|
+
lines.append("No models found in Ollama.")
|
|
105
|
+
|
|
106
|
+
missing = self.models.get("missing", [])
|
|
107
|
+
if missing:
|
|
108
|
+
lines.append("")
|
|
109
|
+
lines.append("Recommended Models To Install:")
|
|
110
|
+
for m in missing:
|
|
111
|
+
lines.append(f" • {m['name']} — {m['description']}")
|
|
112
|
+
lines.append(f" Command: {m['pull_cmd']}")
|
|
113
|
+
|
|
114
|
+
lines.append("")
|
|
115
|
+
lines.append("=" * 60)
|
|
116
|
+
if self.healthy:
|
|
117
|
+
lines.append(" Overall Status: READY (All critical checks passed)")
|
|
118
|
+
else:
|
|
119
|
+
lines.append(" Overall Status: ATTENTION REQUIRED (Check errors above)")
|
|
120
|
+
lines.append("=" * 60)
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def check_git_installed() -> CheckResult:
|
|
125
|
+
try:
|
|
126
|
+
proc = subprocess.run(
|
|
127
|
+
["git", "--version"],
|
|
128
|
+
capture_output=True,
|
|
129
|
+
text=True,
|
|
130
|
+
check=False,
|
|
131
|
+
timeout=5,
|
|
132
|
+
)
|
|
133
|
+
if proc.returncode == 0:
|
|
134
|
+
version_str = proc.stdout.strip()
|
|
135
|
+
git_path = shutil.which("git") or "in PATH"
|
|
136
|
+
return CheckResult(
|
|
137
|
+
name="Git Executable",
|
|
138
|
+
status="ok",
|
|
139
|
+
message=f"{version_str} ({git_path})",
|
|
140
|
+
details={"version": version_str, "path": git_path},
|
|
141
|
+
)
|
|
142
|
+
return CheckResult(
|
|
143
|
+
name="Git Executable",
|
|
144
|
+
status="fail",
|
|
145
|
+
message=f"Git returned non-zero exit code: {proc.returncode}",
|
|
146
|
+
)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
return CheckResult(
|
|
149
|
+
name="Git Executable",
|
|
150
|
+
status="fail",
|
|
151
|
+
message=f"Git is not found or not accessible in PATH: {exc}",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def check_ollama_api(endpoint: str = "http://127.0.0.1:11434") -> tuple[CheckResult, list[str]]:
|
|
156
|
+
try:
|
|
157
|
+
from .profiles import ModelProfile
|
|
158
|
+
dummy_profile = ModelProfile(
|
|
159
|
+
name="check",
|
|
160
|
+
model="check",
|
|
161
|
+
endpoint=endpoint,
|
|
162
|
+
think=False,
|
|
163
|
+
temperature=0.0,
|
|
164
|
+
num_ctx=2048,
|
|
165
|
+
num_predict=128,
|
|
166
|
+
keep_alive="1m",
|
|
167
|
+
max_context_length=8192,
|
|
168
|
+
)
|
|
169
|
+
client = OllamaClient(dummy_profile)
|
|
170
|
+
start = time.perf_counter()
|
|
171
|
+
data = client.available_models()
|
|
172
|
+
latency_ms = round((time.perf_counter() - start) * 1000, 1)
|
|
173
|
+
|
|
174
|
+
raw_models = data.get("models", [])
|
|
175
|
+
names = [
|
|
176
|
+
m.get("name")
|
|
177
|
+
for m in raw_models
|
|
178
|
+
if isinstance(m, dict) and isinstance(m.get("name"), str)
|
|
179
|
+
]
|
|
180
|
+
return CheckResult(
|
|
181
|
+
name="Ollama API",
|
|
182
|
+
status="ok",
|
|
183
|
+
message=f"Connected to {endpoint} (latency: {latency_ms}ms, {len(names)} models)",
|
|
184
|
+
details={"endpoint": endpoint, "latency_ms": latency_ms, "model_count": len(names)},
|
|
185
|
+
), names
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
return CheckResult(
|
|
188
|
+
name="Ollama API",
|
|
189
|
+
status="fail",
|
|
190
|
+
message=f"Failed to connect to Ollama at {endpoint}: {exc}",
|
|
191
|
+
details={"endpoint": endpoint, "error": str(exc)},
|
|
192
|
+
), []
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def check_host_memory() -> CheckResult:
|
|
196
|
+
total_ram_gb: float | None = None
|
|
197
|
+
free_ram_gb: float | None = None
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
if sys.platform == "win32":
|
|
201
|
+
class MEMORYSTATUSEX(ctypes.Structure):
|
|
202
|
+
_fields_ = [
|
|
203
|
+
("dwLength", ctypes.c_ulong),
|
|
204
|
+
("dwMemoryLoad", ctypes.c_ulong),
|
|
205
|
+
("ullTotalPhys", ctypes.c_ulonglong),
|
|
206
|
+
("ullAvailPhys", ctypes.c_ulonglong),
|
|
207
|
+
("ullTotalPageFile", ctypes.c_ulonglong),
|
|
208
|
+
("ullAvailPageFile", ctypes.c_ulonglong),
|
|
209
|
+
("ullTotalVirtual", ctypes.c_ulonglong),
|
|
210
|
+
("ullAvailVirtual", ctypes.c_ulonglong),
|
|
211
|
+
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
|
|
212
|
+
]
|
|
213
|
+
stat = MEMORYSTATUSEX()
|
|
214
|
+
stat.dwLength = ctypes.sizeof(stat)
|
|
215
|
+
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
|
|
216
|
+
total_ram_gb = round(stat.ullTotalPhys / (1024 ** 3), 1)
|
|
217
|
+
free_ram_gb = round(stat.ullAvailPhys / (1024 ** 3), 1)
|
|
218
|
+
elif sys.platform == "linux" and os.path.exists("/proc/meminfo"):
|
|
219
|
+
mem_info: dict[str, int] = {}
|
|
220
|
+
with open("/proc/meminfo", "r", encoding="utf-8") as f:
|
|
221
|
+
for line in f:
|
|
222
|
+
parts = line.split(":")
|
|
223
|
+
if len(parts) == 2:
|
|
224
|
+
key = parts[0].strip()
|
|
225
|
+
val = parts[1].strip().split()[0]
|
|
226
|
+
if val.isdigit():
|
|
227
|
+
mem_info[key] = int(val)
|
|
228
|
+
if "MemTotal" in mem_info and "MemAvailable" in mem_info:
|
|
229
|
+
total_ram_gb = round(mem_info["MemTotal"] / (1024 ** 2), 1)
|
|
230
|
+
free_ram_gb = round(mem_info["MemAvailable"] / (1024 ** 2), 1)
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
if total_ram_gb is not None:
|
|
235
|
+
status = "ok" if (free_ram_gb or 0) >= 4.0 else "warn"
|
|
236
|
+
msg = f"{total_ram_gb} GB total ({free_ram_gb} GB available)"
|
|
237
|
+
return CheckResult(
|
|
238
|
+
name="Host Memory",
|
|
239
|
+
status=status,
|
|
240
|
+
message=f"RAM: {msg}",
|
|
241
|
+
details={"total_ram_gb": total_ram_gb, "available_ram_gb": free_ram_gb},
|
|
242
|
+
)
|
|
243
|
+
return CheckResult(
|
|
244
|
+
name="Host Memory",
|
|
245
|
+
status="ok",
|
|
246
|
+
message="RAM metrics detected",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def recommend_models(installed_names: Sequence[str]) -> dict[str, Any]:
|
|
251
|
+
installed_list = []
|
|
252
|
+
missing_list = []
|
|
253
|
+
|
|
254
|
+
installed_lower = {name.lower() for name in installed_names}
|
|
255
|
+
|
|
256
|
+
for rec in RECOMMENDED_PROFILES:
|
|
257
|
+
target_name = rec["name"].lower()
|
|
258
|
+
target_model = rec["model"].lower()
|
|
259
|
+
# Recommendation status is based on exact served tags or an explicit
|
|
260
|
+
# untagged profile alias, never a broad substring (e.g. 0.6B must not
|
|
261
|
+
# satisfy an 8B recommendation).
|
|
262
|
+
is_present = target_name in installed_lower or target_model in installed_lower
|
|
263
|
+
if not is_present and ":" not in target_model:
|
|
264
|
+
aliases = {
|
|
265
|
+
installed.rsplit(":", 1)[0]
|
|
266
|
+
for installed in installed_lower
|
|
267
|
+
if installed.rsplit(":", 1)[0] == target_name
|
|
268
|
+
}
|
|
269
|
+
is_present = len(aliases) == 1
|
|
270
|
+
if is_present:
|
|
271
|
+
installed_list.append(rec)
|
|
272
|
+
else:
|
|
273
|
+
missing_list.append(rec)
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"installed": [{"name": n} for n in installed_names],
|
|
277
|
+
"recommended_installed": installed_list,
|
|
278
|
+
"missing": missing_list,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def diagnose_environment(endpoint: str = "http://127.0.0.1:11434") -> DoctorReport:
|
|
283
|
+
checks = []
|
|
284
|
+
|
|
285
|
+
# 1. Python version check
|
|
286
|
+
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
287
|
+
py_ok = sys.version_info >= (3, 10)
|
|
288
|
+
checks.append(
|
|
289
|
+
CheckResult(
|
|
290
|
+
name="Python Runtime",
|
|
291
|
+
status="ok" if py_ok else "fail",
|
|
292
|
+
message=f"Python {py_ver} ({platform.platform()})",
|
|
293
|
+
details={"version": py_ver, "executable": sys.executable},
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# 2. Git check
|
|
298
|
+
git_check = check_git_installed()
|
|
299
|
+
checks.append(git_check)
|
|
300
|
+
|
|
301
|
+
# 3. Host Memory check
|
|
302
|
+
mem_check = check_host_memory()
|
|
303
|
+
checks.append(mem_check)
|
|
304
|
+
|
|
305
|
+
# 4. Ollama API check
|
|
306
|
+
ollama_check, installed_models = check_ollama_api(endpoint)
|
|
307
|
+
checks.append(ollama_check)
|
|
308
|
+
|
|
309
|
+
# Model catalog & recommendations
|
|
310
|
+
models_rec = recommend_models(installed_models)
|
|
311
|
+
|
|
312
|
+
critical_failures = [c for c in checks if c.is_fail]
|
|
313
|
+
healthy = len(critical_failures) == 0
|
|
314
|
+
|
|
315
|
+
return DoctorReport(
|
|
316
|
+
healthy=healthy,
|
|
317
|
+
checks=checks,
|
|
318
|
+
models=models_rec,
|
|
319
|
+
system_info={
|
|
320
|
+
"platform": platform.platform(),
|
|
321
|
+
"python": py_ver,
|
|
322
|
+
"executable": sys.executable,
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass
|
|
328
|
+
class DoctorFixReport:
|
|
329
|
+
success: bool
|
|
330
|
+
actions: list[str] = field(default_factory=list)
|
|
331
|
+
recommendations: list[str] = field(default_factory=list)
|
|
332
|
+
errors: list[str] = field(default_factory=list)
|
|
333
|
+
|
|
334
|
+
def to_dict(self) -> dict[str, Any]:
|
|
335
|
+
return {
|
|
336
|
+
"success": self.success,
|
|
337
|
+
"actions": self.actions,
|
|
338
|
+
"recommendations": self.recommendations,
|
|
339
|
+
"errors": self.errors,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
def render_text(self) -> str:
|
|
343
|
+
lines = [
|
|
344
|
+
"=" * 60,
|
|
345
|
+
" Local Coding Agent — System Remediation Wizard (--fix)",
|
|
346
|
+
"=" * 60,
|
|
347
|
+
"",
|
|
348
|
+
"Actions Taken / Configured:",
|
|
349
|
+
]
|
|
350
|
+
if self.actions:
|
|
351
|
+
for act in self.actions:
|
|
352
|
+
lines.append(f" [OK] {act}")
|
|
353
|
+
else:
|
|
354
|
+
lines.append(" (No actions needed)")
|
|
355
|
+
|
|
356
|
+
if self.errors:
|
|
357
|
+
lines.extend(["", "Failures:"])
|
|
358
|
+
lines.extend(f" [FAIL] {error}" for error in self.errors)
|
|
359
|
+
|
|
360
|
+
lines.extend([
|
|
361
|
+
"",
|
|
362
|
+
"-" * 60,
|
|
363
|
+
" Recommended Model Pulls (Run in Terminal):",
|
|
364
|
+
"-" * 60,
|
|
365
|
+
])
|
|
366
|
+
for rec in self.recommendations:
|
|
367
|
+
lines.append(f" $ {rec}")
|
|
368
|
+
lines.append("")
|
|
369
|
+
return "\n".join(lines)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def remediate_environment(
|
|
373
|
+
endpoint: str = "http://127.0.0.1:11434",
|
|
374
|
+
write: bool = True,
|
|
375
|
+
) -> DoctorFixReport:
|
|
376
|
+
"""Remediate environment by configuring MCP, exporting skills, and recommending models."""
|
|
377
|
+
from .mcp_config import integrate_mcp_config
|
|
378
|
+
from .skill_config import integrate_skill_config
|
|
379
|
+
|
|
380
|
+
actions: list[str] = []
|
|
381
|
+
errors: list[str] = []
|
|
382
|
+
|
|
383
|
+
# 1. MCP Configuration for detected IDEs
|
|
384
|
+
mcp_res = integrate_mcp_config(client="all", dry_run=not write)
|
|
385
|
+
for sub in mcp_res.get("results", []):
|
|
386
|
+
if sub.get("error"):
|
|
387
|
+
errors.append(f"{sub.get('client', 'unknown')}: {sub['error']}")
|
|
388
|
+
continue
|
|
389
|
+
tag = "Applied MCP config" if write else "Previewed MCP config"
|
|
390
|
+
actions.append(f"{tag} for {sub.get('client')}: {sub.get('path')}")
|
|
391
|
+
|
|
392
|
+
# 2. Skill export for agents
|
|
393
|
+
skill_res = integrate_skill_config(client="auto", dry_run=not write)
|
|
394
|
+
for sub in skill_res.get("results", []):
|
|
395
|
+
if sub.get("error"):
|
|
396
|
+
errors.append(f"{sub.get('client', 'unknown')}: {sub['error']}")
|
|
397
|
+
continue
|
|
398
|
+
tag = "Installed Agent Skill" if write else "Previewed Agent Skill"
|
|
399
|
+
actions.append(f"{tag} for {sub.get('client')}: {sub.get('path')}")
|
|
400
|
+
|
|
401
|
+
# 3. Model pull prescriptions
|
|
402
|
+
recommendations: list[str] = [
|
|
403
|
+
"ollama run hf.co/unsloth/Qwen3-8B-GGUF:Q6_K",
|
|
404
|
+
"ollama run hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M",
|
|
405
|
+
"ollama pull qwen2.5-coder:latest",
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
return DoctorFixReport(
|
|
409
|
+
success=not errors,
|
|
410
|
+
actions=actions,
|
|
411
|
+
recommendations=recommendations,
|
|
412
|
+
errors=errors,
|
|
413
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""External agent hook bridges and protocol adapters (R30).
|
|
2
|
+
|
|
3
|
+
Provides lifecycle hook interception points (on_pre_tool_call, on_post_tool_call,
|
|
4
|
+
on_turn_start, on_turn_end, on_session_finish) and wire-protocol adapters compatible
|
|
5
|
+
with Codex and Claude Code hook standards.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._adapters import ClaudeCodeHookAdapter, CodexHookAdapter
|
|
11
|
+
from ._bridge import HookBridge
|
|
12
|
+
from ._decision import HookDecision
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ClaudeCodeHookAdapter",
|
|
16
|
+
"CodexHookAdapter",
|
|
17
|
+
"HookBridge",
|
|
18
|
+
"HookDecision",
|
|
19
|
+
]
|