hexiel 0.2.9__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.
- hexiel/__init__.py +1 -0
- hexiel/__main__.py +5 -0
- hexiel/brand.py +43 -0
- hexiel/config.py +194 -0
- hexiel/doctor.py +168 -0
- hexiel/gitlab.py +316 -0
- hexiel/heal.py +73 -0
- hexiel/llm.py +326 -0
- hexiel/main.py +210 -0
- hexiel/memory.py +134 -0
- hexiel/openwebui_compat.py +310 -0
- hexiel/prompt.py +209 -0
- hexiel/session.py +447 -0
- hexiel/settings_edit.py +249 -0
- hexiel/skills.py +103 -0
- hexiel/todo.py +228 -0
- hexiel/tooling.py +234 -0
- hexiel/tools.py +936 -0
- hexiel/tui.py +1049 -0
- hexiel/updater.py +117 -0
- hexiel/web/app.js +419 -0
- hexiel/web/index.html +87 -0
- hexiel/web/logo.svg +28 -0
- hexiel/web/md.js +131 -0
- hexiel/web/style.css +239 -0
- hexiel/webui.py +478 -0
- hexiel-0.2.9.dist-info/METADATA +254 -0
- hexiel-0.2.9.dist-info/RECORD +32 -0
- hexiel-0.2.9.dist-info/WHEEL +5 -0
- hexiel-0.2.9.dist-info/entry_points.txt +2 -0
- hexiel-0.2.9.dist-info/licenses/LICENSE +710 -0
- hexiel-0.2.9.dist-info/top_level.txt +1 -0
hexiel/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.9"
|
hexiel/__main__.py
ADDED
hexiel/brand.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""hexiel's logo: angel wings around an H under a halo. The image lives in
|
|
2
|
+
docs/logo.svg (and web/logo.svg); this is the terminal rendition."""
|
|
3
|
+
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
LOGO = r"""
|
|
7
|
+
.-~~~~~-.
|
|
8
|
+
__.._ `-~~~~~-' _..__
|
|
9
|
+
\ ``--.._ ██ ██ _..--'' /
|
|
10
|
+
\ ``--.._ ``--._ ██ ██ _.--'' _..--'' /
|
|
11
|
+
`-._ ``--._ \ ███████ / _.--'' _.-'
|
|
12
|
+
`-.__ \ \ ██ ██ / / __.-'
|
|
13
|
+
`--.__\ \ ██ ██ / /__.--'
|
|
14
|
+
`-' `-'
|
|
15
|
+
"""
|
|
16
|
+
LOGO_WIDTH = max(len(line) for line in LOGO.splitlines())
|
|
17
|
+
GOLD = "#d9b55a"
|
|
18
|
+
WING = ["#cfe6df", "#cfe6df", "#a8cfc4", "#93c2b5", "#7fb3a5", "#6aa294", "#5f9486", "#5f9486"]
|
|
19
|
+
HALO_COLS = range(22, 33)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def logo_text(tagline: str = "your codebase's guardian angel") -> Text:
|
|
23
|
+
"""The coloured logo: gold halo + H, wings shaded light to sage."""
|
|
24
|
+
out = Text()
|
|
25
|
+
for row, line in enumerate(LOGO.strip("\n").splitlines()):
|
|
26
|
+
for col, ch in enumerate(line):
|
|
27
|
+
gold = ch == "█" or (row < 2 and col in HALO_COLS)
|
|
28
|
+
out.append(ch, style=f"bold {GOLD}" if gold else WING[min(row, len(WING) - 1)])
|
|
29
|
+
out.append("\n")
|
|
30
|
+
out.append(" " * 20 + "hexiel", style=f"bold {GOLD}")
|
|
31
|
+
out.append(f" · {tagline}", style="#8b919d")
|
|
32
|
+
return out
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def small_logo_text() -> Text:
|
|
36
|
+
"""One line for narrow terminals."""
|
|
37
|
+
out = Text()
|
|
38
|
+
out.append("~\\\\ ", style="#a8cfc4")
|
|
39
|
+
out.append("H", style=f"bold {GOLD}")
|
|
40
|
+
out.append(" //~ ", style="#a8cfc4")
|
|
41
|
+
out.append("hexiel", style=f"bold {GOLD}")
|
|
42
|
+
out.append(" · your codebase's guardian angel", style="#8b919d")
|
|
43
|
+
return out
|
hexiel/config.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Configuration loading and defaults for hexiel."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def home_config_dir() -> Path:
|
|
10
|
+
return Path(os.environ.get("HEXIEL_HOME", Path.home() / ".config" / "hexiel"))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def home_data_dir() -> Path:
|
|
14
|
+
return Path(os.environ.get("HEXIEL_DATA", Path.home() / ".local" / "share" / "hexiel"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def memory_dir() -> Path:
|
|
18
|
+
return home_data_dir() / "memory"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def skills_dirs() -> list[Path]:
|
|
22
|
+
dirs = [home_data_dir() / "skills"]
|
|
23
|
+
# compat with existing Claude Code skills so nothing has to be copied over
|
|
24
|
+
dirs.append(Path.home() / ".claude" / "skills")
|
|
25
|
+
dirs.append(Path.cwd() / ".hexiel" / "skills")
|
|
26
|
+
dirs.append(Path.cwd() / ".claude" / "skills")
|
|
27
|
+
return dirs
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
CONFIG_FILE = home_config_dir() / "config.toml"
|
|
31
|
+
OLLAMA_HOST = "http://localhost:11434"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def list_ollama_models(base_url: str = "") -> list[str]:
|
|
35
|
+
"""Live model inventory from an Ollama server (local or cloud-backed)."""
|
|
36
|
+
import httpx
|
|
37
|
+
root = (base_url or OLLAMA_HOST).removesuffix("/v1").removesuffix("/")
|
|
38
|
+
try:
|
|
39
|
+
resp = httpx.get(f"{root}/api/tags", timeout=10)
|
|
40
|
+
return sorted(m["name"] for m in resp.json().get("models", []))
|
|
41
|
+
except (httpx.HTTPError, ValueError):
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
DEFAULT_CONFIG = '''\
|
|
45
|
+
# hexiel configuration. Profiles live under [models.*]; pick one with /model NAME
|
|
46
|
+
# or `hexiel --model NAME`. "default" selects the boot model.
|
|
47
|
+
|
|
48
|
+
default = "glm-flash"
|
|
49
|
+
|
|
50
|
+
[settings]
|
|
51
|
+
auto_approve = false # when true, write tools and bash run without approval
|
|
52
|
+
compact_threshold = 0.82 # auto-compact when the prompt reaches this share of the context window
|
|
53
|
+
request_timeout = 900 # seconds per provider request
|
|
54
|
+
search_backend = "duckduckgo" # "duckduckgo" (no key) or "searxng"
|
|
55
|
+
searxng_url = "" # e.g. http://localhost:8888 when search_backend = "searxng"
|
|
56
|
+
|
|
57
|
+
[models.glm-flash]
|
|
58
|
+
provider = "openai" # "openai" = any OpenAI-compatible /chat/completions endpoint
|
|
59
|
+
base_url = "http://localhost:11434/v1"
|
|
60
|
+
model = "glm-5.3-flash:cloud"
|
|
61
|
+
context_window = 200000 # tokens before auto-compaction kicks in
|
|
62
|
+
max_output_tokens = 16384
|
|
63
|
+
|
|
64
|
+
[models.kimi]
|
|
65
|
+
provider = "openai"
|
|
66
|
+
base_url = "http://localhost:11434/v1"
|
|
67
|
+
model = "kimi-k2.7-code:cloud"
|
|
68
|
+
context_window = 262144
|
|
69
|
+
max_output_tokens = 16384
|
|
70
|
+
|
|
71
|
+
[models.anthropic]
|
|
72
|
+
provider = "anthropic"
|
|
73
|
+
model = "claude-opus-5-5"
|
|
74
|
+
api_key_env = "ANTHROPIC_API_KEY"
|
|
75
|
+
context_window = 200000
|
|
76
|
+
max_output_tokens = 16384
|
|
77
|
+
|
|
78
|
+
[models.anthropic-sonnet]
|
|
79
|
+
provider = "anthropic"
|
|
80
|
+
model = "claude-sonnet-5-5"
|
|
81
|
+
api_key_env = "ANTHROPIC_API_KEY"
|
|
82
|
+
context_window = 200000
|
|
83
|
+
max_output_tokens = 16384
|
|
84
|
+
|
|
85
|
+
# ---- more big-model presets (fill in the key env var / base_url you have) ----
|
|
86
|
+
|
|
87
|
+
[models.openai]
|
|
88
|
+
provider = "openai"
|
|
89
|
+
base_url = "https://api.openai.com/v1"
|
|
90
|
+
model = "gpt-5.2"
|
|
91
|
+
api_key_env = "OPENAI_API_KEY"
|
|
92
|
+
context_window = 400000
|
|
93
|
+
max_output_tokens = 16384
|
|
94
|
+
|
|
95
|
+
[models.gemini]
|
|
96
|
+
provider = "openai" # Gemini speaks the OpenAI-compatible protocol
|
|
97
|
+
base_url = "https://generativelanguage.googleapis.com/v1beta/openai"
|
|
98
|
+
model = "gemini-2.5-pro"
|
|
99
|
+
api_key_env = "GEMINI_API_KEY"
|
|
100
|
+
context_window = 1000000
|
|
101
|
+
max_output_tokens = 16384
|
|
102
|
+
|
|
103
|
+
[models.xai]
|
|
104
|
+
provider = "openai"
|
|
105
|
+
base_url = "https://api.x.ai/v1"
|
|
106
|
+
model = "grok-4"
|
|
107
|
+
api_key_env = "XAI_API_KEY"
|
|
108
|
+
context_window = 256000
|
|
109
|
+
max_output_tokens = 16384
|
|
110
|
+
|
|
111
|
+
[models.openrouter]
|
|
112
|
+
provider = "openai" # hundreds of models behind one key
|
|
113
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
114
|
+
model = "anthropic/claude-opus-4.5"
|
|
115
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
116
|
+
context_window = 200000
|
|
117
|
+
max_output_tokens = 16384
|
|
118
|
+
|
|
119
|
+
[models.deepseek]
|
|
120
|
+
provider = "openai"
|
|
121
|
+
base_url = "https://api.deepseek.com/v1"
|
|
122
|
+
model = "deepseek-chat"
|
|
123
|
+
api_key_env = "DEEPSEEK_API_KEY"
|
|
124
|
+
context_window = 128000
|
|
125
|
+
max_output_tokens = 16384
|
|
126
|
+
'''
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class ModelConfig:
|
|
131
|
+
name: str
|
|
132
|
+
provider: str
|
|
133
|
+
model: str
|
|
134
|
+
base_url: str = ""
|
|
135
|
+
api_key_env: str = ""
|
|
136
|
+
context_window: int = 200_000
|
|
137
|
+
max_output_tokens: int = 16_384
|
|
138
|
+
extra_body: dict = field(default_factory=dict)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class Settings:
|
|
143
|
+
auto_approve: bool = False
|
|
144
|
+
compact_threshold: float = 0.82
|
|
145
|
+
request_timeout: int = 900
|
|
146
|
+
search_backend: str = "duckduckgo"
|
|
147
|
+
searxng_url: str = ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class Config:
|
|
152
|
+
default_model: str
|
|
153
|
+
models: dict[str, ModelConfig]
|
|
154
|
+
settings: Settings
|
|
155
|
+
|
|
156
|
+
def model(self, name: str | None = None) -> ModelConfig:
|
|
157
|
+
key = name or self.default_model
|
|
158
|
+
if key not in self.models:
|
|
159
|
+
raise KeyError(f"unknown model profile '{key}' (have: {', '.join(self.models)})")
|
|
160
|
+
return self.models[key]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def load_config(model_override: str | None = None) -> Config:
|
|
164
|
+
if not CONFIG_FILE.exists():
|
|
165
|
+
try:
|
|
166
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
CONFIG_FILE.write_text(DEFAULT_CONFIG)
|
|
168
|
+
except OSError:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
raw: dict = {}
|
|
172
|
+
if CONFIG_FILE.exists():
|
|
173
|
+
try:
|
|
174
|
+
raw = tomllib.loads(CONFIG_FILE.read_text())
|
|
175
|
+
except tomllib.TOMLDecodeError as exc:
|
|
176
|
+
raise SystemExit(f"bad config {CONFIG_FILE}: {exc}")
|
|
177
|
+
|
|
178
|
+
models: dict[str, ModelConfig] = {}
|
|
179
|
+
for name, body in raw.get("models", {}).items():
|
|
180
|
+
models[name] = ModelConfig(name=name, **body)
|
|
181
|
+
|
|
182
|
+
settings_raw = raw.get("settings", {})
|
|
183
|
+
settings = Settings(
|
|
184
|
+
auto_approve=bool(settings_raw.get("auto_approve", False)),
|
|
185
|
+
compact_threshold=float(settings_raw.get("compact_threshold", 0.82)),
|
|
186
|
+
request_timeout=int(settings_raw.get("request_timeout", 900)),
|
|
187
|
+
search_backend=str(settings_raw.get("search_backend", "duckduckgo")),
|
|
188
|
+
searxng_url=str(settings_raw.get("searxng_url", "")),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
default_model = model_override or raw.get("default", next(iter(models), "default"))
|
|
192
|
+
if default_model not in models:
|
|
193
|
+
models.setdefault(default_model, ModelConfig(name=default_model, provider="openai"))
|
|
194
|
+
return Config(default_model=default_model, models=models, settings=settings)
|
hexiel/doctor.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""hexiel doctor: preflight checks with copy-paste fixes, plus the first-run
|
|
2
|
+
wizard (same checks, guided). Run `hexiel doctor` any time."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .config import CONFIG_FILE, load_config, home_data_dir
|
|
11
|
+
from .gitlab import load_token
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _check(label: str, ok: bool, detail: str = "", fix: str = "") -> bool:
|
|
15
|
+
mark = "\033[32m✓\033[0m" if sys.stdout.isatty() else "✓" if ok else "✗"
|
|
16
|
+
if not ok:
|
|
17
|
+
mark = "\033[31m✗\033[0m" if sys.stdout.isatty() else "✗"
|
|
18
|
+
line = f" {mark} {label}"
|
|
19
|
+
if detail:
|
|
20
|
+
line += f" — {detail}"
|
|
21
|
+
print(line)
|
|
22
|
+
if not ok and fix:
|
|
23
|
+
print(f" fix: {fix}")
|
|
24
|
+
return ok
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_checks(cwd: str) -> dict:
|
|
28
|
+
"""Run every check; returns {name: (ok, detail, fix)} and prints results."""
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
30
|
+
results: dict[str, bool] = {}
|
|
31
|
+
print(f"hexiel {__version__} — doctor\n")
|
|
32
|
+
|
|
33
|
+
# config file
|
|
34
|
+
ok = CONFIG_FILE.exists()
|
|
35
|
+
results["config"] = _check(
|
|
36
|
+
"configuration", ok, str(CONFIG_FILE) if ok else "no config.toml yet",
|
|
37
|
+
fix="just run hexiel once — it writes a starter config")
|
|
38
|
+
|
|
39
|
+
cfg = None
|
|
40
|
+
try:
|
|
41
|
+
cfg = load_config()
|
|
42
|
+
results["config_parse"] = _check("config parses", True, ", ".join(cfg.models))
|
|
43
|
+
except SystemExit as exc:
|
|
44
|
+
results["config_parse"] = _check("config parses", False, str(exc))
|
|
45
|
+
return results
|
|
46
|
+
|
|
47
|
+
# python deps
|
|
48
|
+
for mod, needs in (("textual", "the TUI"), ("httpx", "all providers"),
|
|
49
|
+
("aiohttp", "the web UI (hexiel serve)")):
|
|
50
|
+
try:
|
|
51
|
+
__import__(mod)
|
|
52
|
+
results[f"dep:{mod}"] = _check(f"python: {mod}", True, f"for {needs}")
|
|
53
|
+
except ImportError:
|
|
54
|
+
results[f"dep:{mod}"] = _check(
|
|
55
|
+
f"python: {mod}", False, f"needed for {needs}",
|
|
56
|
+
fix=f"pip install ({mod}) in your hexiel venv")
|
|
57
|
+
|
|
58
|
+
# provider reachability
|
|
59
|
+
default_name = cfg.default_model
|
|
60
|
+
model_cfg = cfg.models.get(default_name)
|
|
61
|
+
results["model"] = _check(
|
|
62
|
+
f"default model '{default_name}'", model_cfg is not None,
|
|
63
|
+
f"{model_cfg.model} @ {model_cfg.base_url or model_cfg.provider}" if model_cfg else "")
|
|
64
|
+
if model_cfg and model_cfg.base_url:
|
|
65
|
+
import httpx
|
|
66
|
+
try:
|
|
67
|
+
from .config import list_ollama_models
|
|
68
|
+
models = list_ollama_models(model_cfg.base_url)
|
|
69
|
+
reachable = bool(models)
|
|
70
|
+
wanted = model_cfg.model in models
|
|
71
|
+
results["provider"] = _check(
|
|
72
|
+
"provider endpoint", reachable, f"{len(models)} models seen, "
|
|
73
|
+
f"target '{model_cfg.model}' {'available' if wanted else 'NOT in inventory'}",
|
|
74
|
+
fix="" if wanted else
|
|
75
|
+
f"ollama pull {model_cfg.model} (or /model <another>)")
|
|
76
|
+
except Exception as exc: # noqa: BLE001
|
|
77
|
+
results["provider"] = _check("provider endpoint", False, repr(exc),
|
|
78
|
+
fix="start ollama serve (localhost:11434)")
|
|
79
|
+
elif model_cfg and model_cfg.api_key_env:
|
|
80
|
+
present = bool(os.environ.get(model_cfg.api_key_env))
|
|
81
|
+
results["provider"] = _check(
|
|
82
|
+
f"{model_cfg.provider} API key", present, model_cfg.api_key_env,
|
|
83
|
+
fix=f"export {model_cfg.api_key_env}=sk-… (or put it in shell profile)")
|
|
84
|
+
|
|
85
|
+
# toolchain
|
|
86
|
+
from .tooling import detect_toolchain
|
|
87
|
+
chain = detect_toolchain(cwd)
|
|
88
|
+
container = chain.container.binary if chain.container else None
|
|
89
|
+
results["container"] = _check(
|
|
90
|
+
"container runtime (docker/podman)", bool(container),
|
|
91
|
+
f"{container}{f' + compose' if (chain.container and chain.container.compose_command) else ''}"
|
|
92
|
+
if container else "none found — container tool disabled",
|
|
93
|
+
fix="sudo pacman -S docker && sudo systemctl enable --now docker")
|
|
94
|
+
results["test_runner"] = _check(
|
|
95
|
+
"test runner detection", bool(chain.test_runners),
|
|
96
|
+
", ".join(r.label for r in chain.test_runners) or "none in this project")
|
|
97
|
+
results["git"] = _check("git", bool(shutil.which("git")),
|
|
98
|
+
"(needed for the /ship and /heal flows)",
|
|
99
|
+
fix="sudo pacman -S git")
|
|
100
|
+
pwsh = shutil.which("pwsh") or shutil.which("powershell.exe") or shutil.which("powershell")
|
|
101
|
+
if os.name == "nt":
|
|
102
|
+
results["powershell"] = _check(
|
|
103
|
+
"PowerShell", bool(pwsh), pwsh or "missing — the shell tools need it",
|
|
104
|
+
fix="winget install Microsoft.PowerShell")
|
|
105
|
+
else:
|
|
106
|
+
results["powershell"] = _check(
|
|
107
|
+
"powershell tool", bool(pwsh),
|
|
108
|
+
pwsh or "not installed — optional here, required on Windows hosts",
|
|
109
|
+
fix="yay -S powershell-bin (or winget install Microsoft.PowerShell)")
|
|
110
|
+
|
|
111
|
+
# gitlab token
|
|
112
|
+
token = load_token()
|
|
113
|
+
results["gitlab"] = _check(
|
|
114
|
+
"GitLab token", bool(token),
|
|
115
|
+
"set (proposes MRs with /ship)" if token else "not set — /ship /heal can only push, not open MRs",
|
|
116
|
+
fix="create one: https://gitlab.com/-/user_settings/personal_access_tokens (api scope),"
|
|
117
|
+
" then HEXIEL_GITLAB_TOKEN=… hexiel or /setup-gitlab <token>")
|
|
118
|
+
|
|
119
|
+
# memory + skills dirs
|
|
120
|
+
mem = home_data_dir() / "memory"
|
|
121
|
+
results["memory"] = _check("memory dir", mem.is_dir(), str(mem),
|
|
122
|
+
fix="created automatically on first run; check permissions")
|
|
123
|
+
from .skills import discover
|
|
124
|
+
skills = discover(cwd)
|
|
125
|
+
results["skills"] = _check("skills", len(skills) > 0,
|
|
126
|
+
", ".join(s.name for s in skills[:8]) +
|
|
127
|
+
(" …" if len(skills) > 8 else "") or "none installed")
|
|
128
|
+
|
|
129
|
+
# incidents
|
|
130
|
+
from .heal import unhealed
|
|
131
|
+
incidents = unhealed(cwd)
|
|
132
|
+
results["incidents"] = _check("self-heal queue", not incidents,
|
|
133
|
+
"all clear" if not incidents
|
|
134
|
+
else f"{len(incidents)} unhealed — run /heal in a session")
|
|
135
|
+
|
|
136
|
+
return results
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def wizard(cwd: str) -> None:
|
|
140
|
+
"""First-run guided setup: config, provider pick, gitlab token (optional)."""
|
|
141
|
+
print("Welcome to hexiel — guided setup (all questions optional, Enter keeps defaults)\n")
|
|
142
|
+
if not CONFIG_FILE.exists():
|
|
143
|
+
load_config() # writes starter config
|
|
144
|
+
print(f" wrote {CONFIG_FILE}")
|
|
145
|
+
from .config import list_ollama_models
|
|
146
|
+
models = list_ollama_models()
|
|
147
|
+
if models:
|
|
148
|
+
names = ", ".join(models[:12]) + (" …" if len(models) > 12 else "")
|
|
149
|
+
print(f" Ollama reachable — models: {names}")
|
|
150
|
+
print(" config already defaults to ollama cloud models; /model NAME switches anytime")
|
|
151
|
+
else:
|
|
152
|
+
print(" Ollama not reachable on localhost:11434 — `ollama serve` + "
|
|
153
|
+
"`ollama pull <model>` enables it, or add a cloud profile to config.toml")
|
|
154
|
+
if not load_token():
|
|
155
|
+
print(" no GitLab token: /ship and /heal can push branches but will not open MRs.")
|
|
156
|
+
print(" add later with /setup-gitlab <token> (api scope PAT).")
|
|
157
|
+
print("\nDone. Start a session with `hexiel` (TUI), `hexiel --serve` (browser), or `hexiel -p \"…\"`.")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def doctor_main(cwd: str = ".") -> int:
|
|
161
|
+
results = run_checks(cwd)
|
|
162
|
+
failed = [k for k, ok in results.items() if not ok]
|
|
163
|
+
print()
|
|
164
|
+
if not failed:
|
|
165
|
+
print("all checks passed — hexiel is ready")
|
|
166
|
+
return 0
|
|
167
|
+
print(f"{len(failed)} check(s) need attention: {', '.join(failed)}")
|
|
168
|
+
return 1
|