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.
- api/__init__.py +5 -0
- api/anthropic_compat.py +1518 -0
- api/artifact_viewer.py +366 -0
- api/caudate_middleware.py +618 -0
- api/forge_bootstrapper_routes.py +377 -0
- api/forge_routes.py +630 -0
- api/forge_system_routes.py +294 -0
- api/openai_compat.py +1993 -0
- api/server.py +667 -0
- api/storyboard_page.py +677 -0
- caudate_cli-0.1.0.dist-info/METADATA +354 -0
- caudate_cli-0.1.0.dist-info/RECORD +153 -0
- caudate_cli-0.1.0.dist-info/WHEEL +5 -0
- caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
- caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
- cognos_mcp/__init__.py +4 -0
- cognos_mcp/bridge.py +41 -0
- cognos_mcp/client.py +70 -0
- cognos_mcp/config.py +49 -0
- cognos_mcp/server.py +66 -0
- config.py +82 -0
- core/__init__.py +0 -0
- core/agent.py +468 -0
- core/agentic_loop.py +731 -0
- core/anthropic_auth.py +91 -0
- core/background.py +113 -0
- core/banner.py +134 -0
- core/bootstrap.py +292 -0
- core/citations.py +131 -0
- core/compaction.py +109 -0
- core/constitution.py +198 -0
- core/diff_viewer.py +87 -0
- core/export.py +85 -0
- core/file_refs.py +119 -0
- core/files.py +199 -0
- core/hooks.py +209 -0
- core/image.py +599 -0
- core/input.py +91 -0
- core/loop.py +238 -0
- core/memory_md.py +147 -0
- core/notifications.py +99 -0
- core/ownership.py +181 -0
- core/paste.py +81 -0
- core/permissions.py +210 -0
- core/plan_mode.py +215 -0
- core/sandbox_prompt.py +185 -0
- core/scheduler.py +195 -0
- core/schemas.py +202 -0
- core/session.py +90 -0
- core/settings.py +132 -0
- core/skills.py +398 -0
- core/slash_commands.py +977 -0
- core/statusline.py +61 -0
- core/subagent.py +300 -0
- core/thinking.py +50 -0
- core/updater.py +122 -0
- core/usage.py +109 -0
- core/worktree.py +93 -0
- execution/__init__.py +0 -0
- execution/executor.py +329 -0
- execution/plugins.py +108 -0
- execution/tools/__init__.py +0 -0
- execution/tools/agent_tool.py +107 -0
- execution/tools/agentic_tool.py +297 -0
- execution/tools/artifact_tool.py +191 -0
- execution/tools/ask_user_question_tool.py +137 -0
- execution/tools/base.py +81 -0
- execution/tools/calculator_tool.py +137 -0
- execution/tools/cognos_card_tool.py +124 -0
- execution/tools/cron_tool.py +215 -0
- execution/tools/datetime_tool.py +215 -0
- execution/tools/describe_image_tool.py +161 -0
- execution/tools/draw_tool.py +164 -0
- execution/tools/edit_image_tool.py +262 -0
- execution/tools/edit_tool.py +245 -0
- execution/tools/file_tool.py +90 -0
- execution/tools/find_anywhere_tool.py +255 -0
- execution/tools/forge_feature_tools.py +377 -0
- execution/tools/glob_tool.py +59 -0
- execution/tools/grep_tool.py +89 -0
- execution/tools/http_request_tool.py +224 -0
- execution/tools/load_skill_tool.py +104 -0
- execution/tools/longcat_avatar_tool.py +384 -0
- execution/tools/mcp_tool.py +100 -0
- execution/tools/notebook_tool.py +279 -0
- execution/tools/openapi_tool.py +440 -0
- execution/tools/plan_mode_tool.py +95 -0
- execution/tools/push_notification_tool.py +157 -0
- execution/tools/python_tool.py +61 -0
- execution/tools/respond_tool.py +40 -0
- execution/tools/sandbox_tool.py +378 -0
- execution/tools/search_tool.py +153 -0
- execution/tools/semantic_search_tool.py +106 -0
- execution/tools/shell_tool.py +283 -0
- execution/tools/speak_tool.py +134 -0
- execution/tools/storyboard_tool.py +727 -0
- execution/tools/system_info_tool.py +212 -0
- execution/tools/task_tool.py +323 -0
- execution/tools/think_tool.py +49 -0
- execution/tools/transcribe_audio_tool.py +86 -0
- execution/tools/update_memory_tool.py +92 -0
- execution/tools/web_fetch_tool.py +82 -0
- execution/tools/worktree_tool.py +174 -0
- llm/__init__.py +0 -0
- llm/fallback.py +116 -0
- llm/models.py +320 -0
- llm/provider.py +1356 -0
- llm/router.py +373 -0
- main.py +1889 -0
- memory/__init__.py +0 -0
- memory/episodic.py +99 -0
- memory/procedural.py +145 -0
- memory/semantic.py +71 -0
- memory/working.py +64 -0
- nn/__init__.py +43 -0
- nn/auto_evolve.py +245 -0
- nn/caudate.py +136 -0
- nn/config.py +141 -0
- nn/consolidator.py +81 -0
- nn/data.py +1635 -0
- nn/encoder.py +258 -0
- nn/forge_advisor.py +303 -0
- nn/format.py +235 -0
- nn/heads.py +432 -0
- nn/observer.py +994 -0
- nn/policy.py +214 -0
- nn/runtime.py +343 -0
- nn/scorer.py +175 -0
- nn/trainer.py +515 -0
- nn/vision.py +352 -0
- personality/__init__.py +23 -0
- personality/engine.py +129 -0
- personality/identity.py +144 -0
- personality/inner_voice.py +100 -0
- personality/mood.py +205 -0
- planning/__init__.py +0 -0
- planning/dev_server.py +221 -0
- planning/forge_models.py +718 -0
- planning/orchestrator.py +1363 -0
- planning/planner.py +451 -0
- planning/task_graph.py +61 -0
- reflection/__init__.py +0 -0
- reflection/meta_learner.py +156 -0
- reflection/reflector.py +127 -0
- ui/__init__.py +5 -0
- ui/display.py +88 -0
- voice/__init__.py +0 -0
- voice/conversation.py +125 -0
- voice/listener.py +111 -0
- voice/speaker.py +59 -0
- voice/stt.py +126 -0
- voice/tts.py +214 -0
core/anthropic_auth.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Subscription-OAuth helper for Cognos's web UI calls to Anthropic.
|
|
2
|
+
|
|
3
|
+
Background
|
|
4
|
+
----------
|
|
5
|
+
Claude Code (the CLI) authenticates to Anthropic with an OAuth Bearer
|
|
6
|
+
token stored at ``~/.claude/.credentials.json``. The Cognos passthrough
|
|
7
|
+
proxy at ``/v1/messages`` already forwards that token verbatim from
|
|
8
|
+
incoming Claude Code requests — it never has to read the file itself.
|
|
9
|
+
|
|
10
|
+
The web UI is a different story: it calls Cognos's own ``/chat`` endpoint,
|
|
11
|
+
which goes through ``LLMProvider`` → LiteLLM → Anthropic. LiteLLM expects
|
|
12
|
+
``ANTHROPIC_API_KEY`` in the environment. The user has a subscription, not
|
|
13
|
+
an API key — so without help, the web UI silently fails on Opus.
|
|
14
|
+
|
|
15
|
+
Scope
|
|
16
|
+
-----
|
|
17
|
+
This helper is intentionally **scoped to the web UI only**. It uses a
|
|
18
|
+
``ContextVar`` set by the ``/chat`` and ``/chat/stream`` endpoints, so
|
|
19
|
+
that the same ``LLMProvider`` instance behaves normally for every other
|
|
20
|
+
caller (background agentic loops, voice, MCP). Other code paths that hit
|
|
21
|
+
Anthropic without a real API key will still fail with the original auth
|
|
22
|
+
error — that's the desired blast radius.
|
|
23
|
+
|
|
24
|
+
Token freshness
|
|
25
|
+
---------------
|
|
26
|
+
We read the credentials file on every call rather than caching, since
|
|
27
|
+
Claude Code may rotate the access token. The file is ~470 bytes; the
|
|
28
|
+
parse cost is negligible next to the network round-trip to Anthropic.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import logging
|
|
35
|
+
from contextlib import contextmanager
|
|
36
|
+
from contextvars import ContextVar
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Iterator
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_CREDENTIALS_PATH = Path.home() / ".claude" / ".credentials.json"
|
|
44
|
+
|
|
45
|
+
# Set by web-UI endpoints during their request scope. LLMProvider checks
|
|
46
|
+
# this; if False (the default), behaviour is identical to before.
|
|
47
|
+
_use_subscription: ContextVar[bool] = ContextVar(
|
|
48
|
+
"cognos_use_subscription_auth", default=False,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_active() -> bool:
|
|
53
|
+
"""True if the current async-context is allowed to use the subscription."""
|
|
54
|
+
return _use_subscription.get()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@contextmanager
|
|
58
|
+
def subscription_auth_scope() -> Iterator[None]:
|
|
59
|
+
"""Enable subscription-OAuth auth for the duration of the ``with`` block.
|
|
60
|
+
|
|
61
|
+
Usage (inside a request handler)::
|
|
62
|
+
|
|
63
|
+
with subscription_auth_scope():
|
|
64
|
+
reply = await agent.chat(...)
|
|
65
|
+
"""
|
|
66
|
+
token = _use_subscription.set(True)
|
|
67
|
+
try:
|
|
68
|
+
yield
|
|
69
|
+
finally:
|
|
70
|
+
_use_subscription.reset(token)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_subscription_token() -> str | None:
|
|
74
|
+
"""Read the current Claude Code OAuth access token, or None if missing.
|
|
75
|
+
|
|
76
|
+
Returns None on any failure (file missing, bad JSON, missing key) so
|
|
77
|
+
callers can fall through gracefully rather than crash.
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
with _CREDENTIALS_PATH.open() as f:
|
|
81
|
+
data = json.load(f)
|
|
82
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
83
|
+
logger.debug(f"could not read {_CREDENTIALS_PATH}: {e}")
|
|
84
|
+
return None
|
|
85
|
+
oauth = data.get("claudeAiOauth")
|
|
86
|
+
if not isinstance(oauth, dict):
|
|
87
|
+
return None
|
|
88
|
+
token = oauth.get("accessToken")
|
|
89
|
+
if not isinstance(token, str) or not token:
|
|
90
|
+
return None
|
|
91
|
+
return token
|
core/background.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Background task management — run agent turns asynchronously.
|
|
2
|
+
|
|
3
|
+
A background task is a CognosAgent.chat call that's been kicked off
|
|
4
|
+
without blocking the REPL. The pool tracks each task by id, exposes
|
|
5
|
+
status (pending/running/done/failed), and stores the final reply so the
|
|
6
|
+
user can `watch` it later.
|
|
7
|
+
|
|
8
|
+
This is intentionally process-local: when the CLI exits, in-flight
|
|
9
|
+
tasks are abandoned. Persistence would require a separate worker
|
|
10
|
+
process or supervisor — outside scope of a UX feature.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, Awaitable, Callable
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class BackgroundTask:
|
|
27
|
+
id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])
|
|
28
|
+
label: str = ""
|
|
29
|
+
status: str = "pending" # pending | running | done | failed | cancelled
|
|
30
|
+
started_at: float = 0.0
|
|
31
|
+
finished_at: float = 0.0
|
|
32
|
+
result: str | None = None
|
|
33
|
+
error: str | None = None
|
|
34
|
+
_task: asyncio.Task | None = field(default=None, repr=False)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def duration(self) -> float:
|
|
38
|
+
if not self.started_at:
|
|
39
|
+
return 0.0
|
|
40
|
+
end = self.finished_at or time.time()
|
|
41
|
+
return end - self.started_at
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BackgroundTaskPool:
|
|
45
|
+
"""In-process pool of CognosAgent invocations."""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._tasks: dict[str, BackgroundTask] = {}
|
|
49
|
+
|
|
50
|
+
def submit(
|
|
51
|
+
self,
|
|
52
|
+
coro_factory: Callable[[], Awaitable[Any]],
|
|
53
|
+
label: str = "",
|
|
54
|
+
) -> BackgroundTask:
|
|
55
|
+
"""Schedule `coro_factory()` on the running loop.
|
|
56
|
+
|
|
57
|
+
Must be called from within a running asyncio loop. The factory
|
|
58
|
+
is invoked once, immediately, and the resulting awaitable is
|
|
59
|
+
wrapped in a Task. The BackgroundTask handle is returned
|
|
60
|
+
synchronously so the caller can show an id.
|
|
61
|
+
"""
|
|
62
|
+
bg = BackgroundTask(label=label)
|
|
63
|
+
bg.status = "running"
|
|
64
|
+
bg.started_at = time.time()
|
|
65
|
+
loop = asyncio.get_running_loop()
|
|
66
|
+
|
|
67
|
+
async def _runner() -> None:
|
|
68
|
+
try:
|
|
69
|
+
result = await coro_factory()
|
|
70
|
+
bg.result = str(result) if result is not None else ""
|
|
71
|
+
bg.status = "done"
|
|
72
|
+
except asyncio.CancelledError:
|
|
73
|
+
bg.status = "cancelled"
|
|
74
|
+
raise
|
|
75
|
+
except Exception as e:
|
|
76
|
+
bg.error = str(e)
|
|
77
|
+
bg.status = "failed"
|
|
78
|
+
logger.exception(f"Background task {bg.id} failed: {e}")
|
|
79
|
+
finally:
|
|
80
|
+
bg.finished_at = time.time()
|
|
81
|
+
|
|
82
|
+
bg._task = loop.create_task(_runner(), name=f"cognos-bg-{bg.id}")
|
|
83
|
+
self._tasks[bg.id] = bg
|
|
84
|
+
return bg
|
|
85
|
+
|
|
86
|
+
def get(self, task_id: str) -> BackgroundTask | None:
|
|
87
|
+
return self._tasks.get(task_id)
|
|
88
|
+
|
|
89
|
+
def list(self) -> list[BackgroundTask]:
|
|
90
|
+
return sorted(self._tasks.values(), key=lambda t: t.started_at, reverse=True)
|
|
91
|
+
|
|
92
|
+
def cancel(self, task_id: str) -> bool:
|
|
93
|
+
bg = self._tasks.get(task_id)
|
|
94
|
+
if bg is None or bg._task is None or bg._task.done():
|
|
95
|
+
return False
|
|
96
|
+
bg._task.cancel()
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def prune_finished(self) -> int:
|
|
100
|
+
before = len(self._tasks)
|
|
101
|
+
self._tasks = {
|
|
102
|
+
tid: t for tid, t in self._tasks.items()
|
|
103
|
+
if t.status in ("pending", "running")
|
|
104
|
+
}
|
|
105
|
+
return before - len(self._tasks)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Process-wide pool — mirrors usage tracker.
|
|
109
|
+
_GLOBAL = BackgroundTaskPool()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_global_pool() -> BackgroundTaskPool:
|
|
113
|
+
return _GLOBAL
|
core/banner.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Branded boot banner for the Cognos CLI.
|
|
2
|
+
|
|
3
|
+
Lands on stdout when `cognos` (or `cognos interactive`) starts so the
|
|
4
|
+
launch feels like a real CLI tool — ASCII logo, brain status, a one-
|
|
5
|
+
line tip on slash commands. Replaces the prior ad-hoc `console.print`
|
|
6
|
+
blob in `main.py:interactive`.
|
|
7
|
+
|
|
8
|
+
The banner is plain Rich — no Textual, no full-screen takeover — so
|
|
9
|
+
piping (`cognos -p ...`) and non-TTY contexts still work cleanly.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from rich.align import Align
|
|
17
|
+
from rich.console import Console, Group
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_LOGO = r"""
|
|
23
|
+
____
|
|
24
|
+
/ ___|___ __ _ _ __ ___ ___
|
|
25
|
+
| | / _ \ / _` | '_ \ / _ \/ __|
|
|
26
|
+
| |__| (_) | (_| | | | | (_) \__ \
|
|
27
|
+
\____\___/ \__, |_| |_|\___/|___/
|
|
28
|
+
|___/
|
|
29
|
+
""".rstrip("\n").lstrip("\n")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _version() -> str:
|
|
33
|
+
try:
|
|
34
|
+
from importlib.metadata import version
|
|
35
|
+
return version("cognos")
|
|
36
|
+
except Exception:
|
|
37
|
+
return "0.1.0"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _brain_summary(agent: Any) -> Text:
|
|
41
|
+
"""Two-line summary of which models are wired in."""
|
|
42
|
+
from llm.router import DualLLMProvider
|
|
43
|
+
t = Text()
|
|
44
|
+
if isinstance(agent.llm, DualLLMProvider):
|
|
45
|
+
t.append("S1 ", style="bold cyan")
|
|
46
|
+
t.append(getattr(agent.llm_fast, "model", "?"), style="white")
|
|
47
|
+
t.append(" S2 ", style="bold magenta")
|
|
48
|
+
t.append(getattr(agent.llm_slow, "model", "?"), style="white")
|
|
49
|
+
else:
|
|
50
|
+
t.append("model ", style="bold")
|
|
51
|
+
t.append(getattr(agent.llm, "model", "?"), style="white")
|
|
52
|
+
return t
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _caudate_summary(agent: Any) -> Text | None:
|
|
56
|
+
"""Caudate router trust + active model — only if she's loaded."""
|
|
57
|
+
t = Text()
|
|
58
|
+
try:
|
|
59
|
+
caudate = getattr(agent, "caudate", None)
|
|
60
|
+
if caudate is None:
|
|
61
|
+
return None
|
|
62
|
+
trust = getattr(caudate, "trust_level", None) or getattr(caudate, "trust", None) or "whisper"
|
|
63
|
+
params = getattr(caudate, "n_params", None)
|
|
64
|
+
t.append("caudate ", style="bold green")
|
|
65
|
+
t.append(str(trust).lower(), style="white")
|
|
66
|
+
if params:
|
|
67
|
+
t.append(f" {params/1e6:.2f}M params", style="dim")
|
|
68
|
+
return t
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _session_summary(agent: Any, resumed: bool) -> Text:
|
|
74
|
+
t = Text()
|
|
75
|
+
try:
|
|
76
|
+
sid = agent.session.id
|
|
77
|
+
n = len(agent.session.messages)
|
|
78
|
+
except Exception:
|
|
79
|
+
sid, n = "?", 0
|
|
80
|
+
if resumed:
|
|
81
|
+
t.append("resumed ", style="yellow")
|
|
82
|
+
t.append(f"{sid[:8]} · {n} messages", style="dim")
|
|
83
|
+
else:
|
|
84
|
+
t.append("new session ", style="green")
|
|
85
|
+
t.append(sid[:8], style="dim")
|
|
86
|
+
return t
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def print_startup_banner(
|
|
90
|
+
agent: Any,
|
|
91
|
+
console: Console,
|
|
92
|
+
*,
|
|
93
|
+
resumed: bool = False,
|
|
94
|
+
extras: list[str] | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Render the boot screen.
|
|
97
|
+
|
|
98
|
+
`extras` is a list of mode flags shown as a footer (e.g. ["streaming",
|
|
99
|
+
"thinking"]). Empty list / None hides that line.
|
|
100
|
+
"""
|
|
101
|
+
logo = Text(_LOGO, style="bold cyan")
|
|
102
|
+
version_line = Text()
|
|
103
|
+
version_line.append(f"v{_version()}", style="dim")
|
|
104
|
+
version_line.append(" · ", style="dim")
|
|
105
|
+
version_line.append("local-first cognitive agent", style="italic dim")
|
|
106
|
+
|
|
107
|
+
rows: list[Any] = [
|
|
108
|
+
Align.center(logo),
|
|
109
|
+
Align.center(version_line),
|
|
110
|
+
Text(""),
|
|
111
|
+
_brain_summary(agent),
|
|
112
|
+
]
|
|
113
|
+
caudate = _caudate_summary(agent)
|
|
114
|
+
if caudate is not None:
|
|
115
|
+
rows.append(caudate)
|
|
116
|
+
rows.append(_session_summary(agent, resumed))
|
|
117
|
+
if extras:
|
|
118
|
+
flags = Text(" ".join(extras), style="dim italic")
|
|
119
|
+
rows.append(flags)
|
|
120
|
+
rows.append(Text(""))
|
|
121
|
+
|
|
122
|
+
tip = Text()
|
|
123
|
+
tip.append("type ", style="dim")
|
|
124
|
+
tip.append("/", style="bold")
|
|
125
|
+
tip.append(" for the command palette · ", style="dim")
|
|
126
|
+
tip.append("/quit", style="bold")
|
|
127
|
+
tip.append(" to exit", style="dim")
|
|
128
|
+
rows.append(tip)
|
|
129
|
+
|
|
130
|
+
console.print(Panel(
|
|
131
|
+
Group(*rows),
|
|
132
|
+
border_style="cyan",
|
|
133
|
+
padding=(1, 2),
|
|
134
|
+
))
|
core/bootstrap.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""First-run bootstrap + health checks.
|
|
2
|
+
|
|
3
|
+
A stranger who runs `pip install cognos` shouldn't have to know about
|
|
4
|
+
Ollama, HuggingFace, Anthropic keys, or `data/nn/caudate.pt`. This
|
|
5
|
+
module covers the cold-start path:
|
|
6
|
+
|
|
7
|
+
- `cognos init` — interactive wizard: picks system1/system2, downloads
|
|
8
|
+
Caudate weights, writes `~/.cognos/settings.json`,
|
|
9
|
+
creates a starter sandbox directory.
|
|
10
|
+
- `cognos doctor` — read-only diagnostic: which dependencies are wired,
|
|
11
|
+
which are missing, exact one-line fixes per gap.
|
|
12
|
+
|
|
13
|
+
Caudate weights live on HuggingFace as a public model repo (set via the
|
|
14
|
+
`COGNOS_CAUDATE_HF_REPO` env var, defaulting to `raveuk/cognos-caudate`).
|
|
15
|
+
The repo is expected to contain `caudate.pt` and `caudate.meta.json`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import socket
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
from rich.panel import Panel
|
|
31
|
+
from rich.prompt import Confirm, Prompt
|
|
32
|
+
from rich.table import Table
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_DEFAULT_HF_REPO = "raveuk/cognos-caudate"
|
|
36
|
+
_CAUDATE_FILES = ("caudate.pt", "caudate.meta.json")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------
|
|
40
|
+
# Locations
|
|
41
|
+
# ---------------------------------------------------------------------
|
|
42
|
+
def cognos_home() -> Path:
|
|
43
|
+
"""`~/.cognos`. Created on demand."""
|
|
44
|
+
p = Path(os.path.expanduser("~")) / ".cognos"
|
|
45
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
return p
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def settings_path() -> Path:
|
|
50
|
+
return cognos_home() / "settings.json"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def caudate_dir() -> Path:
|
|
54
|
+
"""Where Caudate weights live for an installed cognos.
|
|
55
|
+
|
|
56
|
+
Repo checkouts have `<repo>/data/nn/caudate.pt`; pip-installed users
|
|
57
|
+
get `~/.cognos/nn/caudate.pt`.
|
|
58
|
+
"""
|
|
59
|
+
p = cognos_home() / "nn"
|
|
60
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
return p
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_first_run() -> bool:
|
|
65
|
+
return not settings_path().exists()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------
|
|
69
|
+
# Checks
|
|
70
|
+
# ---------------------------------------------------------------------
|
|
71
|
+
@dataclass
|
|
72
|
+
class CheckResult:
|
|
73
|
+
name: str
|
|
74
|
+
ok: bool
|
|
75
|
+
detail: str
|
|
76
|
+
fix: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def check_ollama() -> CheckResult:
|
|
80
|
+
"""Is Ollama reachable on the default port?"""
|
|
81
|
+
host = os.environ.get("OLLAMA_HOST", "127.0.0.1:11434")
|
|
82
|
+
if "://" in host:
|
|
83
|
+
host = host.split("://", 1)[1]
|
|
84
|
+
h, _, port = host.partition(":")
|
|
85
|
+
port = int(port or "11434")
|
|
86
|
+
try:
|
|
87
|
+
with socket.create_connection((h, port), timeout=1.5):
|
|
88
|
+
pass
|
|
89
|
+
return CheckResult("ollama", True, f"reachable at {h}:{port}")
|
|
90
|
+
except Exception:
|
|
91
|
+
return CheckResult(
|
|
92
|
+
"ollama", False, f"not reachable at {h}:{port}",
|
|
93
|
+
fix="install from https://ollama.com and run `ollama serve`",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def check_caudate() -> CheckResult:
|
|
98
|
+
"""Are Caudate weights present (either in repo or in ~/.cognos)?"""
|
|
99
|
+
repo = Path.cwd() / "data" / "nn" / "caudate.pt"
|
|
100
|
+
user = caudate_dir() / "caudate.pt"
|
|
101
|
+
for p in (repo, user):
|
|
102
|
+
if p.exists():
|
|
103
|
+
mb = p.stat().st_size / 1e6
|
|
104
|
+
return CheckResult("caudate", True, f"{p} ({mb:.1f} MB)")
|
|
105
|
+
return CheckResult(
|
|
106
|
+
"caudate", False, "no caudate.pt found",
|
|
107
|
+
fix="run `cognos init` (downloads weights from HuggingFace)",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def check_anthropic_key() -> CheckResult:
|
|
112
|
+
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
113
|
+
return CheckResult("anthropic_key", True, "ANTHROPIC_API_KEY set in env")
|
|
114
|
+
return CheckResult(
|
|
115
|
+
"anthropic_key", False, "ANTHROPIC_API_KEY not set",
|
|
116
|
+
fix="export ANTHROPIC_API_KEY=sk-ant-... (only needed if "
|
|
117
|
+
"system1 or system2 is an `anthropic/...` model)",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def check_settings() -> CheckResult:
|
|
122
|
+
p = settings_path()
|
|
123
|
+
if not p.exists():
|
|
124
|
+
return CheckResult(
|
|
125
|
+
"settings", False, f"{p} missing",
|
|
126
|
+
fix="run `cognos init`",
|
|
127
|
+
)
|
|
128
|
+
try:
|
|
129
|
+
data = json.loads(p.read_text())
|
|
130
|
+
s1 = data.get("system1") or "?"
|
|
131
|
+
s2 = data.get("system2") or "?"
|
|
132
|
+
return CheckResult("settings", True, f"system1={s1}, system2={s2}")
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return CheckResult(
|
|
135
|
+
"settings", False, f"{p} unreadable ({e})",
|
|
136
|
+
fix="delete the file and re-run `cognos init`",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def run_checks() -> list[CheckResult]:
|
|
141
|
+
return [
|
|
142
|
+
check_settings(),
|
|
143
|
+
check_ollama(),
|
|
144
|
+
check_caudate(),
|
|
145
|
+
check_anthropic_key(),
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------
|
|
150
|
+
# Doctor — read-only diagnostic
|
|
151
|
+
# ---------------------------------------------------------------------
|
|
152
|
+
def cmd_doctor(console: Console) -> int:
|
|
153
|
+
"""Print a status table + fix hints. Returns 0 if all green."""
|
|
154
|
+
results = run_checks()
|
|
155
|
+
table = Table(show_header=True, header_style="bold", expand=False)
|
|
156
|
+
table.add_column("check", style="cyan")
|
|
157
|
+
table.add_column("status")
|
|
158
|
+
table.add_column("detail", style="dim")
|
|
159
|
+
for r in results:
|
|
160
|
+
mark = "[green]✓[/green]" if r.ok else "[red]✗[/red]"
|
|
161
|
+
table.add_row(r.name, mark, r.detail)
|
|
162
|
+
console.print(Panel(table, title="cognos doctor", border_style="cyan"))
|
|
163
|
+
|
|
164
|
+
missing = [r for r in results if not r.ok]
|
|
165
|
+
if missing:
|
|
166
|
+
console.print()
|
|
167
|
+
console.print("[bold]fixes:[/bold]")
|
|
168
|
+
for r in missing:
|
|
169
|
+
if r.fix:
|
|
170
|
+
console.print(f" [yellow]{r.name}[/yellow]: {r.fix}")
|
|
171
|
+
return 1
|
|
172
|
+
console.print("[green]all checks passed[/green]")
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------
|
|
177
|
+
# Init — interactive wizard
|
|
178
|
+
# ---------------------------------------------------------------------
|
|
179
|
+
_MODEL_PRESETS = [
|
|
180
|
+
("local-only",
|
|
181
|
+
"ollama/glm-5.1:cloud", "ollama/qwen3.6:35b",
|
|
182
|
+
"fully local; both brains run on Ollama"),
|
|
183
|
+
("hybrid",
|
|
184
|
+
"ollama/glm-5.1:cloud", "anthropic/claude-haiku-4-5",
|
|
185
|
+
"local fast, hosted slow (recommended)"),
|
|
186
|
+
("hosted-only",
|
|
187
|
+
"anthropic/claude-haiku-4-5", "anthropic/claude-opus-4-7",
|
|
188
|
+
"no Ollama needed; requires ANTHROPIC_API_KEY"),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _download_caudate(console: Console, repo: str) -> bool:
|
|
193
|
+
"""Pull caudate.pt + caudate.meta.json from a HF repo into ~/.cognos/nn/.
|
|
194
|
+
|
|
195
|
+
Returns True on success. Best-effort: if `huggingface_hub` isn't
|
|
196
|
+
installed we fall back to printing the manual download URLs.
|
|
197
|
+
"""
|
|
198
|
+
target = caudate_dir()
|
|
199
|
+
try:
|
|
200
|
+
from huggingface_hub import hf_hub_download
|
|
201
|
+
except ImportError:
|
|
202
|
+
console.print(
|
|
203
|
+
f"[yellow]huggingface_hub not installed.[/yellow] Download "
|
|
204
|
+
f"these two files into {target}:"
|
|
205
|
+
)
|
|
206
|
+
for fname in _CAUDATE_FILES:
|
|
207
|
+
console.print(
|
|
208
|
+
f" https://huggingface.co/{repo}/resolve/main/{fname}"
|
|
209
|
+
)
|
|
210
|
+
return False
|
|
211
|
+
try:
|
|
212
|
+
for fname in _CAUDATE_FILES:
|
|
213
|
+
console.print(f" fetching [cyan]{fname}[/cyan] from {repo}…")
|
|
214
|
+
src = hf_hub_download(repo_id=repo, filename=fname)
|
|
215
|
+
shutil.copy2(src, target / fname)
|
|
216
|
+
console.print(f"[green]caudate weights ready at {target}[/green]")
|
|
217
|
+
return True
|
|
218
|
+
except Exception as e:
|
|
219
|
+
console.print(f"[red]download failed:[/red] {e}")
|
|
220
|
+
console.print(
|
|
221
|
+
f"manual fallback — get the files from "
|
|
222
|
+
f"https://huggingface.co/{repo} and drop them in {target}"
|
|
223
|
+
)
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def cmd_init(console: Console, *, force: bool = False) -> int:
|
|
228
|
+
"""Interactive first-run setup."""
|
|
229
|
+
console.print(Panel(
|
|
230
|
+
"[bold]cognos init[/bold] — first-run setup\n"
|
|
231
|
+
"Picks your fast/slow models, downloads Caudate, "
|
|
232
|
+
"writes ~/.cognos/settings.json",
|
|
233
|
+
border_style="cyan",
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
sp = settings_path()
|
|
237
|
+
if sp.exists() and not force:
|
|
238
|
+
if not Confirm.ask(
|
|
239
|
+
f"settings already exist at {sp} — overwrite?", default=False,
|
|
240
|
+
):
|
|
241
|
+
console.print("[dim]aborted — existing settings preserved[/dim]")
|
|
242
|
+
return 1
|
|
243
|
+
|
|
244
|
+
# --- model preset
|
|
245
|
+
console.print("\n[bold]model preset[/bold]")
|
|
246
|
+
table = Table(show_header=True, header_style="bold")
|
|
247
|
+
table.add_column("#", style="cyan")
|
|
248
|
+
table.add_column("name")
|
|
249
|
+
table.add_column("system1 (fast)")
|
|
250
|
+
table.add_column("system2 (slow)")
|
|
251
|
+
table.add_column("notes", style="dim")
|
|
252
|
+
for i, (name, s1, s2, note) in enumerate(_MODEL_PRESETS, 1):
|
|
253
|
+
table.add_row(str(i), name, s1, s2, note)
|
|
254
|
+
console.print(table)
|
|
255
|
+
choice = Prompt.ask(
|
|
256
|
+
"pick a preset (1-3) or [c]ustom", choices=["1", "2", "3", "c"], default="2",
|
|
257
|
+
)
|
|
258
|
+
if choice == "c":
|
|
259
|
+
s1 = Prompt.ask("system1 (fast model)", default="ollama/glm-5.1:cloud")
|
|
260
|
+
s2 = Prompt.ask("system2 (slow model)", default="anthropic/claude-haiku-4-5")
|
|
261
|
+
else:
|
|
262
|
+
_, s1, s2, _ = _MODEL_PRESETS[int(choice) - 1]
|
|
263
|
+
|
|
264
|
+
# --- caudate download
|
|
265
|
+
download = Confirm.ask(
|
|
266
|
+
"\ndownload caudate weights (~62 MB) from HuggingFace?", default=True,
|
|
267
|
+
)
|
|
268
|
+
if download:
|
|
269
|
+
repo = os.environ.get("COGNOS_CAUDATE_HF_REPO", _DEFAULT_HF_REPO)
|
|
270
|
+
_download_caudate(console, repo)
|
|
271
|
+
|
|
272
|
+
# --- write settings
|
|
273
|
+
settings: dict[str, object] = {"system1": s1, "system2": s2}
|
|
274
|
+
if sp.exists():
|
|
275
|
+
try:
|
|
276
|
+
existing = json.loads(sp.read_text())
|
|
277
|
+
existing.update(settings)
|
|
278
|
+
settings = existing
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
sp.write_text(json.dumps(settings, indent=2) + "\n")
|
|
282
|
+
console.print(f"\n[green]wrote {sp}[/green]")
|
|
283
|
+
|
|
284
|
+
# --- key reminder
|
|
285
|
+
if any("anthropic" in m for m in (s1, s2)) and not os.environ.get("ANTHROPIC_API_KEY"):
|
|
286
|
+
console.print(
|
|
287
|
+
"\n[yellow]heads up:[/yellow] you picked an anthropic model. "
|
|
288
|
+
"set [bold]ANTHROPIC_API_KEY[/bold] in your shell before running."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
console.print("\n[bold]done.[/bold] try `cognos doctor` to verify, then `cognos`.")
|
|
292
|
+
return 0
|