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.
Files changed (101) hide show
  1. local_coding_agent/__init__.py +314 -0
  2. local_coding_agent/__main__.py +5 -0
  3. local_coding_agent/acp_server/__init__.py +38 -0
  4. local_coding_agent/acp_server/_codec.py +95 -0
  5. local_coding_agent/acp_server/_protocol.py +13 -0
  6. local_coding_agent/acp_server/_server.py +663 -0
  7. local_coding_agent/acp_server/_session.py +24 -0
  8. local_coding_agent/acp_server/_tools.py +126 -0
  9. local_coding_agent/ast_compactor.py +130 -0
  10. local_coding_agent/atomizer.py +77 -0
  11. local_coding_agent/benchmark/__init__.py +57 -0
  12. local_coding_agent/benchmark/_cases.py +307 -0
  13. local_coding_agent/benchmark/_judge.py +175 -0
  14. local_coding_agent/benchmark/_oracles.py +157 -0
  15. local_coding_agent/benchmark/_runner.py +259 -0
  16. local_coding_agent/benchmark/_stats.py +22 -0
  17. local_coding_agent/benchmark/_summary.py +143 -0
  18. local_coding_agent/benchmark_oracle_worker.py +169 -0
  19. local_coding_agent/calibration.py +154 -0
  20. local_coding_agent/capability.py +450 -0
  21. local_coding_agent/chat_compaction.py +276 -0
  22. local_coding_agent/cli/__init__.py +12 -0
  23. local_coding_agent/cli/_dispatch.py +377 -0
  24. local_coding_agent/cli/_handlers.py +320 -0
  25. local_coding_agent/cli/_handlers2.py +725 -0
  26. local_coding_agent/cli/_input.py +61 -0
  27. local_coding_agent/cli/_main.py +159 -0
  28. local_coding_agent/cli/_parser.py +327 -0
  29. local_coding_agent/context_manager.py +155 -0
  30. local_coding_agent/controller/__init__.py +18 -0
  31. local_coding_agent/controller/_constants.py +108 -0
  32. local_coding_agent/controller/_controller.py +1008 -0
  33. local_coding_agent/controller/_post_apply.py +216 -0
  34. local_coding_agent/delegator.py +180 -0
  35. local_coding_agent/desktop/__init__.py +8 -0
  36. local_coding_agent/desktop/app.py +100 -0
  37. local_coding_agent/desktop/assets/lucide.min.js +12 -0
  38. local_coding_agent/desktop/assets/tailwind.css +1 -0
  39. local_coding_agent/desktop/client_js.py +1271 -0
  40. local_coding_agent/desktop/components.py +555 -0
  41. local_coding_agent/desktop/server/__init__.py +37 -0
  42. local_coding_agent/desktop/server/_handlers.py +2178 -0
  43. local_coding_agent/desktop/server/_models.py +204 -0
  44. local_coding_agent/desktop/server/_server.py +347 -0
  45. local_coding_agent/desktop/server/_telemetry.py +87 -0
  46. local_coding_agent/desktop/styles.py +76 -0
  47. local_coding_agent/desktop/ui.py +53 -0
  48. local_coding_agent/doctor.py +413 -0
  49. local_coding_agent/hooks/__init__.py +19 -0
  50. local_coding_agent/hooks/_adapters.py +458 -0
  51. local_coding_agent/hooks/_bridge.py +346 -0
  52. local_coding_agent/hooks/_decision.py +26 -0
  53. local_coding_agent/lsp/__init__.py +60 -0
  54. local_coding_agent/lsp/_client.py +322 -0
  55. local_coding_agent/lsp/_fallback.py +297 -0
  56. local_coding_agent/lsp/_manager.py +173 -0
  57. local_coding_agent/lsp/_mock.py +200 -0
  58. local_coding_agent/lsp/_types.py +398 -0
  59. local_coding_agent/mcp_config.py +398 -0
  60. local_coding_agent/mcp_server.py +377 -0
  61. local_coding_agent/memory.py +140 -0
  62. local_coding_agent/mode_router.py +200 -0
  63. local_coding_agent/model_scanner.py +525 -0
  64. local_coding_agent/monitor.py +635 -0
  65. local_coding_agent/observation_policy.py +184 -0
  66. local_coding_agent/ollama_adapter.py +736 -0
  67. local_coding_agent/plan_mode.py +653 -0
  68. local_coding_agent/prescriptions.py +278 -0
  69. local_coding_agent/profiles.py +239 -0
  70. local_coding_agent/repository_tools.py +782 -0
  71. local_coding_agent/ripgrep.py +326 -0
  72. local_coding_agent/semantic_linter.py +122 -0
  73. local_coding_agent/service.py +405 -0
  74. local_coding_agent/session_events.py +684 -0
  75. local_coding_agent/session_query.py +578 -0
  76. local_coding_agent/skill_config.py +426 -0
  77. local_coding_agent/smoke.py +268 -0
  78. local_coding_agent/speculative_racing.py +85 -0
  79. local_coding_agent/spill.py +258 -0
  80. local_coding_agent/stats.py +255 -0
  81. local_coding_agent/stdio.py +132 -0
  82. local_coding_agent/subagent.py +516 -0
  83. local_coding_agent/task.py +90 -0
  84. local_coding_agent/task_store.py +142 -0
  85. local_coding_agent/tasks.py +240 -0
  86. local_coding_agent/terminal/__init__.py +35 -0
  87. local_coding_agent/terminal/_manager.py +156 -0
  88. local_coding_agent/terminal/_process.py +148 -0
  89. local_coding_agent/terminal/_session.py +452 -0
  90. local_coding_agent/terminal/_tools.py +233 -0
  91. local_coding_agent/terminal/_types.py +60 -0
  92. local_coding_agent/tool_call_parser.py +178 -0
  93. local_coding_agent/validators.py +563 -0
  94. local_coding_agent/vram_fit.py +240 -0
  95. local_coding_agent/worker_pool.py +516 -0
  96. local_coding_agent-1.1.0.dist-info/METADATA +202 -0
  97. local_coding_agent-1.1.0.dist-info/RECORD +101 -0
  98. local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
  99. local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
  100. local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
  101. local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,525 @@
1
+ """Universal model discovery, indexing, and persistent local registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import string
9
+ import subprocess
10
+ import threading
11
+ import time
12
+ from dataclasses import asdict, dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ _CONFIG_DIR = Path.home() / ".local_coding_agent"
18
+ _REGISTRY_FILE = _CONFIG_DIR / "models.json"
19
+
20
+ # Common noise directories to skip during deep drive scans
21
+ _SKIP_DIR_NAMES = {
22
+ "$recycle.bin",
23
+ "system volume information",
24
+ "windows",
25
+ "winnt",
26
+ "program files (x86)",
27
+ "node_modules",
28
+ ".git",
29
+ ".pytest_cache",
30
+ "__pycache__",
31
+ "venv",
32
+ ".venv",
33
+ "env",
34
+ "site-packages",
35
+ "dist",
36
+ "build",
37
+ "steamlibrary",
38
+ "steamapps",
39
+ "hearthstone",
40
+ "overwatch",
41
+ "epic games",
42
+ }
43
+
44
+ # Non-LLM substrings in GGUF names to ignore (image diffusion, projectors, audio)
45
+ _NON_LLM_INDICATORS = (
46
+ "mmproj",
47
+ "flux",
48
+ "seedvr",
49
+ "t5-v1",
50
+ "clip",
51
+ "vae",
52
+ "unet",
53
+ "diffusion",
54
+ "stable-diffusion",
55
+ "sdxl",
56
+ "controlnet",
57
+ "lora",
58
+ )
59
+
60
+
61
+ def gguf_model_id(path: str | Path) -> str:
62
+ """Stable normalized-path id for a GGUF file.
63
+
64
+ Two discovered models may share a basename in different directories; the
65
+ id disambiguates them deterministically: forward slashes, lowercase
66
+ Windows drive letter and extension, collapse of redundant separators.
67
+ """
68
+ try:
69
+ normalized = str(Path(path).resolve())
70
+ except OSError:
71
+ normalized = str(path)
72
+ normalized = normalized.replace("\\", "/")
73
+ if len(normalized) >= 2 and normalized[1] == ":":
74
+ directory, separator, filename = normalized.rpartition("/")
75
+ normalized = directory.lower() + separator + filename
76
+ if normalized.lower().endswith(".gguf"):
77
+ normalized = normalized[:-5]
78
+ return normalized
79
+
80
+
81
+ @dataclass
82
+ class DiscoveredModel:
83
+ """Metadata for a discovered model on the local system."""
84
+
85
+ name: str
86
+ display_name: str
87
+ path: str
88
+ size_gb: float
89
+ backend: str = "gguf" # 'gguf' or 'ollama'
90
+ source: str = "scanner" # 'standard', 'custom', 'deep_scan', 'ollama'
91
+ modified_at: float = 0.0
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ data = asdict(self)
95
+ # Stable unambiguous identity: two GGUFs may share a basename, so the
96
+ # UI and /api/model/load must address them by path-derived id.
97
+ data["id"] = gguf_model_id(self.path)
98
+ return data
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict[str, Any]) -> DiscoveredModel:
102
+ return cls(
103
+ name=data.get("name", ""),
104
+ display_name=data.get("display_name", data.get("name", "")),
105
+ path=data.get("path", ""),
106
+ size_gb=float(data.get("size_gb", 0.0)),
107
+ backend=data.get("backend", "gguf"),
108
+ source=data.get("source", "scanner"),
109
+ modified_at=float(data.get("modified_at", 0.0)),
110
+ )
111
+
112
+
113
+ @dataclass
114
+ class ModelRegistryData:
115
+ """Persistent storage schema for ~/.local_coding_agent/models.json."""
116
+
117
+ custom_directories: list[str] = field(default_factory=list)
118
+ discovered_models: list[dict[str, Any]] = field(default_factory=list)
119
+ last_scanned_at: float = 0.0
120
+ scan_stats: dict[str, Any] = field(default_factory=dict)
121
+
122
+
123
+ class LocalModelRegistry:
124
+ """Manages discovery and local caching of local LLM models (GGUF / Ollama)."""
125
+
126
+ def __init__(self, registry_file: Path | None = None) -> None:
127
+ self.registry_file = registry_file or _REGISTRY_FILE
128
+ self._lock = threading.Lock()
129
+ self._ensure_config_dir()
130
+
131
+ def _ensure_config_dir(self) -> None:
132
+ self.registry_file.parent.mkdir(parents=True, exist_ok=True)
133
+
134
+ def load(self) -> ModelRegistryData:
135
+ """Load registry from disk or return default data."""
136
+ if not self.registry_file.exists():
137
+ return ModelRegistryData()
138
+ with self._lock:
139
+ try:
140
+ raw = self.registry_file.read_text(encoding="utf-8")
141
+ data = json.loads(raw)
142
+ if isinstance(data, dict):
143
+ return ModelRegistryData(
144
+ custom_directories=data.get("custom_directories", []),
145
+ discovered_models=data.get("discovered_models", []),
146
+ last_scanned_at=data.get("last_scanned_at", 0.0),
147
+ scan_stats=data.get("scan_stats", {}),
148
+ )
149
+ except (OSError, json.JSONDecodeError):
150
+ pass
151
+ return ModelRegistryData()
152
+
153
+ def save(self, data: ModelRegistryData) -> None:
154
+ """Save registry data to local machine JSON file atomically."""
155
+ self._ensure_config_dir()
156
+ with self._lock:
157
+ try:
158
+ payload = {
159
+ "custom_directories": data.custom_directories,
160
+ "discovered_models": data.discovered_models,
161
+ "last_scanned_at": data.last_scanned_at,
162
+ "scan_stats": data.scan_stats,
163
+ }
164
+ tmp_file = self.registry_file.with_suffix(".tmp")
165
+ tmp_file.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
166
+ tmp_file.replace(self.registry_file)
167
+ except OSError:
168
+ pass
169
+
170
+ def add_custom_directory(self, path: str | Path) -> bool:
171
+ """Add a custom directory to scan and persist it."""
172
+ norm = str(Path(path).resolve())
173
+ data = self.load()
174
+ if norm not in data.custom_directories:
175
+ data.custom_directories.append(norm)
176
+ self.save(data)
177
+ return True
178
+ return False
179
+
180
+ def remove_custom_directory(self, path: str | Path) -> bool:
181
+ """Remove a custom directory from the scan list."""
182
+ norm = str(Path(path).resolve())
183
+ data = self.load()
184
+ if norm in data.custom_directories:
185
+ data.custom_directories.remove(norm)
186
+ self.save(data)
187
+ return True
188
+ return False
189
+
190
+ def list_custom_directories(self) -> list[str]:
191
+ return self.load().custom_directories
192
+
193
+ def get_models(self, auto_scan: bool = True) -> list[DiscoveredModel]:
194
+ """Return discovered models. If empty and auto_scan=True, performs quick scan."""
195
+ data = self.load()
196
+ models = [
197
+ DiscoveredModel.from_dict(m)
198
+ for m in data.discovered_models
199
+ if isinstance(m, dict) and isinstance(m.get("path"), str) and Path(m["path"]).is_file()
200
+ ]
201
+ if not models and auto_scan:
202
+ return self.scan(deep=False)
203
+ return models
204
+
205
+ @staticmethod
206
+ def get_system_drives() -> list[Path]:
207
+ """Detect all available drive roots on Windows or root mount points on POSIX."""
208
+ drives: list[Path] = []
209
+ if os.name == "nt":
210
+ # Check available Windows drive letters
211
+ for letter in string.ascii_uppercase:
212
+ drive_path = Path(f"{letter}:\\")
213
+ try:
214
+ if drive_path.is_dir():
215
+ drives.append(drive_path)
216
+ except OSError:
217
+ continue
218
+ else:
219
+ drives.append(Path("/"))
220
+ # Check common mount roots
221
+ for m in (Path("/media"), Path("/mnt"), Path("/Volumes")):
222
+ if m.is_dir():
223
+ drives.append(m)
224
+ return drives
225
+
226
+ def get_standard_search_roots(self) -> list[Path]:
227
+ """Get standard well-known directories where users typically store local models."""
228
+ roots: list[Path] = []
229
+ home = Path.home()
230
+
231
+ # 1. LM Studio locations
232
+ roots.append(home / ".cache" / "lm-studio" / "models")
233
+ roots.append(home / ".lmstudio" / "models")
234
+
235
+ # 2. Ollama & GPT4All locations
236
+ roots.append(home / ".ollama" / "models")
237
+ roots.append(home / ".local" / "share" / "nomic.ai" / "GPT4All")
238
+
239
+ # 3. User home models directory
240
+ roots.append(home / "models")
241
+ roots.append(home / "LLM")
242
+ roots.append(home / "AI")
243
+
244
+ # 4. Standard per-drive candidate folders
245
+ for d in self.get_system_drives():
246
+ roots.append(d / "models")
247
+ roots.append(d / "AI" / "models")
248
+ roots.append(d / "LLM")
249
+ roots.append(d / "llama.cpp" / "models")
250
+
251
+ # 5. Environment variable overrides
252
+ env_dirs = os.environ.get("LOCAL_GGUF_DIRS") or os.environ.get("LOCAL_MODELS_PATH") or os.environ.get("GGUF_MODEL_DIR")
253
+ if env_dirs:
254
+ for part in env_dirs.split(os.pathsep):
255
+ if part.strip() and Path(part.strip()).is_dir():
256
+ roots.append(Path(part.strip()))
257
+
258
+ available: list[Path] = []
259
+ for root in roots:
260
+ try:
261
+ if root.is_dir():
262
+ available.append(root)
263
+ except OSError:
264
+ continue
265
+ return available
266
+
267
+ def scan(
268
+ self,
269
+ deep: bool = False,
270
+ target_drives: list[str] | None = None,
271
+ max_depth: int = 6,
272
+ ) -> list[DiscoveredModel]:
273
+ """Perform a scan for local GGUF models across standard roots and drives."""
274
+ start_time = time.monotonic()
275
+ data = self.load()
276
+
277
+ search_dirs: list[tuple[Path, str]] = []
278
+
279
+ # 1. Custom user directories (highest priority)
280
+ for cd in data.custom_directories:
281
+ p = Path(cd)
282
+ try:
283
+ if p.is_dir():
284
+ search_dirs.append((p, "custom"))
285
+ except OSError:
286
+ continue
287
+
288
+ # 2. Standard known roots
289
+ for sr in self.get_standard_search_roots():
290
+ search_dirs.append((sr, "standard"))
291
+
292
+ # 3. If deep scan requested, include drive roots
293
+ if deep:
294
+ drives = self.get_system_drives()
295
+ if target_drives:
296
+ allowed_letters = {td.upper().rstrip(":\\/") for td in target_drives}
297
+ drives = [d for d in drives if d.drive.upper().rstrip(":") in allowed_letters or str(d) in allowed_letters]
298
+
299
+ for d in drives:
300
+ search_dirs.append((d, "deep_scan"))
301
+
302
+ found_models: list[DiscoveredModel] = []
303
+ seen_paths: set[str] = set()
304
+
305
+ for base_dir, source in search_dirs:
306
+ self._scan_directory(
307
+ base_dir=base_dir,
308
+ source=source,
309
+ found_models=found_models,
310
+ seen_paths=seen_paths,
311
+ max_depth=max_depth if source != "deep_scan" else 4,
312
+ )
313
+
314
+ found_models.sort(key=lambda m: m.name.lower())
315
+
316
+ # Update and save registry
317
+ data.discovered_models = [m.to_dict() for m in found_models]
318
+ data.last_scanned_at = time.time()
319
+ data.scan_stats = {
320
+ "elapsed_seconds": round(time.monotonic() - start_time, 2),
321
+ "total_models": len(found_models),
322
+ "scanned_roots_count": len(search_dirs),
323
+ "deep": deep,
324
+ }
325
+ self.save(data)
326
+
327
+ return found_models
328
+
329
+ def _scan_directory(
330
+ self,
331
+ base_dir: Path,
332
+ source: str,
333
+ found_models: list[DiscoveredModel],
334
+ seen_paths: set[str],
335
+ max_depth: int = 5,
336
+ current_depth: int = 0,
337
+ ) -> None:
338
+ if current_depth > max_depth:
339
+ return
340
+
341
+ try:
342
+ if not base_dir.is_dir():
343
+ return
344
+ for entry in base_dir.iterdir():
345
+ # Check for noise directory skipping
346
+ if entry.is_dir():
347
+ dir_name_lower = entry.name.lower()
348
+ if dir_name_lower in _SKIP_DIR_NAMES or dir_name_lower.startswith("."):
349
+ continue
350
+ self._scan_directory(
351
+ base_dir=entry,
352
+ source=source,
353
+ found_models=found_models,
354
+ seen_paths=seen_paths,
355
+ max_depth=max_depth,
356
+ current_depth=current_depth + 1,
357
+ )
358
+ elif entry.is_file() and entry.name.lower().endswith(".gguf"):
359
+ name_lower = entry.name.lower()
360
+ if any(ind in name_lower for ind in _NON_LLM_INDICATORS):
361
+ continue
362
+
363
+ norm_path = str(entry.resolve())
364
+ if norm_path in seen_paths:
365
+ continue
366
+ seen_paths.add(norm_path)
367
+
368
+ try:
369
+ stat = entry.stat()
370
+ size_gb = round(stat.st_size / (1024**3), 2)
371
+ # Filter out tiny test files < 200MB (unless custom directory)
372
+ if size_gb >= 0.2 or source == "custom":
373
+ found_models.append(
374
+ DiscoveredModel(
375
+ name=entry.name,
376
+ display_name=entry.stem,
377
+ path=norm_path,
378
+ size_gb=size_gb,
379
+ backend="gguf",
380
+ source=source,
381
+ modified_at=stat.st_mtime,
382
+ )
383
+ )
384
+ except Exception:
385
+ pass
386
+ except (PermissionError, OSError):
387
+ pass
388
+
389
+
390
+ # Global singleton registry
391
+ _GLOBAL_REGISTRY: LocalModelRegistry | None = None
392
+ _GLOBAL_REGISTRY_LOCK = threading.Lock()
393
+
394
+
395
+ def get_model_registry() -> LocalModelRegistry:
396
+ """Return singleton instance of LocalModelRegistry."""
397
+ global _GLOBAL_REGISTRY
398
+ with _GLOBAL_REGISTRY_LOCK:
399
+ if _GLOBAL_REGISTRY is None or _GLOBAL_REGISTRY.registry_file != _REGISTRY_FILE:
400
+ _GLOBAL_REGISTRY = LocalModelRegistry()
401
+ return _GLOBAL_REGISTRY
402
+
403
+
404
+ def discover_all_gguf_models(deep: bool = False, force_rescan: bool = False) -> list[dict[str, Any]]:
405
+ """Helper function to discover all GGUF models across local machine."""
406
+ reg = get_model_registry()
407
+ if force_rescan:
408
+ models = reg.scan(deep=deep)
409
+ else:
410
+ models = reg.get_models(auto_scan=True)
411
+ if not models and not force_rescan:
412
+ models = reg.scan(deep=deep)
413
+ return [m.to_dict() for m in models]
414
+
415
+
416
+ def get_live_system_path() -> str:
417
+ """Read fresh Windows User and System PATH from Registry so dynamically added paths work immediately."""
418
+ paths: list[str] = []
419
+ if os.name == "nt":
420
+ try:
421
+ import winreg
422
+ for root, subkey in [
423
+ (winreg.HKEY_CURRENT_USER, r"Environment"),
424
+ (winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"),
425
+ ]:
426
+ try:
427
+ with winreg.OpenKey(root, subkey) as key:
428
+ val, _ = winreg.QueryValueEx(key, "Path")
429
+ if val:
430
+ paths.extend(val.split(";"))
431
+ except Exception:
432
+ pass
433
+ except Exception:
434
+ pass
435
+
436
+ current_p = os.environ.get("PATH", "")
437
+ paths.extend(current_p.split(os.pathsep))
438
+ expanded = [os.path.expandvars(p.strip()) for p in paths if p.strip()]
439
+ cleaned = [p for p in expanded if p and Path(p).exists()]
440
+ return os.pathsep.join(list(dict.fromkeys(cleaned)))
441
+
442
+
443
+ def discover_llama_server_binary(custom_path: str | None = None) -> str | None:
444
+ """Universally discover llama-server executable without any hardcoded drive letters or paths."""
445
+ # 1. Custom explicit path if provided
446
+ if custom_path and custom_path.strip():
447
+ p = Path(custom_path.strip()).expanduser()
448
+ if p.is_file():
449
+ return str(p.resolve())
450
+
451
+ # 2. Environment variables
452
+ for env_var in ("LLAMA_SERVER_PATH", "LLAMA_CPP_PATH", "LLAMA_BIN", "LLAMACPP_SERVER_PATH"):
453
+ env_val = os.environ.get(env_var)
454
+ if env_val and env_val.strip():
455
+ p = Path(env_val.strip()).expanduser()
456
+ if p.is_file():
457
+ return str(p.resolve())
458
+ if p.is_dir():
459
+ for name in ("llama-server.exe", "llama-server", "server.exe", "server"):
460
+ cand = p / name
461
+ if cand.is_file():
462
+ return str(cand.resolve())
463
+
464
+ # 3. Live system PATH across the machine
465
+ live_path = get_live_system_path()
466
+ for name in ("llama-server", "llama-server.exe", "server", "server.exe"):
467
+ found = shutil.which(name, path=live_path)
468
+ if found:
469
+ return str(Path(found).resolve())
470
+
471
+ # 4. Universal relative subpaths across all dynamically detected system drives and user home
472
+ # A discovered executable path is an explicit local installation, not a
473
+ # request to execute a foreign binary. Check both conventional suffixes
474
+ # on every platform so discovery and its fixtures stay path-based.
475
+ exe_names = ("llama-server.exe", "llama-server", "server.exe", "server")
476
+
477
+ subpaths = [
478
+ Path("AI") / "llama-server",
479
+ Path("AI") / "llama.cpp",
480
+ Path("llama-server"),
481
+ Path("llama.cpp"),
482
+ Path("tools") / "llama-server",
483
+ Path("tools") / "llama.cpp",
484
+ Path("bin"),
485
+ Path("Program Files") / "llama.cpp",
486
+ Path("Program Files") / "llama-server",
487
+ Path(".docker") / "bin" / "inference",
488
+ Path(".local") / "bin",
489
+ Path("AppData") / "Local" / "Programs" / "llama-server",
490
+ Path("AppData") / "Local" / "Programs" / "llama.cpp",
491
+ ]
492
+
493
+ # Search user home directory standard paths safely
494
+ try:
495
+ home = Path.home()
496
+ except Exception:
497
+ home = None
498
+
499
+ if home:
500
+ for sp in subpaths:
501
+ for name in exe_names:
502
+ cand = home / sp / name
503
+ if cand.is_file():
504
+ return str(cand.resolve())
505
+
506
+ # Search across all dynamically detected drives (C:\, D:\, E:\, etc. on Windows, or / on POSIX)
507
+ for drive in LocalModelRegistry.get_system_drives():
508
+ for sp in subpaths:
509
+ for name in exe_names:
510
+ cand = drive / sp / name
511
+ if cand.is_file():
512
+ return str(cand.resolve())
513
+
514
+ # POSIX standard directories
515
+ if os.name != "nt":
516
+ for posix_p in (
517
+ Path("/usr/local/bin/llama-server"),
518
+ Path("/opt/homebrew/bin/llama-server"),
519
+ Path("/usr/bin/llama-server"),
520
+ Path("/opt/llama.cpp/llama-server"),
521
+ ):
522
+ if posix_p.is_file():
523
+ return str(posix_p.resolve())
524
+
525
+ return None