caudate-cli 0.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 (153) hide show
  1. api/__init__.py +5 -0
  2. api/anthropic_compat.py +1518 -0
  3. api/artifact_viewer.py +366 -0
  4. api/caudate_middleware.py +618 -0
  5. api/forge_bootstrapper_routes.py +377 -0
  6. api/forge_routes.py +630 -0
  7. api/forge_system_routes.py +294 -0
  8. api/openai_compat.py +1993 -0
  9. api/server.py +667 -0
  10. api/storyboard_page.py +677 -0
  11. caudate_cli-0.1.0.dist-info/METADATA +354 -0
  12. caudate_cli-0.1.0.dist-info/RECORD +153 -0
  13. caudate_cli-0.1.0.dist-info/WHEEL +5 -0
  14. caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
  15. caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  16. caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
  17. cognos_mcp/__init__.py +4 -0
  18. cognos_mcp/bridge.py +41 -0
  19. cognos_mcp/client.py +70 -0
  20. cognos_mcp/config.py +49 -0
  21. cognos_mcp/server.py +66 -0
  22. config.py +82 -0
  23. core/__init__.py +0 -0
  24. core/agent.py +468 -0
  25. core/agentic_loop.py +731 -0
  26. core/anthropic_auth.py +91 -0
  27. core/background.py +113 -0
  28. core/banner.py +134 -0
  29. core/bootstrap.py +292 -0
  30. core/citations.py +131 -0
  31. core/compaction.py +109 -0
  32. core/constitution.py +198 -0
  33. core/diff_viewer.py +87 -0
  34. core/export.py +85 -0
  35. core/file_refs.py +119 -0
  36. core/files.py +199 -0
  37. core/hooks.py +209 -0
  38. core/image.py +599 -0
  39. core/input.py +91 -0
  40. core/loop.py +238 -0
  41. core/memory_md.py +147 -0
  42. core/notifications.py +99 -0
  43. core/ownership.py +181 -0
  44. core/paste.py +81 -0
  45. core/permissions.py +210 -0
  46. core/plan_mode.py +215 -0
  47. core/sandbox_prompt.py +185 -0
  48. core/scheduler.py +195 -0
  49. core/schemas.py +202 -0
  50. core/session.py +90 -0
  51. core/settings.py +132 -0
  52. core/skills.py +398 -0
  53. core/slash_commands.py +977 -0
  54. core/statusline.py +61 -0
  55. core/subagent.py +300 -0
  56. core/thinking.py +50 -0
  57. core/updater.py +122 -0
  58. core/usage.py +109 -0
  59. core/worktree.py +93 -0
  60. execution/__init__.py +0 -0
  61. execution/executor.py +329 -0
  62. execution/plugins.py +108 -0
  63. execution/tools/__init__.py +0 -0
  64. execution/tools/agent_tool.py +107 -0
  65. execution/tools/agentic_tool.py +297 -0
  66. execution/tools/artifact_tool.py +191 -0
  67. execution/tools/ask_user_question_tool.py +137 -0
  68. execution/tools/base.py +81 -0
  69. execution/tools/calculator_tool.py +137 -0
  70. execution/tools/cognos_card_tool.py +124 -0
  71. execution/tools/cron_tool.py +215 -0
  72. execution/tools/datetime_tool.py +215 -0
  73. execution/tools/describe_image_tool.py +161 -0
  74. execution/tools/draw_tool.py +164 -0
  75. execution/tools/edit_image_tool.py +262 -0
  76. execution/tools/edit_tool.py +245 -0
  77. execution/tools/file_tool.py +90 -0
  78. execution/tools/find_anywhere_tool.py +255 -0
  79. execution/tools/forge_feature_tools.py +377 -0
  80. execution/tools/glob_tool.py +59 -0
  81. execution/tools/grep_tool.py +89 -0
  82. execution/tools/http_request_tool.py +224 -0
  83. execution/tools/load_skill_tool.py +104 -0
  84. execution/tools/longcat_avatar_tool.py +384 -0
  85. execution/tools/mcp_tool.py +100 -0
  86. execution/tools/notebook_tool.py +279 -0
  87. execution/tools/openapi_tool.py +440 -0
  88. execution/tools/plan_mode_tool.py +95 -0
  89. execution/tools/push_notification_tool.py +157 -0
  90. execution/tools/python_tool.py +61 -0
  91. execution/tools/respond_tool.py +40 -0
  92. execution/tools/sandbox_tool.py +378 -0
  93. execution/tools/search_tool.py +153 -0
  94. execution/tools/semantic_search_tool.py +106 -0
  95. execution/tools/shell_tool.py +283 -0
  96. execution/tools/speak_tool.py +134 -0
  97. execution/tools/storyboard_tool.py +727 -0
  98. execution/tools/system_info_tool.py +212 -0
  99. execution/tools/task_tool.py +323 -0
  100. execution/tools/think_tool.py +49 -0
  101. execution/tools/transcribe_audio_tool.py +86 -0
  102. execution/tools/update_memory_tool.py +92 -0
  103. execution/tools/web_fetch_tool.py +82 -0
  104. execution/tools/worktree_tool.py +174 -0
  105. llm/__init__.py +0 -0
  106. llm/fallback.py +116 -0
  107. llm/models.py +320 -0
  108. llm/provider.py +1356 -0
  109. llm/router.py +373 -0
  110. main.py +1889 -0
  111. memory/__init__.py +0 -0
  112. memory/episodic.py +99 -0
  113. memory/procedural.py +145 -0
  114. memory/semantic.py +71 -0
  115. memory/working.py +64 -0
  116. nn/__init__.py +43 -0
  117. nn/auto_evolve.py +245 -0
  118. nn/caudate.py +136 -0
  119. nn/config.py +141 -0
  120. nn/consolidator.py +81 -0
  121. nn/data.py +1635 -0
  122. nn/encoder.py +258 -0
  123. nn/forge_advisor.py +303 -0
  124. nn/format.py +235 -0
  125. nn/heads.py +432 -0
  126. nn/observer.py +994 -0
  127. nn/policy.py +214 -0
  128. nn/runtime.py +343 -0
  129. nn/scorer.py +175 -0
  130. nn/trainer.py +515 -0
  131. nn/vision.py +352 -0
  132. personality/__init__.py +23 -0
  133. personality/engine.py +129 -0
  134. personality/identity.py +144 -0
  135. personality/inner_voice.py +100 -0
  136. personality/mood.py +205 -0
  137. planning/__init__.py +0 -0
  138. planning/dev_server.py +221 -0
  139. planning/forge_models.py +718 -0
  140. planning/orchestrator.py +1363 -0
  141. planning/planner.py +451 -0
  142. planning/task_graph.py +61 -0
  143. reflection/__init__.py +0 -0
  144. reflection/meta_learner.py +156 -0
  145. reflection/reflector.py +127 -0
  146. ui/__init__.py +5 -0
  147. ui/display.py +88 -0
  148. voice/__init__.py +0 -0
  149. voice/conversation.py +125 -0
  150. voice/listener.py +111 -0
  151. voice/speaker.py +59 -0
  152. voice/stt.py +126 -0
  153. voice/tts.py +214 -0
@@ -0,0 +1,294 @@
1
+ """Forge system + provider routes — hardware detection, provider scan.
2
+
3
+ Mirrors LocalForge's /api/system/hardware and /api/providers/scan
4
+ under Cognos's /forge prefix. Lazy detection + on-disk cache so the
5
+ hot path is fast on every platform.
6
+
7
+ Endpoints:
8
+ GET /forge/system/hardware?refresh=1 CPU/RAM/GPU info
9
+ GET /forge/providers/scan probe Ollama + LM-Studio
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import json
16
+ import logging
17
+ import os
18
+ import platform
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ import httpx
27
+ from fastapi import APIRouter
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ _HARDWARE_CACHE_PATH = Path(__file__).resolve().parents[1] / "data" / "hardware-cache.json"
32
+ _memory_cache: dict[str, Any] | None = None
33
+
34
+
35
+ # ─────────────────────────── Hardware detection ────────────────────────
36
+
37
+
38
+ def _read_disk_cache() -> dict[str, Any] | None:
39
+ try:
40
+ if not _HARDWARE_CACHE_PATH.exists():
41
+ return None
42
+ data = json.loads(_HARDWARE_CACHE_PATH.read_text())
43
+ if data.get("platform") != sys.platform:
44
+ return None
45
+ return data
46
+ except Exception:
47
+ return None
48
+
49
+
50
+ def _write_disk_cache(value: dict[str, Any]) -> None:
51
+ try:
52
+ _HARDWARE_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
53
+ _HARDWARE_CACHE_PATH.write_text(json.dumps(value, indent=2))
54
+ except Exception as e:
55
+ logger.debug(f"hardware cache write failed: {e}")
56
+
57
+
58
+ def _detect_apple_silicon() -> bool:
59
+ return sys.platform == "darwin" and platform.machine() in ("arm64", "aarch64")
60
+
61
+
62
+ def _detect_total_ram_mb() -> int:
63
+ try:
64
+ import psutil
65
+ return round(psutil.virtual_memory().total / 1024 / 1024)
66
+ except Exception:
67
+ # Fallback for hosts without psutil
68
+ try:
69
+ with open("/proc/meminfo") as f:
70
+ for line in f:
71
+ if line.startswith("MemTotal:"):
72
+ kb = int(line.split()[1])
73
+ return round(kb / 1024)
74
+ except Exception:
75
+ pass
76
+ return 0
77
+
78
+
79
+ def _detect_nvidia_gpus() -> list[dict[str, Any]]:
80
+ """Return one dict per NVIDIA GPU via nvidia-smi."""
81
+ if shutil.which("nvidia-smi") is None:
82
+ return []
83
+ try:
84
+ out = subprocess.check_output(
85
+ ["nvidia-smi",
86
+ "--query-gpu=name,memory.total",
87
+ "--format=csv,noheader,nounits"],
88
+ stderr=subprocess.DEVNULL, timeout=3,
89
+ ).decode().strip()
90
+ except Exception:
91
+ return []
92
+ gpus: list[dict[str, Any]] = []
93
+ for line in out.splitlines():
94
+ parts = [p.strip() for p in line.split(",")]
95
+ if len(parts) >= 2:
96
+ try:
97
+ vram = int(float(parts[1]))
98
+ except ValueError:
99
+ continue
100
+ gpus.append({
101
+ "vendor": "NVIDIA",
102
+ "model": parts[0],
103
+ "vram_mb": vram,
104
+ })
105
+ return gpus
106
+
107
+
108
+ def _detect_amd_gpus() -> list[dict[str, Any]]:
109
+ """Best-effort AMD ROCm detection via rocm-smi."""
110
+ if shutil.which("rocm-smi") is None:
111
+ return []
112
+ try:
113
+ out = subprocess.check_output(
114
+ ["rocm-smi", "--showmeminfo", "vram", "--csv"],
115
+ stderr=subprocess.DEVNULL, timeout=3,
116
+ ).decode()
117
+ except Exception:
118
+ return []
119
+ gpus: list[dict[str, Any]] = []
120
+ # Parse CSV; columns vary by version. Look for vram total bytes.
121
+ for line in out.splitlines():
122
+ if "card" not in line.lower():
123
+ continue
124
+ cols = [c.strip() for c in line.split(",")]
125
+ if len(cols) >= 2:
126
+ try:
127
+ vram_bytes = int(cols[-1])
128
+ gpus.append({
129
+ "vendor": "AMD",
130
+ "model": cols[0],
131
+ "vram_mb": vram_bytes // (1024 * 1024),
132
+ })
133
+ except ValueError:
134
+ pass
135
+ return gpus
136
+
137
+
138
+ def _detect_apple_gpu_budget(total_ram_mb: int) -> dict[str, Any]:
139
+ """Apple Silicon: GPU shares unified memory. Budget ~75% of RAM."""
140
+ return {
141
+ "vendor": "Apple",
142
+ "model": "Apple Silicon",
143
+ "vram_mb": round(total_ram_mb * 0.75),
144
+ }
145
+
146
+
147
+ def _detect_hardware() -> dict[str, Any]:
148
+ """Snapshot the local machine's CPU/RAM/GPU. Conservative — failures
149
+ return ``{available: false, reason: ...}`` so the UI can render a
150
+ helpful message instead of a stack trace."""
151
+ plat = sys.platform
152
+ total_ram = _detect_total_ram_mb()
153
+ is_apple = _detect_apple_silicon()
154
+ cpu_count = os.cpu_count() or 0
155
+
156
+ if is_apple:
157
+ gpu = _detect_apple_gpu_budget(total_ram)
158
+ return {
159
+ "available": True,
160
+ "platform": plat,
161
+ "is_apple_silicon": True,
162
+ "cpu_count": cpu_count,
163
+ "gpus": [gpu],
164
+ "total_vram_mb": gpu["vram_mb"],
165
+ "total_system_ram_mb": total_ram,
166
+ "detected_at": time.time(),
167
+ }
168
+
169
+ gpus = _detect_nvidia_gpus() + _detect_amd_gpus()
170
+ if not gpus:
171
+ return {
172
+ "available": False,
173
+ "platform": plat,
174
+ "is_apple_silicon": False,
175
+ "cpu_count": cpu_count,
176
+ "total_system_ram_mb": total_ram,
177
+ "reason": "No discrete GPU with reportable VRAM detected "
178
+ "(checked nvidia-smi, rocm-smi).",
179
+ "detected_at": time.time(),
180
+ }
181
+ gpus.sort(key=lambda g: g["vram_mb"], reverse=True)
182
+ return {
183
+ "available": True,
184
+ "platform": plat,
185
+ "is_apple_silicon": False,
186
+ "cpu_count": cpu_count,
187
+ "gpus": gpus,
188
+ "total_vram_mb": max(g["vram_mb"] for g in gpus),
189
+ "total_system_ram_mb": total_ram,
190
+ "detected_at": time.time(),
191
+ }
192
+
193
+
194
+ # ─────────────────────────── Provider scan ─────────────────────────────
195
+
196
+
197
+ _PROVIDERS = [
198
+ {
199
+ "id": "ollama",
200
+ "label": "Ollama",
201
+ "default_base_url": "http://127.0.0.1:11434",
202
+ "tags_path": "/api/tags",
203
+ },
204
+ {
205
+ "id": "lm-studio",
206
+ "label": "LM Studio",
207
+ "default_base_url": "http://127.0.0.1:1234",
208
+ "tags_path": "/v1/models",
209
+ },
210
+ ]
211
+
212
+
213
+ async def _probe_ollama(base_url: str, timeout: float = 1.5) -> int | None:
214
+ """Return model count if Ollama responds with the expected shape."""
215
+ url = base_url.rstrip("/") + "/api/tags"
216
+ try:
217
+ async with httpx.AsyncClient(timeout=timeout) as cx:
218
+ r = await cx.get(url)
219
+ if r.status_code != 200:
220
+ return None
221
+ data = r.json()
222
+ if not isinstance(data, dict) or not isinstance(data.get("models"), list):
223
+ return None
224
+ return len(data["models"])
225
+ except Exception:
226
+ return None
227
+
228
+
229
+ async def _probe_lm_studio(base_url: str, timeout: float = 1.5) -> int | None:
230
+ """Return model count if LM-Studio's OpenAI-compat endpoint answers."""
231
+ url = base_url.rstrip("/") + "/v1/models"
232
+ try:
233
+ async with httpx.AsyncClient(timeout=timeout) as cx:
234
+ r = await cx.get(url)
235
+ if r.status_code != 200:
236
+ return None
237
+ data = r.json()
238
+ if not isinstance(data, dict) or not isinstance(data.get("data"), list):
239
+ return None
240
+ return len(data["data"])
241
+ except Exception:
242
+ return None
243
+
244
+
245
+ async def _scan_providers() -> list[dict[str, Any]]:
246
+ hits: list[dict[str, Any]] = []
247
+ probes = [
248
+ (_PROVIDERS[0], _probe_ollama(_PROVIDERS[0]["default_base_url"])),
249
+ (_PROVIDERS[1], _probe_lm_studio(_PROVIDERS[1]["default_base_url"])),
250
+ ]
251
+ for desc, coro in probes:
252
+ try:
253
+ count = await coro
254
+ except Exception:
255
+ count = None
256
+ if count is not None:
257
+ hits.append({
258
+ "provider_id": desc["id"],
259
+ "label": desc["label"],
260
+ "url": desc["default_base_url"],
261
+ "model_count": count,
262
+ })
263
+ return hits
264
+
265
+
266
+ # ─────────────────────────────── Router ────────────────────────────────
267
+
268
+
269
+ def build_router() -> APIRouter:
270
+ router = APIRouter(prefix="/forge", tags=["forge-system"])
271
+
272
+ @router.get("/system/hardware")
273
+ async def hardware(refresh: int = 0) -> dict[str, Any]:
274
+ global _memory_cache
275
+ if not refresh and _memory_cache is not None:
276
+ return _memory_cache
277
+ if not refresh:
278
+ disk = _read_disk_cache()
279
+ if disk is not None:
280
+ _memory_cache = disk
281
+ return disk
282
+ result = await asyncio.get_running_loop().run_in_executor(
283
+ None, _detect_hardware,
284
+ )
285
+ _memory_cache = result
286
+ if result.get("available"):
287
+ _write_disk_cache(result)
288
+ return result
289
+
290
+ @router.get("/providers/scan")
291
+ async def providers_scan() -> dict[str, Any]:
292
+ return {"hits": await _scan_providers()}
293
+
294
+ return router