diff --git a/README.md b/README.md index 8fbf7ff9..e590ffda 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Other install methods: [pip install](#alternative-install-with-pip) | [uv instal ## 🔥🔥🔥 News (Pacific Time) -- June 16, 2026 (latest): **Internal modules drop the `cc_` prefix — `cc_config` / `cc_daemon` / `cc_kernel` / `cc_mcp` are now `config` / `daemon` / `kernel` / `mcp_client`.** A readability cleanup with no behavior change. The MCP client is named `mcp_client` (not bare `mcp`) to avoid shadowing Python's namespace and the `modelcontextprotocol` package. **Breaking only if you import these modules directly** — update your imports (`import cc_kernel` → `import kernel`, `from cc_mcp.client import …` → `from mcp_client.client import …`). Whole-word rename so unrelated tokens are untouched; two name-collision regressions caught and fixed; full suite green (**2449 passed, 3 skipped**). Details: [docs/news.md](docs/news.md). +- June 16, 2026 (latest): **All internal modules now live under a single `cheetahclaws` package — import as `cheetahclaws.config`, `cheetahclaws.kernel`, … instead of bare `config` / `kernel`.** The old flat top-level layout let generic names (`config`, `daemon`) get shadowed by other things on `sys.path` once the app was *installed* and launched from its entry point — another project's `config/` dir, the PyPI `python-daemon` package — which broke `cheetahclaws` at startup (`ImportError: … from 'config' (unknown location)`). Owning one `cheetahclaws.*` namespace removes that whole class of bug — a prerequisite for shipping as a broadly-installable app. **Breaking only if you import internals directly** (`import kernel` → `from cheetahclaws import kernel`). A built wheel ships everything under `cheetahclaws/` with no bare top-level modules; full suite green (**2449 passed, 3 skipped**). Details: [docs/news.md](docs/news.md). - June 6, 2026 (**v3.5.82**): **macOS install reliably puts `cheetahclaws` on PATH, and local Ollama models that emit tool calls as text now actually execute them** (two fixes from issue #131). **(1) Install/PATH on macOS:** the installer `source`s the dedicated venv it creates, which made the post-install `command -v cheetahclaws` check succeed *inside the script's own shell* — so it reported "on PATH" and **skipped the entire rc-file step**, leaving `~/.zshrc` untouched and the binary unreachable in new terminals. It now symlinks only the `cheetahclaws` entry point into `~/.local/bin` (pipx-style, so the venv's `python`/`pip` don't shadow yours), creates `~/.zshrc` / `.bash_profile` if missing, and appends `~/.local/bin` to PATH there — without trusting the venv-polluted `command -v` (`scripts/install.sh`). **(2) Ollama tool calls:** `stream_ollama` only read Ollama's structured `message.tool_calls` field, while the cloud path already recovers calls a model emits as **text**, so Qwen-coder / Gemma / Mistral over Ollama produced "tool-calling-style chat" that streamed as plain text and never ran — the model seemed to "just keep talking." `stream_ollama` now mirrors the cloud path's interceptor: it buffers from the first `` / `<|tool_call|>` / `[TOOL_CALLS]` marker (so raw markup never reaches the user) and parses it into real tool calls at end-of-stream (`providers.py`). Details: [docs/guides/usage.md](docs/guides/usage.md#usage-open-source-models-local) · [docs/guides/faq.md](docs/guides/faq.md) · [docs/news.md](docs/news.md). - June 5, 2026 (**v3.5.82**): **User-controllable token/cost budgets** — `/budget $5` / `/budget 200k` / `/budget daily $20` cap spend per session or per day, enforced before each model call; on hit the session auto-saves and you're shown how to `/resume` or raise the cap and continue (warns at ≥80%/95%; `--budget` sets it at startup). Details: [docs/guides/features.md](docs/guides/features.md) · [docs/news.md](docs/news.md). - June 5, 2026: **Adaptive Markdown streaming — live output stays correct on every device** by auto-selecting a per-device tier (`live` in-place redraw on capable terminals incl. modern SSH emulators, append-only `commit` for SSH/Apple Terminal/pipes/CJK text so frames never duplicate, `plain` fallback); also ships a visual `/context` usage grid and a 1M context window for `deepseek-v4-flash`. Details: [docs/guides/features.md](docs/guides/features.md) · [docs/news.md](docs/news.md). diff --git a/cheetahclaws/__init__.py b/cheetahclaws/__init__.py new file mode 100644 index 00000000..54d0f183 --- /dev/null +++ b/cheetahclaws/__init__.py @@ -0,0 +1,66 @@ +"""CheetahClaws — package root. + +Kept intentionally light. Importing ``cheetahclaws`` — or any submodule such +as ``cheetahclaws.config`` — must NOT trigger the CLI's heavy module-level +setup in :mod:`cheetahclaws.cli` (``.env`` loading, ``stdout``/``stderr`` +wrapping, the command-table build). The CLI entry symbols (``cmd_*``, +``info``, ``err`` …) are exposed lazily through :func:`__getattr__`, so +``from cheetahclaws import cmd_init`` keeps working without paying that cost +on every submodule import. + +Why a package at all: the modules used to live at the top level (``config``, +``daemon``, ``kernel`` …). Generic names like ``config`` / ``daemon`` collide +with other things on ``sys.path`` (another project's ``config/`` dir, the +``python-daemon`` package) once CheetahClaws is *installed* and launched from +its entry point rather than the repo dir. Owning a single ``cheetahclaws.*`` +namespace removes that entire class of import-shadowing bug. +""" +from __future__ import annotations + +from pathlib import Path + + +def _read_version() -> str: + """Resolve the version: installed metadata first, pyproject.toml fallback.""" + try: + from importlib.metadata import version as _v + return _v("cheetahclaws") + except Exception: + pass + try: + # pyproject.toml sits at the repo root, one level above this package. + _toml = Path(__file__).resolve().parent.parent / "pyproject.toml" + for _line in _toml.read_text(encoding="utf-8").splitlines(): + if _line.startswith("version"): + return _line.split("=", 1)[1].strip().strip('"').strip("'") + except Exception: + pass + return "0.0.0" + + +VERSION = __version__ = _read_version() + + +def __getattr__(name: str): + """Resolve ``cheetahclaws.`` lazily (PEP 562). + + Submodules (``config``, ``daemon``, ``kernel`` …) are imported directly — + crucially *without* touching :mod:`cheetahclaws.cli`, otherwise a + ``from cheetahclaws import config`` would drag in the heavy CLI module and + recurse through its own ``from cheetahclaws import `` lines. + Only names that are not submodules fall back to proxying a CLI entry symbol + (``cmd_init``, ``info``, ``err`` …). + """ + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + import importlib + try: + return importlib.import_module(f"{__name__}.{name}") + except ImportError: + pass + from . import cli + try: + return getattr(cli, name) + except AttributeError: + raise AttributeError( + f"module 'cheetahclaws' has no attribute {name!r}") from None diff --git a/cheetahclaws/__main__.py b/cheetahclaws/__main__.py new file mode 100644 index 00000000..0409fbea --- /dev/null +++ b/cheetahclaws/__main__.py @@ -0,0 +1,5 @@ +"""``python -m cheetahclaws`` entry point — delegates to the CLI.""" +from cheetahclaws.cli import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent.py b/cheetahclaws/agent.py similarity index 96% rename from agent.py rename to cheetahclaws/agent.py index 5ad49098..54d06600 100644 --- a/agent.py +++ b/cheetahclaws/agent.py @@ -7,15 +7,15 @@ from dataclasses import dataclass, field from typing import Generator -from tool_registry import get_tool_schemas -from tools import execute_tool -import tools as _tools_init # ensure built-in tools are registered on import -from providers import stream, AssistantTurn, TextChunk, ThinkingChunk, detect_provider, nim_next_model -from compaction import maybe_compact, estimate_tokens, get_context_limit, compact_messages, sanitize_history -import logging_utils as _log -import quota as _quota -from circuit_breaker import CircuitOpenError as _CircuitOpenError -import runtime +from cheetahclaws.tool_registry import get_tool_schemas +from cheetahclaws.tools import execute_tool +from cheetahclaws import tools as _tools_init # ensure built-in tools are registered on import +from cheetahclaws.providers import stream, AssistantTurn, TextChunk, ThinkingChunk, detect_provider, nim_next_model +from cheetahclaws.compaction import maybe_compact, estimate_tokens, get_context_limit, compact_messages, sanitize_history +from cheetahclaws import logging_utils as _log +from cheetahclaws import quota as _quota +from cheetahclaws.circuit_breaker import CircuitOpenError as _CircuitOpenError +from cheetahclaws import runtime # ── Re-export event types (used by cheetahclaws.py) ──────────────────────── __all__ = [ @@ -170,8 +170,8 @@ def run( if any(config.get(k) for k in ("session_token_budget", "session_cost_budget", "daily_token_budget", "daily_cost_budget")): try: - from compaction import estimate_tokens as _est_tok - from providers import calc_cost as _calc_cost + from cheetahclaws.compaction import estimate_tokens as _est_tok + from cheetahclaws.providers import calc_cost as _calc_cost _proj_tokens = (_est_tok(state.messages) + _est_tok([{"role": "system", "content": system_prompt}])) _proj_cost = _calc_cost(config["model"], _proj_tokens, 0) @@ -232,7 +232,7 @@ def run( return # circuit manages its own cooldown — don't retry except Exception as e: - from error_classifier import classify as _classify_err, ErrorCategory as _ErrCat + from cheetahclaws.error_classifier import classify as _classify_err, ErrorCategory as _ErrCat cerr = _classify_err(e) # NIM 429 cascade: swap to the next free-tier model before @@ -446,7 +446,7 @@ def run( # Determine which tools can run in parallel — but treat redundant # read-only calls as "sequential" (and short-circuit during exec) # so the dedup path always lands on a single, predictable code path. - from tool_registry import get_tool as _get_tool + from cheetahclaws.tool_registry import get_tool as _get_tool parallel_batch = [] sequential_batch = [] for tc in tool_calls: @@ -539,11 +539,11 @@ def _exec_one(tc): or _res_low.startswith("denied")) if not _is_err: try: - from multi_agent.fanout import ( + from cheetahclaws.multi_agent.fanout import ( should_fanout, fanout_summarize, make_llm_caller, fanout_notice, ) - from compaction import get_context_limit + from cheetahclaws.compaction import get_context_limit _ctx = get_context_limit(config.get("model", ""), config) if should_fanout(tc["name"], result, _ctx, config): # Find last user message for query focus @@ -650,7 +650,7 @@ def _check_permission(tc: dict, config: dict) -> bool: if name == "NotebookEdit": return False if name == "Bash": - from tools import _is_safe_bash + from cheetahclaws.tools import _is_safe_bash return _is_safe_bash(tc["input"].get("command", "")) return True # reads are fine @@ -658,7 +658,7 @@ def _check_permission(tc: dict, config: dict) -> bool: if name in ("Read", "Glob", "Grep", "WebFetch", "WebSearch"): return True if name == "Bash": - from tools import _is_safe_bash + from cheetahclaws.tools import _is_safe_bash return _is_safe_bash(tc["input"].get("command", "")) return False # Write, Edit → ask @@ -678,12 +678,12 @@ def _force_compact(state: AgentState, config: dict) -> bool: before = estimate_tokens(state.messages) if before <= 0: return False - from compaction import snip_old_tool_results + from cheetahclaws.compaction import snip_old_tool_results snip_old_tool_results(state.messages, max_chars=1000, preserve_last_n_turns=3) if estimate_tokens(state.messages) < limit * 0.9: return True state.messages = compact_messages(state.messages, config) - from compaction import _restore_plan_context + from cheetahclaws.compaction import _restore_plan_context state.messages.extend(_restore_plan_context(config)) after = estimate_tokens(state.messages) return after < before diff --git a/agent_runner.py b/cheetahclaws/agent_runner.py similarity index 98% rename from agent_runner.py rename to cheetahclaws/agent_runner.py index c985e24d..5602f4bc 100644 --- a/agent_runner.py +++ b/cheetahclaws/agent_runner.py @@ -24,7 +24,7 @@ from pathlib import Path from typing import Callable, Optional -import logging_utils as _log +from cheetahclaws import logging_utils as _log # ── Template resolution ──────────────────────────────────────────────────── @@ -122,7 +122,7 @@ def start_runner( # send_fn is ignored in subprocess mode for the skeleton — # ``notify`` IPC messages are dropped on the supervisor side # until F-6/7/8 wires bridge delivery in. - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor return runner_supervisor.start( name=name, template_name=template_name, @@ -161,7 +161,7 @@ def stop_runner(name: str) -> bool: return True # Subprocess mode (F-4): the handle lives in the daemon supervisor. try: - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor except Exception: return False return runner_supervisor.stop(name) @@ -175,7 +175,7 @@ def stop_all() -> int: r.stop() count = len(runners) try: - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor count += runner_supervisor.stop_all() except Exception: pass @@ -338,8 +338,8 @@ def _handle_permission_request(self, event) -> str: return "permission" def _run_loop(self) -> None: - from agent import AgentState, PermissionRequest, TurnDone - from agent import TextChunk, ToolStart, ToolEnd + from cheetahclaws.agent import AgentState, PermissionRequest, TurnDone + from cheetahclaws.agent import TextChunk, ToolStart, ToolEnd state = AgentState() config = self._config.copy() @@ -417,7 +417,7 @@ def _run_loop(self) -> None: # catches it internally and yields a quota text chunk — # remains unchanged). try: - import quota as _quota_mod + from cheetahclaws import quota as _quota_mod _quota_mod.check_quota( self._config.get("_session_id", "default"), self._config) except _quota_mod.QuotaExceeded as _qe: @@ -441,7 +441,7 @@ def _run_loop(self) -> None: err_msg = "" try: - for event in __import__("agent").run( + for event in __import__("cheetahclaws.agent", fromlist=["run"]).run( prompt, state, config, system_prompt ): if self._stop_event.is_set(): @@ -642,7 +642,7 @@ def _pipe_main(name_arg: Optional[str] = None) -> int: """Subprocess entry point. Returns the process exit code.""" import argparse import sys as _sys - from daemon.runner_ipc import IpcReadTimeout, JsonLineChannel + from cheetahclaws.daemon.runner_ipc import IpcReadTimeout, JsonLineChannel parser = argparse.ArgumentParser(prog="agent_runner") parser.add_argument("--pipe", action="store_true", @@ -700,8 +700,8 @@ def _pipe_main(name_arg: Optional[str] = None) -> int: # paths can never reach it. The test caller drives termination via # ``rs.stop()`` once it sees the iteration counter rise. if os.environ.get("CHEETAHCLAWS_E2E_FAKE_AGENT") == "1": - import agent as _agent_mod - from agent import ( + from cheetahclaws import agent as _agent_mod + from cheetahclaws.agent import ( TextChunk as _StubTextChunk, TurnDone as _StubTurnDone, PermissionRequest as _StubPermissionRequest, diff --git a/agent_templates/auto_bug_fixer.md b/cheetahclaws/agent_templates/auto_bug_fixer.md similarity index 100% rename from agent_templates/auto_bug_fixer.md rename to cheetahclaws/agent_templates/auto_bug_fixer.md diff --git a/agent_templates/auto_coder.md b/cheetahclaws/agent_templates/auto_coder.md similarity index 100% rename from agent_templates/auto_coder.md rename to cheetahclaws/agent_templates/auto_coder.md diff --git a/agent_templates/lab/analyst.md b/cheetahclaws/agent_templates/lab/analyst.md similarity index 100% rename from agent_templates/lab/analyst.md rename to cheetahclaws/agent_templates/lab/analyst.md diff --git a/agent_templates/lab/designer.md b/cheetahclaws/agent_templates/lab/designer.md similarity index 100% rename from agent_templates/lab/designer.md rename to cheetahclaws/agent_templates/lab/designer.md diff --git a/agent_templates/lab/engineer.md b/cheetahclaws/agent_templates/lab/engineer.md similarity index 100% rename from agent_templates/lab/engineer.md rename to cheetahclaws/agent_templates/lab/engineer.md diff --git a/agent_templates/lab/lay_reader.md b/cheetahclaws/agent_templates/lab/lay_reader.md similarity index 100% rename from agent_templates/lab/lay_reader.md rename to cheetahclaws/agent_templates/lab/lay_reader.md diff --git a/agent_templates/lab/pi.md b/cheetahclaws/agent_templates/lab/pi.md similarity index 100% rename from agent_templates/lab/pi.md rename to cheetahclaws/agent_templates/lab/pi.md diff --git a/agent_templates/lab/questioner.md b/cheetahclaws/agent_templates/lab/questioner.md similarity index 100% rename from agent_templates/lab/questioner.md rename to cheetahclaws/agent_templates/lab/questioner.md diff --git a/agent_templates/lab/reviewer.md b/cheetahclaws/agent_templates/lab/reviewer.md similarity index 100% rename from agent_templates/lab/reviewer.md rename to cheetahclaws/agent_templates/lab/reviewer.md diff --git a/agent_templates/lab/surveyor.md b/cheetahclaws/agent_templates/lab/surveyor.md similarity index 100% rename from agent_templates/lab/surveyor.md rename to cheetahclaws/agent_templates/lab/surveyor.md diff --git a/agent_templates/lab/writer.md b/cheetahclaws/agent_templates/lab/writer.md similarity index 100% rename from agent_templates/lab/writer.md rename to cheetahclaws/agent_templates/lab/writer.md diff --git a/agent_templates/paper_writer.md b/cheetahclaws/agent_templates/paper_writer.md similarity index 100% rename from agent_templates/paper_writer.md rename to cheetahclaws/agent_templates/paper_writer.md diff --git a/agent_templates/research_assistant.md b/cheetahclaws/agent_templates/research_assistant.md similarity index 100% rename from agent_templates/research_assistant.md rename to cheetahclaws/agent_templates/research_assistant.md diff --git a/auxiliary.py b/cheetahclaws/auxiliary.py similarity index 98% rename from auxiliary.py rename to cheetahclaws/auxiliary.py index 44cf0892..7f2111bd 100644 --- a/auxiliary.py +++ b/cheetahclaws/auxiliary.py @@ -11,7 +11,7 @@ import os from typing import Optional -import providers +from cheetahclaws import providers # ── Fast model candidates (checked in order) ───────────────────────────── # Each entry: (model_name, required_env_var_or_None) diff --git a/bootstrap.py b/cheetahclaws/bootstrap.py similarity index 93% rename from bootstrap.py rename to cheetahclaws/bootstrap.py index 6eae6787..8e2225ff 100644 --- a/bootstrap.py +++ b/cheetahclaws/bootstrap.py @@ -15,7 +15,7 @@ """ from __future__ import annotations -import logging_utils as _log +from cheetahclaws import logging_utils as _log _bootstrapped: bool = False @@ -36,14 +36,14 @@ def bootstrap(config: dict) -> None: # ── 2. Tool registry ─────────────────────────────────────────────────── # tools.py self-registers built-ins + extension tools on first import. # This import is the single explicit trigger; subsequent imports are no-ops. - import tools as _tools # noqa: F401 + from cheetahclaws import tools as _tools # noqa: F401 _log.debug("bootstrap_tools_ready") # ── 3. Health-check HTTP server ──────────────────────────────────────── port = config.get("health_check_port") if port: try: - from health import start_health_server + from cheetahclaws.health import start_health_server start_health_server(int(port), config) _log.info("health_server_started", port=int(port)) except Exception as exc: diff --git a/bridges/__init__.py b/cheetahclaws/bridges/__init__.py similarity index 100% rename from bridges/__init__.py rename to cheetahclaws/bridges/__init__.py diff --git a/bridges/draft_cache.py b/cheetahclaws/bridges/draft_cache.py similarity index 100% rename from bridges/draft_cache.py rename to cheetahclaws/bridges/draft_cache.py diff --git a/bridges/interactive_session.py b/cheetahclaws/bridges/interactive_session.py similarity index 99% rename from bridges/interactive_session.py rename to cheetahclaws/bridges/interactive_session.py index b398c48f..25385578 100644 --- a/bridges/interactive_session.py +++ b/cheetahclaws/bridges/interactive_session.py @@ -29,7 +29,7 @@ import time from typing import Callable -import logging_utils as _log +from cheetahclaws import logging_utils as _log # ── pyte: proper vt100 terminal emulator ───────────────────────────────── try: @@ -115,7 +115,7 @@ def __init__(self, cmd: str, send_fn: Callable[[str], None], if not isinstance(cmd, str) or "\x00" in cmd or len(cmd) > 4096: raise RuntimeError("Refused: command empty, too long, or contains NUL.") try: - from tools.shell import _bash_hard_denied + from cheetahclaws.tools.shell import _bash_hard_denied denied = _bash_hard_denied(cmd) except Exception: denied = None diff --git a/bridges/qq.py b/cheetahclaws/bridges/qq.py similarity index 98% rename from bridges/qq.py rename to cheetahclaws/bridges/qq.py index 4200f1c2..6b6c02f9 100644 --- a/bridges/qq.py +++ b/cheetahclaws/bridges/qq.py @@ -21,11 +21,11 @@ import threading import time as _time_mod -import jobs as _jobs -import logging_utils as _log -import runtime -from tools.interaction import _qq_thread_local -from ui.render import clr, err, info, ok, warn +from cheetahclaws import jobs as _jobs +from cheetahclaws import logging_utils as _log +from cheetahclaws import runtime +from cheetahclaws.tools.interaction import _qq_thread_local +from cheetahclaws.ui.render import clr, err, info, ok, warn _qq_thread: threading.Thread | None = None _qq_stop = threading.Event() @@ -538,7 +538,7 @@ async def _run_botpy(): sctx.qq_current_msg_type = msg_type # ── Interactive PTY session ──────────────────────────────── - from bridges.interactive_session import ( + from cheetahclaws.bridges.interactive_session import ( InteractiveSession, get_session, remove_session, @@ -636,7 +636,7 @@ def _qq_slash_runner(_slash_text, _tid, _mtype): sess_key = f"qq_{target_id}" if raw_cmd.lower() in ("stop", ""): - from bridges.terminal_runner import stop_terminal + from cheetahclaws.bridges.terminal_runner import stop_terminal killed = stop_terminal(sess_key) _qq_send( @@ -687,7 +687,7 @@ def _send(out): continue def _qq_terminal(cmd, tid, skey): - from bridges.terminal_runner import run_terminal + from cheetahclaws.bridges.terminal_runner import run_terminal _qq_send(tid, f"▶ {cmd}", config) run_terminal( @@ -935,7 +935,7 @@ def _on_chunk(chunk: str): _flush_chunks() def _on_tool_start(name: str, inputs: dict): - from ui.render import _tool_desc + from cheetahclaws.ui.render import _tool_desc desc = _tool_desc(name, inputs or {}) _jobs.add_step(job.id, name, desc[:80]) @@ -1115,8 +1115,8 @@ def cmd_qq(args: str, _state, config) -> bool: """ global _qq_thread, _qq_stop import os as _os - from config import save_config - from bridges import resolve_bridge_token, scrub_token_from_history + from cheetahclaws.config import save_config + from cheetahclaws.bridges import resolve_bridge_token, scrub_token_from_history parts = args.strip().split() diff --git a/bridges/slack.py b/cheetahclaws/bridges/slack.py similarity index 97% rename from bridges/slack.py rename to cheetahclaws/bridges/slack.py index 1955dac0..9d3dcaf6 100644 --- a/bridges/slack.py +++ b/cheetahclaws/bridges/slack.py @@ -15,10 +15,10 @@ import threading import time as _time_mod -from ui.render import clr, info, ok, warn, err -import runtime -import logging_utils as _log -import jobs as _jobs +from cheetahclaws.ui.render import clr, info, ok, warn, err +from cheetahclaws import runtime +from cheetahclaws import logging_utils as _log +from cheetahclaws import jobs as _jobs _slack_thread: threading.Thread | None = None _slack_stop = threading.Event() @@ -76,7 +76,7 @@ def _slack_send(token: str, channel: str, text: str) -> None: def _slack_poll_loop(token: str, channel: str, config: dict) -> str: """Returns "stopped", "auth_error", or raises on unexpected fatal error.""" - from tools import _slack_thread_local + from cheetahclaws.tools import _slack_thread_local session_ctx = runtime.get_session_ctx(config.get("_session_id", "default")) run_query_cb = session_ctx.run_query @@ -152,7 +152,7 @@ def _slack_poll_loop(token: str, channel: str, config: dict) -> str: continue # ── Interactive PTY session ──────────────────────────────── - from bridges.interactive_session import get_session, set_session, remove_session, InteractiveSession + from cheetahclaws.bridges.interactive_session import get_session, set_session, remove_session, InteractiveSession _sess_key = f"slack_{channel}" _active_sess = get_session(_sess_key) @@ -178,7 +178,7 @@ def _slack_poll_loop(token: str, channel: str, config: dict) -> str: def _sl_agent_ctrl(aargs, ch): def _send(msg): _slack_send(token, ch, msg) try: - from agent_runner import list_runners, stop_runner, stop_all, get_runner + from cheetahclaws.agent_runner import list_runners, stop_runner, stop_all, get_runner subcmd_parts = aargs.split(None, 1) subcmd = subcmd_parts[0].lower() if subcmd_parts else "list" rest = subcmd_parts[1] if len(subcmd_parts) > 1 else "" @@ -207,7 +207,7 @@ def _send(msg): _slack_send(token, ch, msg) if text.strip().startswith("!"): raw_cmd = text.strip()[1:].strip() if not raw_cmd or raw_cmd.lower() == "stop": - from bridges.terminal_runner import stop_terminal + from cheetahclaws.bridges.terminal_runner import stop_terminal killed = stop_terminal(_sess_key) _slack_send(token, channel, "🛑 Stopped." if killed else "ℹ Nothing running.") continue @@ -230,7 +230,7 @@ def _send(out): _slack_send(token, ch, out) daemon=True).start() continue def _slack_terminal(cmd, ch, skey): - from bridges.terminal_runner import run_terminal + from cheetahclaws.bridges.terminal_runner import run_terminal _slack_send(token, ch, f"▶ `{cmd}`") run_terminal(cmd, lambda out: _slack_send(token, ch, out), session_key=skey, stop_event=_slack_stop) @@ -321,13 +321,13 @@ def flush(self): sess_key = f"slack_{channel}" if raw_cmd.lower() in ("stop", ""): - from bridges.terminal_runner import stop_terminal + from cheetahclaws.bridges.terminal_runner import stop_terminal killed = stop_terminal(sess_key) _slack_send(token, channel, "🛑 Command stopped." if killed else "ℹ No command running.") continue def _slack_terminal(cmd, ch, skey): - from bridges.terminal_runner import run_terminal + from cheetahclaws.bridges.terminal_runner import run_terminal _slack_send(token, ch, f"▶ `{cmd}`") run_terminal(cmd, lambda out: _slack_send(token, ch, out), session_key=skey, stop_event=_slack_stop) @@ -615,8 +615,8 @@ def cmd_slack(args: str, _state, config) -> bool: """ global _slack_thread, _slack_stop import os as _os - from config import save_config - from bridges import resolve_bridge_token, scrub_token_from_history + from cheetahclaws.config import save_config + from cheetahclaws.bridges import resolve_bridge_token, scrub_token_from_history parts = args.strip().split() diff --git a/bridges/telegram.py b/cheetahclaws/bridges/telegram.py similarity index 97% rename from bridges/telegram.py rename to cheetahclaws/bridges/telegram.py index abd551f2..998927fd 100644 --- a/bridges/telegram.py +++ b/cheetahclaws/bridges/telegram.py @@ -13,10 +13,10 @@ import threading import time as _time_mod -from ui.render import clr, info, ok, warn, err -import runtime -import logging_utils as _log -import jobs as _jobs +from cheetahclaws.ui.render import clr, info, ok, warn, err +from cheetahclaws import runtime +from cheetahclaws import logging_utils as _log +from cheetahclaws import jobs as _jobs _telegram_thread: threading.Thread | None = None _telegram_stop = threading.Event() @@ -256,7 +256,7 @@ def _tg_poll_loop(token: str, chat_id: int, config: dict) -> str: "auth_error" — token rejected by Telegram (don't reconnect) Raises on unexpected fatal errors so the supervisor can reconnect. """ - from tools import _tg_thread_local + from cheetahclaws.tools import _tg_thread_local session_ctx = runtime.get_session_ctx(config.get("_session_id", "default")) run_query_cb = session_ctx.run_query # Flush old messages @@ -386,7 +386,7 @@ def _tg_poll_loop(token: str, chat_id: int, config: dict) -> str: size_kb = len(audio_bytes) / 1024 _tg_send(token, chat_id, f"🎙 Voice received ({duration}s, {size_kb:.0f} KB) — transcribing...") print(clr(f"\n 📩 Telegram: 🎙 voice ({duration}s, {size_kb:.0f} KB)", "cyan")) - from voice import transcribe_audio_file + from cheetahclaws.voice import transcribe_audio_file suffix = ".ogg" if msg.get("voice") else ".mp3" transcribed = transcribe_audio_file(audio_bytes, suffix=suffix) if transcribed: @@ -413,7 +413,7 @@ def _tg_poll_loop(token: str, chat_id: int, config: dict) -> str: continue # ── Interactive PTY session (e.g. !claude, !python, !bash) ─ - from bridges.interactive_session import get_session, set_session, remove_session, InteractiveSession + from cheetahclaws.bridges.interactive_session import get_session, set_session, remove_session, InteractiveSession _sess_key = f"tg_{chat_id}" _active_sess = get_session(_sess_key) @@ -459,7 +459,7 @@ def _send_file_async(path, t, cid): agent_args = text.strip()[6:].strip() def _agent_ctrl(aargs, chat_token, cid): try: - from agent_runner import list_runners, stop_runner, stop_all, get_runner + from cheetahclaws.agent_runner import list_runners, stop_runner, stop_all, get_runner subcmd_parts = aargs.split(None, 1) subcmd = subcmd_parts[0].lower() if subcmd_parts else "list" rest = subcmd_parts[1] if len(subcmd_parts) > 1 else "" @@ -505,7 +505,7 @@ def _agent_ctrl(aargs, chat_token, cid): if text.strip().startswith("!"): raw_cmd = text.strip()[1:].strip() if not raw_cmd or raw_cmd.lower() == "stop": - from bridges.terminal_runner import stop_terminal + from cheetahclaws.bridges.terminal_runner import stop_terminal killed = stop_terminal(_sess_key) _tg_send(token, chat_id, "🛑 Stopped." if killed else "ℹ Nothing running.") continue @@ -531,7 +531,7 @@ def _send(out): _tg_send(chat_token, cid, out) continue # Non-interactive command → run and stream output def _terminal_runner(cmd, chat_token, cid, skey): - from bridges.terminal_runner import run_terminal + from cheetahclaws.bridges.terminal_runner import run_terminal _tg_send(chat_token, cid, f"▶ `{cmd}`") run_terminal(cmd, lambda out: _tg_send(chat_token, cid, out), session_key=skey, stop_event=_telegram_stop) @@ -669,13 +669,13 @@ def flush(self): sess_key = f"tg_{chat_id}" if raw_cmd.lower() in ("stop", ""): - from bridges.terminal_runner import stop_terminal + from cheetahclaws.bridges.terminal_runner import stop_terminal killed = stop_terminal(sess_key) _tg_send(token, chat_id, "🛑 Command stopped." if killed else "ℹ No command running.") continue def _terminal_runner(cmd, chat_token, cid, skey): - from bridges.terminal_runner import run_terminal + from cheetahclaws.bridges.terminal_runner import run_terminal _tg_send(chat_token, cid, f"▶ `{cmd}`") run_terminal(cmd, lambda out: _tg_send(chat_token, cid, out), session_key=skey, stop_event=_telegram_stop) @@ -929,8 +929,8 @@ def cmd_telegram(args: str, _state, config) -> bool: /telegram status — show current status """ global _telegram_thread, _telegram_stop - from config import save_config - from bridges import resolve_bridge_token, scrub_token_from_history + from cheetahclaws.config import save_config + from cheetahclaws.bridges import resolve_bridge_token, scrub_token_from_history parts = args.strip().split() diff --git a/bridges/terminal_runner.py b/cheetahclaws/bridges/terminal_runner.py similarity index 96% rename from bridges/terminal_runner.py rename to cheetahclaws/bridges/terminal_runner.py index a197018d..a1680d8e 100644 --- a/bridges/terminal_runner.py +++ b/cheetahclaws/bridges/terminal_runner.py @@ -2,7 +2,7 @@ bridges/terminal_runner.py — Shell command execution with streaming output for bridges. Usage (from any bridge bg thread): - from bridges.terminal_runner import run_terminal, stop_terminal + from cheetahclaws.bridges.terminal_runner import run_terminal, stop_terminal When a user sends "!" from phone, the bridge calls run_terminal(). Stdout/stderr are streamed back to the phone via send_fn in chunks. @@ -17,7 +17,7 @@ import time from typing import Callable -import logging_utils as _log +from cheetahclaws import logging_utils as _log # ── Active process registry ──────────────────────────────────────────────── _active: dict[str, subprocess.Popen] = {} # session_key → Popen @@ -57,7 +57,7 @@ def run_terminal( send_fn("⚠ Refused: command empty, too long, or contains NUL.") return try: - from tools.shell import _bash_hard_denied + from cheetahclaws.tools.shell import _bash_hard_denied denied = _bash_hard_denied(cmd) except Exception: denied = None diff --git a/bridges/wechat.py b/cheetahclaws/bridges/wechat.py similarity index 97% rename from bridges/wechat.py rename to cheetahclaws/bridges/wechat.py index b242a1c3..73d88c3f 100644 --- a/bridges/wechat.py +++ b/cheetahclaws/bridges/wechat.py @@ -21,10 +21,10 @@ import struct as _struct_mod import secrets as _secrets_mod -from ui.render import clr, info, ok, warn, err -import runtime -import logging_utils as _log -import jobs as _jobs +from cheetahclaws.ui.render import clr, info, ok, warn, err +from cheetahclaws import runtime +from cheetahclaws import logging_utils as _log +from cheetahclaws import jobs as _jobs _wechat_thread: threading.Thread | None = None _wechat_stop = threading.Event() @@ -267,7 +267,7 @@ def _wx_typing_loop(user_id: str, stop_event: threading.Event, config: dict) -> def _wx_qr_login(config: dict, bot_type: str = _ILINK_DEFAULT_BOT_TYPE, timeout_seconds: int = 480) -> bool: - from config import save_config + from cheetahclaws.config import save_config import time as _time info("Fetching WeChat QR code from iLink...") @@ -355,8 +355,8 @@ def _wx_qr_login(config: dict, bot_type: str = _ILINK_DEFAULT_BOT_TYPE, def _wx_poll_loop(token: str, base_url: str, config: dict) -> str: """Returns "stopped", "auth_error", or raises on unexpected fatal error.""" - from tools import _wx_thread_local - from bridges import wechat_smart_reply as _sr + from cheetahclaws.tools import _wx_thread_local + from cheetahclaws.bridges import wechat_smart_reply as _sr session_ctx = runtime.get_session_ctx(config.get("_session_id", "default")) run_query_cb = session_ctx.run_query sync_buf = "" @@ -394,7 +394,7 @@ def _wx_poll_loop(token: str, base_url: str, config: dict) -> str: print(clr("\n ⚠ WeChat: session expired — re-authenticate with /wechat login", "yellow")) config.pop("wechat_token", None) config.pop("wechat_base_url", None) - from config import save_config + from cheetahclaws.config import save_config save_config(config) _log.warn("bridge_auth_error", bridge="wechat", ret=ret, errcode=errcode) session_ctx.wx_send = None @@ -425,7 +425,7 @@ def _wx_poll_loop(token: str, base_url: str, config: dict) -> str: and not from_uid.endswith("@chatroom")): config["wechat_self_uid"] = from_uid try: - from config import save_config + from cheetahclaws.config import save_config save_config(config) except Exception: pass @@ -506,7 +506,7 @@ def _wx_poll_loop(token: str, base_url: str, config: dict) -> str: # chosen line — no agent, no smart-reply panel. One-shot. _stripped = text.strip() if _stripped.isdigit() and 1 <= int(_stripped) <= 9: - from bridges.draft_cache import take as _draft_take + from cheetahclaws.bridges.draft_cache import take as _draft_take _picked = _draft_take(from_uid, int(_stripped)) if _picked is not None: _wx_send(from_uid, _picked, config) @@ -514,7 +514,7 @@ def _wx_poll_loop(token: str, base_url: str, config: dict) -> str: continue # ── Interactive PTY session ──────────────────────────────── - from bridges.interactive_session import get_session, set_session, remove_session, InteractiveSession + from cheetahclaws.bridges.interactive_session import get_session, set_session, remove_session, InteractiveSession _sess_key = f"wx_{from_uid}" _active_sess = get_session(_sess_key) @@ -540,7 +540,7 @@ def _wx_poll_loop(token: str, base_url: str, config: dict) -> str: def _wx_agent_ctrl(aargs, uid): def _send(msg): _wx_send(uid, msg, config) try: - from agent_runner import list_runners, stop_runner, stop_all, get_runner + from cheetahclaws.agent_runner import list_runners, stop_runner, stop_all, get_runner subcmd_parts = aargs.split(None, 1) subcmd = subcmd_parts[0].lower() if subcmd_parts else "list" rest = subcmd_parts[1] if len(subcmd_parts) > 1 else "" @@ -569,7 +569,7 @@ def _send(msg): _wx_send(uid, msg, config) if text.strip().startswith("!"): raw_cmd = text.strip()[1:].strip() if not raw_cmd or raw_cmd.lower() == "stop": - from bridges.terminal_runner import stop_terminal + from cheetahclaws.bridges.terminal_runner import stop_terminal killed = stop_terminal(_sess_key) _wx_send(from_uid, "🛑 Stopped." if killed else "ℹ Nothing running.", config) continue @@ -593,7 +593,7 @@ def _send(out): _wx_send(uid, out, config) daemon=True).start() continue def _wx_terminal(cmd, uid, skey): - from bridges.terminal_runner import run_terminal + from cheetahclaws.bridges.terminal_runner import run_terminal _wx_send(uid, f"▶ {cmd}", config) run_terminal(cmd, lambda out: _wx_send(uid, out, config), session_key=skey, stop_event=_wechat_stop) @@ -684,13 +684,13 @@ def flush(self): sess_key = f"wx_{from_uid}" if raw_cmd.lower() in ("stop", ""): - from bridges.terminal_runner import stop_terminal + from cheetahclaws.bridges.terminal_runner import stop_terminal killed = stop_terminal(sess_key) _wx_send(from_uid, "🛑 Command stopped." if killed else "ℹ No command running.", config) continue def _wx_terminal(cmd, uid, skey): - from bridges.terminal_runner import run_terminal + from cheetahclaws.bridges.terminal_runner import run_terminal _wx_send(uid, f"▶ {cmd}", config) run_terminal(cmd, lambda out: _wx_send(uid, out, config), session_key=skey, stop_event=_wechat_stop) @@ -1022,7 +1022,7 @@ def cmd_wechat(args: str, _state, config) -> bool: /wechat logout — clear saved credentials """ global _wechat_thread, _wechat_stop - from config import save_config + from cheetahclaws.config import save_config sub = args.strip().split()[0].lower() if args.strip() else "" diff --git a/bridges/wechat_smart_reply.py b/cheetahclaws/bridges/wechat_smart_reply.py similarity index 99% rename from bridges/wechat_smart_reply.py rename to cheetahclaws/bridges/wechat_smart_reply.py index 46a1a1fc..8bd3fd00 100644 --- a/bridges/wechat_smart_reply.py +++ b/cheetahclaws/bridges/wechat_smart_reply.py @@ -337,8 +337,8 @@ def generate_candidates( if _stream_fn is None: try: - import providers - from auxiliary import get_auxiliary_model + from cheetahclaws import providers + from cheetahclaws.auxiliary import get_auxiliary_model except Exception: return [] _stream_fn = providers.stream diff --git a/bridges/wechat_smart_reply_store.py b/cheetahclaws/bridges/wechat_smart_reply_store.py similarity index 100% rename from bridges/wechat_smart_reply_store.py rename to cheetahclaws/bridges/wechat_smart_reply_store.py diff --git a/checkpoint/__init__.py b/cheetahclaws/checkpoint/__init__.py similarity index 100% rename from checkpoint/__init__.py rename to cheetahclaws/checkpoint/__init__.py diff --git a/checkpoint/hooks.py b/cheetahclaws/checkpoint/hooks.py similarity index 98% rename from checkpoint/hooks.py rename to cheetahclaws/checkpoint/hooks.py index 392719c3..d0bcefc9 100644 --- a/checkpoint/hooks.py +++ b/cheetahclaws/checkpoint/hooks.py @@ -54,7 +54,7 @@ def install_hooks() -> None: return _hooks_installed = True - from tool_registry import get_tool + from cheetahclaws.tool_registry import get_tool # Hook Write write_tool = get_tool("Write") diff --git a/checkpoint/store.py b/cheetahclaws/checkpoint/store.py similarity index 100% rename from checkpoint/store.py rename to cheetahclaws/checkpoint/store.py diff --git a/checkpoint/types.py b/cheetahclaws/checkpoint/types.py similarity index 100% rename from checkpoint/types.py rename to cheetahclaws/checkpoint/types.py diff --git a/circuit_breaker.py b/cheetahclaws/circuit_breaker.py similarity index 97% rename from circuit_breaker.py rename to cheetahclaws/circuit_breaker.py index 38e0f8a3..3f559d4c 100644 --- a/circuit_breaker.py +++ b/cheetahclaws/circuit_breaker.py @@ -72,12 +72,12 @@ def record_success(self) -> None: self._failure_times.clear() self._opened_at = None if was_open: - import logging_utils as _log + from cheetahclaws import logging_utils as _log _log.info("circuit_closed", provider=self.provider) def record_failure(self) -> None: """Call after a provider exception.""" - import logging_utils as _log + from cheetahclaws import logging_utils as _log with self._lock: now = time.monotonic() self._failure_times.append(now) diff --git a/cheetahclaws.py b/cheetahclaws/cli.py similarity index 96% rename from cheetahclaws.py rename to cheetahclaws/cli.py index 80bdee00..85296cdd 100755 --- a/cheetahclaws.py +++ b/cheetahclaws/cli.py @@ -199,7 +199,7 @@ def __getattr__(self, name): # ── UI / rendering ───────────────────────────────────────────────────────── -from ui.render import ( +from cheetahclaws.ui.render import ( C, clr, info, ok, warn, err, _truncate_err_global, render_diff, _has_diff, stream_text, stream_thinking, flush_response, @@ -212,63 +212,63 @@ def __getattr__(self, name): ) # ── Input layer (prompt_toolkit with readline fallback) ────────────────── -import ui.input as _ui_input +import cheetahclaws.ui.input as _ui_input _pt_read_line = _ui_input.read_line HAS_PROMPT_TOOLKIT = _ui_input.HAS_PROMPT_TOOLKIT # ── Bridge commands ──────────────────────────────────────────────────────── -import bridges.telegram as _btg -import bridges.wechat as _bwx -import bridges.slack as _bslk -import bridges.qq as _bqq -from bridges.telegram import cmd_telegram, _tg_send -from bridges.wechat import cmd_wechat, _wx_start_bridge -from bridges.slack import cmd_slack, _slack_start_bridge -from bridges.qq import cmd_qq, _qq_start_bridge, _qq_send +import cheetahclaws.bridges.telegram as _btg +import cheetahclaws.bridges.wechat as _bwx +import cheetahclaws.bridges.slack as _bslk +import cheetahclaws.bridges.qq as _bqq +from cheetahclaws.bridges.telegram import cmd_telegram, _tg_send +from cheetahclaws.bridges.wechat import cmd_wechat, _wx_start_bridge +from cheetahclaws.bridges.slack import cmd_slack, _slack_start_bridge +from cheetahclaws.bridges.qq import cmd_qq, _qq_start_bridge, _qq_send # ── Session commands ─────────────────────────────────────────────────────── -from commands.session import ( +from cheetahclaws.commands.session import ( cmd_save, cmd_load, cmd_resume, cmd_history, cmd_search, cmd_cloudsave, cmd_exit, save_latest, ) # ── Config commands ──────────────────────────────────────────────────────── -from commands.config_cmd import ( +from cheetahclaws.commands.config_cmd import ( cmd_model, cmd_config, cmd_verbose, cmd_thinking, cmd_quiet, cmd_permissions, cmd_cwd, _interactive_ollama_picker, ) # ── Core commands ────────────────────────────────────────────────────────── -from commands.core import ( +from cheetahclaws.commands.core import ( cmd_help, cmd_clear, cmd_context, cmd_cost, cmd_budget, cmd_compact, cmd_init, cmd_export, cmd_copy, cmd_status, cmd_doctor, cmd_proactive, cmd_image, cmd_circuit, cmd_web, run_setup_wizard, ) # ── Checkpoint / Plan commands ───────────────────────────────────────────── -from commands.checkpoint_plan import cmd_checkpoint, cmd_rewind, cmd_plan +from cheetahclaws.commands.checkpoint_plan import cmd_checkpoint, cmd_rewind, cmd_plan # ── Advanced commands ────────────────────────────────────────────────────── -from commands.advanced import ( +from cheetahclaws.commands.advanced import ( cmd_brainstorm, cmd_worker, cmd_ssj, cmd_draft, cmd_summarize, cmd_memory, cmd_agents, cmd_skills, cmd_mcp, cmd_plugin, cmd_tasks, _save_synthesis, _print_background_notifications, ) # ── Agent (autonomous loop) command ─────────────────────────────────────── -from commands.agent_cmd import cmd_agent +from cheetahclaws.commands.agent_cmd import cmd_agent # ── Monitor / Subscribe commands ────────────────────────────────────────── -from commands.monitor_cmd import cmd_subscribe, cmd_subscriptions, cmd_unsubscribe, cmd_monitor +from cheetahclaws.commands.monitor_cmd import cmd_subscribe, cmd_subscriptions, cmd_unsubscribe, cmd_monitor -from commands.research_cmd import cmd_research, cmd_reports -from commands.lab_cmd import cmd_lab +from cheetahclaws.commands.research_cmd import cmd_research, cmd_reports +from cheetahclaws.commands.lab_cmd import cmd_lab # ── Theme command ────────────────────────────────────────────────────────── -from commands.theme_cmd import cmd_theme +from cheetahclaws.commands.theme_cmd import cmd_theme # ── Tools / thread-local bridge state ───────────────────────────────────── -from tools import ( +from cheetahclaws.tools import ( ask_input_interactive, _tg_thread_local, _is_in_tg_turn, _wx_thread_local, _is_in_wx_turn, @@ -277,12 +277,13 @@ def __getattr__(self, name): ) # ── Live session context (replaces config["_run_query_callback"] etc.) ───── -import runtime +from cheetahclaws import runtime def _read_version() -> str: """Read version from pyproject.toml (single source of truth).""" try: - _toml = Path(__file__).resolve().parent / "pyproject.toml" + # pyproject.toml lives at the repo root, one level above this package. + _toml = Path(__file__).resolve().parent.parent / "pyproject.toml" for _line in _toml.read_text(encoding="utf-8").splitlines(): if _line.startswith("version"): return _line.split("=", 1)[1].strip().strip('"').strip("'") @@ -301,7 +302,7 @@ def _read_version() -> str: # Commands from modular/ are merged into COMMANDS after the dict is built. # Each module is optional — missing modules degrade gracefully. try: - from modular import load_all_commands as _modular_load_commands + from cheetahclaws.modular import load_all_commands as _modular_load_commands _MODULAR_AVAILABLE = True except ImportError: _MODULAR_AVAILABLE = False @@ -312,7 +313,7 @@ def _modular_has(cmd_name: str) -> bool: if not _MODULAR_AVAILABLE: return False try: - import modular + from cheetahclaws import modular cmds = modular.load_all_commands() return cmd_name in cmds except Exception: @@ -396,7 +397,7 @@ def _proactive_foreign_daemon_running() -> bool: """ try: import os - from daemon import discovery + from cheetahclaws.daemon import discovery info_d = discovery.locate() if info_d is None: return False @@ -436,7 +437,7 @@ def _proactive_watcher_loop(config): "continue and complete that work now. " "Otherwise, check if you have any pending tasks to execute or simply say 'No pending tasks'.") except Exception as e: - import logging_utils as _log + from cheetahclaws import logging_utils as _log _log.error("proactive_watcher_error", error=str(e)[:200]) @@ -542,7 +543,7 @@ def _load_external_commands_into(commands_dict: dict) -> None: # 2. user-installed plugins (via /plugin install) try: - from plugin.loader import load_plugin_commands + from cheetahclaws.plugin.loader import load_plugin_commands for cmd_name, cmd_def in load_plugin_commands().items(): if cmd_name not in commands_dict and callable(cmd_def.get("func")): commands_dict[cmd_name] = cmd_def["func"] @@ -567,7 +568,7 @@ def __getattr__(name: str): return _missing_module_cmd("voice") if name == "_voice_language": try: - import modular.voice.cmd as _vc + import cheetahclaws.modular.voice.cmd as _vc return _vc._voice_language except Exception: return "auto" @@ -592,7 +593,7 @@ def handle_slash(line: str, state, config) -> Union[bool, tuple]: return True # Fall through to skill lookup - from skill import find_skill + from cheetahclaws.skill import find_skill skill = find_skill(line) if skill: cmd_parts = line.strip().split(maxsplit=1) @@ -807,12 +808,12 @@ def _start_headless_bridges(config: dict) -> None: and not (config.get("qq_appid") and config.get("qq_secret")): return # nothing configured — no-op - import runtime as _runtime - from agent import ( + from cheetahclaws import runtime as _runtime + from cheetahclaws.agent import ( AgentState, run as _agent_run, TextChunk, ToolStart, ToolEnd, PermissionRequest, ) - from context import build_system_prompt + from cheetahclaws.context import build_system_prompt state = AgentState(messages=[], total_input_tokens=0, total_output_tokens=0) session_ctx = _runtime.get_session_ctx(config.get("_session_id", "default")) @@ -896,9 +897,9 @@ def _headless_run_query(prompt: str, is_background: bool = False) -> None: # ── Main REPL ────────────────────────────────────────────────────────────── def repl(config: dict, initial_prompt: str = None): - from config import HISTORY_FILE - from context import build_system_prompt - from agent import AgentState, run, TextChunk, ThinkingChunk, ToolStart, ToolEnd, TurnDone, PermissionRequest, QuotaPause + from cheetahclaws.config import HISTORY_FILE + from cheetahclaws.context import build_system_prompt + from cheetahclaws.agent import AgentState, run, TextChunk, ThinkingChunk, ToolStart, ToolEnd, TurnDone, PermissionRequest, QuotaPause if HAS_PROMPT_TOOLKIT: # Inject live providers so ui.input's completer enumerates the same @@ -918,7 +919,7 @@ def repl(config: dict, initial_prompt: str = None): # Create the per-session RuntimeContext early so all wiring uses it, not # the global singleton. session_id must be set in config before any # bridge or tool code runs so they can look up the right context. - import checkpoint as ckpt + from cheetahclaws import checkpoint as ckpt session_id = uuid.uuid4().hex[:8] config["_session_id"] = session_id session_ctx = runtime.get_session_ctx(session_id) @@ -932,7 +933,7 @@ def repl(config: dict, initial_prompt: str = None): # Banner if not initial_prompt: - from providers import detect_provider + from cheetahclaws.providers import detect_provider # ── Cheetah startup animation ── _CHEETAH_FRAMES = [ @@ -1268,14 +1269,14 @@ def run_query(user_input: str, is_background: bool = False): print(clr(f" ⛔ Budget reached — {event.reason}", "yellow", "bold")) # save_latest() prints the saved paths itself — don't echo. try: - from commands.session import save_latest + from cheetahclaws.commands.session import save_latest save_latest("", state, config) except Exception: pass # Suggest raising the cap that actually broke, in its own # unit/scope — a token cap can't be lifted with a $ amount. try: - import quota as _q + from cheetahclaws import quota as _q _pre = "daily " if event.scope == "daily" else "" _amt = _q.fmt_amount((event.limit or 0) * 2, event.unit or "tok") _raise_cmd = f"/budget {_pre}{_amt}" if event.limit else "/budget 40k" @@ -1297,7 +1298,7 @@ def run_query(user_input: str, is_background: bool = False): import urllib.error # Catch 404 Not Found (Ollama model missing) if isinstance(e, urllib.error.HTTPError) and e.code == 404: - from providers import detect_provider + from cheetahclaws.providers import detect_provider if detect_provider(config["model"]) == "ollama": err(f"Ollama model '{config['model']}' not found.") if _interactive_ollama_picker(config): @@ -1306,7 +1307,7 @@ def run_query(user_input: str, is_background: bool = False): return run_query(user_input, is_background) return # ── Actionable error messages via error classifier ──────── - from error_classifier import classify as _classify_err + from cheetahclaws.error_classifier import classify as _classify_err cerr = _classify_err(e) err(f"Error: {type(e).__name__}: {_truncate_err_global(str(e))}") if cerr.hint: @@ -1325,7 +1326,7 @@ def run_query(user_input: str, is_background: bool = False): # stop arrives. Skipped when this turn already hit the cap. if not quota_paused: try: - import quota as _quota + from cheetahclaws import quota as _quota for _level, _msg in _quota.warnings(config.get("_session_id", "default"), config): (err if _level == "crit" else warn)(f" ⚠ Budget: {_msg} — /budget to view") except Exception: @@ -1557,7 +1558,7 @@ def _read_input(prompt: str) -> str: # Context usage indicator in prompt ctx_hint = "" try: - from compaction import estimate_tokens, get_context_limit + from cheetahclaws.compaction import estimate_tokens, get_context_limit used = estimate_tokens(state.messages) limit = get_context_limit(config.get("model", ""), config) pct = int(used / limit * 100) if limit else 0 @@ -1889,7 +1890,7 @@ def _spin_and_query(phrase, prompt): skill, skill_args = result info(f"Running skill: {skill.name}" + (f" [{skill.context}]" if skill.context == "fork" else "")) try: - from skill import substitute_arguments + from cheetahclaws.skill import substitute_arguments rendered = substitute_arguments(skill.prompt, skill_args, skill.arguments) run_query(f"[Skill: {skill.name}]\n\n{rendered}") except KeyboardInterrupt: @@ -1918,24 +1919,24 @@ def main(): # rotate-token). See docs/RFC/0001-daemon-design-note.md and # docs/RFC/0002-daemon-foundation-roadmap.md. if len(sys.argv) >= 2 and sys.argv[1] == "serve": - from daemon.cli import serve_main as _serve_main + from cheetahclaws.daemon.cli import serve_main as _serve_main sys.exit(_serve_main(sys.argv[2:])) if len(sys.argv) >= 2 and sys.argv[1] == "daemon": - from commands.daemon_cmd import dispatch as _daemon_dispatch + from cheetahclaws.commands.daemon_cmd import dispatch as _daemon_dispatch sys.exit(_daemon_dispatch(sys.argv[2:])) # Read-only kernel inspection (RFC 0003+ surface). Talks to a # running `cheetahclaws serve --enable-kernel` daemon over the # existing daemon RPC channel; gracefully reports "not running" # when the daemon is absent. if len(sys.argv) >= 2 and sys.argv[1] == "kernel": - from kernel.cli import dispatch as _kernel_dispatch + from cheetahclaws.kernel.cli import dispatch as _kernel_dispatch sys.exit(_kernel_dispatch(sys.argv[2:])) # Backward-compat alias for the spike's `cheetahclaws spike-daemon ...` # surface (referenced in docs/RFC/0001-spike-notes.md). Routes through # the same paths as `serve` / `daemon ` so spike-notes commands # keep working unchanged. if len(sys.argv) >= 2 and sys.argv[1] == "spike-daemon": - from daemon.cli import main as _legacy_main + from cheetahclaws.daemon.cli import main as _legacy_main sys.exit(_legacy_main(sys.argv[2:])) parser = argparse.ArgumentParser( @@ -1985,44 +1986,44 @@ def main(): sys.exit(0) if args.web: - from config import load_config as _load_cfg, save_config as _save_cfg + from cheetahclaws.config import load_config as _load_cfg, save_config as _save_cfg _cfg = _load_cfg() # --model needs to persist: web request handlers reload config from # disk per request, so an in-memory override would be ignored. if args.model: m = args.model if "/" not in m and ":" in m: - from providers import PROVIDERS as _PROVIDERS + from cheetahclaws.providers import PROVIDERS as _PROVIDERS left, _ = m.split(":", 1) if left in _PROVIDERS: m = m.replace(":", "/", 1) _cfg["model"] = m _save_cfg(_cfg) - from bootstrap import bootstrap as _bootstrap + from cheetahclaws.bootstrap import bootstrap as _bootstrap _bootstrap(_cfg) # Auto-start configured Telegram/WeChat/Slack bridges in the same # process as the web server so a headless server deployment (Docker, # systemd) gets both channels with one command. _start_headless_bridges(_cfg) - from web.server import start_web_server + from cheetahclaws.web.server import start_web_server start_web_server(port=args.port, host=args.host, no_auth=args.no_auth) sys.exit(0) - from config import load_config, save_config, has_api_key - from providers import detect_provider, PROVIDERS + from cheetahclaws.config import load_config, save_config, has_api_key + from cheetahclaws.providers import detect_provider, PROVIDERS config = load_config() # Apply persisted console theme (if any) before any output is rendered. try: - from ui.render import apply_theme as _apply_theme + from cheetahclaws.ui.render import apply_theme as _apply_theme _apply_theme(config.get("theme", "default")) except Exception: pass # Explicit bootstrap: configure logging, ensure tool registry is ready, # and start the optional health-check server. - from bootstrap import bootstrap as _bootstrap + from cheetahclaws.bootstrap import bootstrap as _bootstrap _bootstrap(config) # Apply CLI overrides first (so key check uses the right provider) @@ -2030,7 +2031,7 @@ def main(): m = args.model # Convert "provider:model" → "provider/model" only when left side is a known provider if "/" not in m and ":" in m: - from providers import PROVIDERS + from cheetahclaws.providers import PROVIDERS left, _ = m.split(":", 1) if left in PROVIDERS: m = m.replace(":", "/", 1) @@ -2044,7 +2045,7 @@ def main(): if args.thinking: config["thinking"] = True if getattr(args, "budget", None): - import quota as _quota + from cheetahclaws import quota as _quota try: _kind, _val = _quota.parse_budget(args.budget) config[_quota.BUDGET_KEYS[(_kind, "session")]] = _val @@ -2054,7 +2055,7 @@ def main(): warn(f"--budget: {_e} (e.g. --budget $5 or --budget 200k); ignoring.") # ── Setup wizard: --setup flag or first-run auto-trigger ───────────── - from config import CONFIG_FILE + from cheetahclaws.config import CONFIG_FILE is_first_run = not CONFIG_FILE.exists() or os.path.getsize(CONFIG_FILE) < 5 if args.setup or (is_first_run and sys.stdin.isatty() and not args.print_mode): run_setup_wizard(config) diff --git a/cloudsave.py b/cheetahclaws/cloudsave.py similarity index 100% rename from cloudsave.py rename to cheetahclaws/cloudsave.py diff --git a/commands/__init__.py b/cheetahclaws/commands/__init__.py similarity index 100% rename from commands/__init__.py rename to cheetahclaws/commands/__init__.py diff --git a/commands/advanced.py b/cheetahclaws/commands/advanced.py similarity index 98% rename from commands/advanced.py rename to cheetahclaws/commands/advanced.py index ba508790..433d06c4 100644 --- a/commands/advanced.py +++ b/cheetahclaws/commands/advanced.py @@ -11,11 +11,11 @@ from pathlib import Path from typing import Union -from ui.render import ( +from cheetahclaws.ui.render import ( clr, info, ok, warn, err, _start_tool_spinner, _stop_tool_spinner, ) -from tools import _is_in_tg_turn, _is_in_web_turn +from cheetahclaws.tools import _is_in_tg_turn, _is_in_web_turn # ── Brainstorm ───────────────────────────────────────────────────────────── @@ -165,7 +165,7 @@ def _fetch_grounding(topic: str, top_n: int, config: dict) -> str: same topic are basically free. """ try: - from research.aggregator import research as _research + from cheetahclaws.research.aggregator import research as _research except Exception as e: warn(f" Grounding skipped — research module unavailable: {e}") return "" @@ -493,7 +493,7 @@ def _llm_oneshot(model: str, system: str, user: str, config: dict, max_chunks: i silent (return ""); callers fall back to skipping the stage so a flaky lead model never breaks the brainstorm flow. """ - from providers import stream, TextChunk + from cheetahclaws.providers import stream, TextChunk internal = config.copy() internal["no_tools"] = True chunks: list[str] = [] @@ -771,7 +771,7 @@ def _parse_models_flag(args: str) -> tuple[list[str], str]: def _generate_personas(topic: str, curr_model: str, config: dict, count: int = 5) -> dict | None: - from providers import stream, TextChunk + from cheetahclaws.providers import stream, TextChunk import json example_entries = "\n".join( @@ -813,8 +813,8 @@ def _generate_personas(topic: str, curr_model: str, config: dict, count: int = 5 def cmd_brainstorm(args: str, state, config) -> bool: """Run a multi-persona iterative brainstorming session on the project.""" - from providers import stream, TextChunk - from tools import ask_input_interactive + from cheetahclaws.providers import stream, TextChunk + from cheetahclaws.tools import ask_input_interactive # ── /brainstorm status — list active background brainstorms and exit. ─ if args.strip().lower() == "status": @@ -1493,7 +1493,7 @@ def cmd_draft(args: str, _state, config) -> bool: if rest.strip(): key = head[1:] try: - from bridges.wechat_smart_reply import ContactsStore + from cheetahclaws.bridges.wechat_smart_reply import ContactsStore store = ContactsStore() c = store.get(key) if c is None: @@ -1518,7 +1518,7 @@ def cmd_draft(args: str, _state, config) -> bool: history = [] try: - from bridges.wechat_smart_reply_store import make_store + from cheetahclaws.bridges.wechat_smart_reply_store import make_store store = make_store(timeout_s=300) if hasattr(store, "recent_replies"): # Mirror the smart-reply path: exclude this contact so we don't @@ -1532,7 +1532,7 @@ def cmd_draft(args: str, _state, config) -> bool: _start_tool_spinner() try: - from bridges.wechat_smart_reply import generate_candidates + from cheetahclaws.bridges.wechat_smart_reply import generate_candidates candidates = generate_candidates( raw, sender_label, config, contact=contact, history=history, @@ -1556,7 +1556,7 @@ def cmd_draft(args: str, _state, config) -> bool: # they can see and copy them on their phone. Best-effort: failures # never break the terminal output. try: - import runtime + from cheetahclaws import runtime ctx = runtime.get_ctx(config) bridge_text = ( f"💬 Drafts for 「{msg_preview}」\n\n" @@ -1565,8 +1565,8 @@ def cmd_draft(args: str, _state, config) -> bool: ) wx_uid = getattr(ctx, "wx_current_user_id", "") or "" if wx_uid: - from bridges.wechat import _wx_send - from bridges.draft_cache import put as _draft_put + from cheetahclaws.bridges.wechat import _wx_send + from cheetahclaws.bridges.draft_cache import put as _draft_put # Stash so a follow-up "1"/"2"/"3" from this uid resolves to # the chosen candidate (handled in bridges/wechat.py inbound). _draft_put(wx_uid, candidates) @@ -1575,14 +1575,14 @@ def cmd_draft(args: str, _state, config) -> bool: chat_id = config.get("telegram_chat_id") token = config.get("telegram_token") if chat_id and token: - from bridges.telegram import _tg_send + from cheetahclaws.bridges.telegram import _tg_send _tg_send(token, chat_id, bridge_text) slack_chan = getattr(ctx, "slack_current_channel", "") or "" if slack_chan: slack_token = config.get("slack_bot_token") or config.get("slack_token") if slack_token: try: - from bridges.slack import _slack_send + from cheetahclaws.bridges.slack import _slack_send _slack_send(slack_token, slack_chan, bridge_text) except Exception: pass @@ -1706,7 +1706,7 @@ def cmd_worker(args: str, state, config) -> bool: def _ssj_trading_submenu(config, state): """Interactive trading sub-menu for SSJ mode.""" - from tools import ask_input_interactive + from cheetahclaws.tools import ask_input_interactive _TRADING_SUBMENU = ( clr("\n╭─ 📈 Trading Agent ", "dim") + clr("━━━━━━━━━━━━━━━━━━━━━━━━━", "dim") @@ -1832,7 +1832,7 @@ def _ssj_trading_submenu(config, state): def cmd_ssj(args: str, state, config) -> bool: """SSJ Developer Mode — Interactive power menu for project workflows.""" try: - import modular + from cheetahclaws import modular _all_cmds = modular.load_all_commands() _VIDEO_AVAILABLE = "video" in _all_cmds _VOICE_MODULAR = "voice" in _all_cmds @@ -1842,7 +1842,7 @@ def cmd_ssj(args: str, state, config) -> bool: _VOICE_MODULAR = False _TRADING_AVAILABLE = False - from tools import ask_input_interactive + from cheetahclaws.tools import ask_input_interactive _SSJ_MENU = ( clr("\n╭─ SSJ Developer Mode ", "dim") + clr("⚡", "yellow") + clr(" ─────────────────────────", "dim") @@ -2108,7 +2108,7 @@ def cmd_summarize(args: str, _state, config) -> bool: if focus: info(clr(f" Focus: {focus}", "dim")) _start_tool_spinner() - from tools.files import _summarize_large_file + from cheetahclaws.tools.files import _summarize_large_file try: result = _summarize_large_file( {"file_path": str(p), "focus": focus}, config, @@ -2125,13 +2125,13 @@ def cmd_summarize(args: str, _state, config) -> bool: # ── Memory ───────────────────────────────────────────────────────────────── def cmd_memory(args: str, _state, config) -> bool: - from memory import search_memory, load_index - from memory.scan import scan_all_memories, format_memory_manifest, memory_freshness_text + from cheetahclaws.memory import search_memory, load_index + from cheetahclaws.memory.scan import scan_all_memories, format_memory_manifest, memory_freshness_text stripped = args.strip() if stripped == "consolidate": - from memory import consolidate_session + from cheetahclaws.memory import consolidate_session msgs = _state.get("messages", []) if hasattr(_state, 'get') else getattr(_state, 'messages', []) info(" Analyzing session for long-term memories…") saved = consolidate_session(msgs, config) @@ -2172,7 +2172,7 @@ def cmd_memory(args: str, _state, config) -> bool: def cmd_agents(_args: str, _state, config) -> bool: try: - from multi_agent.tools import get_agent_manager + from cheetahclaws.multi_agent.tools import get_agent_manager mgr = get_agent_manager() tasks = mgr.list_tasks() if not tasks: @@ -2191,7 +2191,7 @@ def cmd_agents(_args: str, _state, config) -> bool: def _print_background_notifications(): """Print notifications for newly completed background agent tasks.""" try: - from multi_agent.tools import get_agent_manager + from cheetahclaws.multi_agent.tools import get_agent_manager mgr = get_agent_manager() except Exception: return @@ -2220,7 +2220,7 @@ def _print_background_notifications(): # ── Skills ───────────────────────────────────────────────────────────────── def cmd_skills(_args: str, _state, config) -> bool: - from skill import load_skills + from cheetahclaws.skill import load_skills skills = load_skills() if not skills: info("No skills found.") @@ -2240,10 +2240,10 @@ def cmd_skills(_args: str, _state, config) -> bool: def cmd_mcp(args: str, _state, config) -> bool: """Show MCP server status, or manage servers.""" - from mcp_client.client import get_mcp_manager - from mcp_client.config import (load_mcp_configs, add_server_to_user_config, + from cheetahclaws.mcp_client.client import get_mcp_manager + from cheetahclaws.mcp_client.config import (load_mcp_configs, add_server_to_user_config, remove_server_from_user_config, list_config_files) - from mcp_client.tools import initialize_mcp, reload_mcp, refresh_server + from cheetahclaws.mcp_client.tools import initialize_mcp, reload_mcp, refresh_server parts = args.split() if args.strip() else [] subcmd = parts[0].lower() if parts else "" @@ -2354,7 +2354,7 @@ def cmd_mcp(args: str, _state, config) -> bool: def cmd_plugin(args: str, _state, config) -> bool: """Manage plugins.""" - from plugin import ( + from cheetahclaws.plugin import ( install_plugin, uninstall_plugin, enable_plugin, disable_plugin, disable_all_plugins, update_plugin, list_plugins, get_plugin, PluginScope, recommend_plugins, format_recommendations, @@ -2432,7 +2432,7 @@ def cmd_plugin(args: str, _state, config) -> bool: if subcmd == "recommend": context = rest if not context: - from plugin.recommend import recommend_from_files + from cheetahclaws.plugin.recommend import recommend_from_files files = list(Path.cwd().glob("**/*"))[:200] recs = recommend_from_files(files) else: @@ -2474,8 +2474,8 @@ def cmd_plugin(args: str, _state, config) -> bool: def cmd_tasks(args: str, _state, config) -> bool: """Show and manage tasks.""" - from task import list_tasks, get_task, create_task, update_task, delete_task, clear_all_tasks - from task.types import TaskStatus + from cheetahclaws.task import list_tasks, get_task, create_task, update_task, delete_task, clear_all_tasks + from cheetahclaws.task.types import TaskStatus parts = args.split(None, 1) subcmd = parts[0].lower() if parts else "" diff --git a/commands/agent_cmd.py b/cheetahclaws/commands/agent_cmd.py similarity index 98% rename from commands/agent_cmd.py rename to cheetahclaws/commands/agent_cmd.py index 4f4dc68d..9fed75ef 100644 --- a/commands/agent_cmd.py +++ b/cheetahclaws/commands/agent_cmd.py @@ -27,7 +27,7 @@ import sys from pathlib import Path -from ui.render import info, ok, warn, err, clr +from cheetahclaws.ui.render import info, ok, warn, err, clr # ── Wizard helpers ───────────────────────────────────────────────────────── @@ -39,7 +39,7 @@ def _ask(prompt: str, default: str = "", config: dict | None = None) -> str: full_prompt = f" {prompt}{suffix}: " if config is not None: try: - from tools import ask_input_interactive + from cheetahclaws.tools import ask_input_interactive val = ask_input_interactive(clr(full_prompt, "cyan"), config).strip() return val if val else default except (KeyboardInterrupt, EOFError): @@ -92,7 +92,7 @@ def _resolve_output_path(filename: str, agent_name: str) -> Path: def _wizard(config: dict) -> bool: """Interactive wizard — returns True when done (start or abort).""" - from agent_runner import list_templates, start_runner, get_runner + from cheetahclaws.agent_runner import list_templates, start_runner, get_runner def _q(prompt: str, default: str = "") -> str: """Bridge-aware ask, closed over config.""" @@ -112,7 +112,7 @@ def _q(prompt: str, default: str = "") -> str: print(f" {clr('q', 'yellow')} Quit\n") try: - from tools import ask_input_interactive + from cheetahclaws.tools import ask_input_interactive choice_raw = ask_input_interactive( clr(" Choice [1-5, q]: ", "cyan"), config, _menu_text ).strip() @@ -297,7 +297,7 @@ def cmd_agent(args: str, state, config) -> bool: /agent status — recent iteration log /agent templates — list available templates """ - from agent_runner import ( + from cheetahclaws.agent_runner import ( list_templates, list_runners, start_runner, stop_runner, stop_all, get_runner, ) @@ -450,7 +450,7 @@ def cmd_agent(args: str, state, config) -> bool: def _get_bridge_send_fn(config: dict): """Return a send_fn that pushes to the first active bridge, or None.""" - import runtime + from cheetahclaws import runtime ctx = runtime.get_session_ctx(config.get("_session_id", "default")) if ctx.tg_send: diff --git a/commands/checkpoint_plan.py b/cheetahclaws/commands/checkpoint_plan.py similarity index 96% rename from commands/checkpoint_plan.py rename to cheetahclaws/commands/checkpoint_plan.py index 38184b13..b852963f 100644 --- a/commands/checkpoint_plan.py +++ b/cheetahclaws/commands/checkpoint_plan.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Union -from ui.render import clr, info, ok, warn, err +from cheetahclaws.ui.render import clr, info, ok, warn, err def cmd_checkpoint(args: str, state, config) -> bool: @@ -19,8 +19,8 @@ def cmd_checkpoint(args: str, state, config) -> bool: /checkpoint — restore to checkpoint #id /checkpoint clear — delete all checkpoints for this session """ - import checkpoint as ckpt - from tools import ask_input_interactive + from cheetahclaws import checkpoint as ckpt + from cheetahclaws.tools import ask_input_interactive session_id = config.get("_session_id") if not session_id: @@ -137,7 +137,7 @@ def cmd_plan(args: str, state, config) -> Union[bool, tuple]: """ arg = args.strip() - import runtime + from cheetahclaws import runtime sctx = runtime.get_ctx(config) plan_file = sctx.plan_file or "" in_plan_mode = config.get("permission_mode") == "plan" diff --git a/commands/config_cmd.py b/cheetahclaws/commands/config_cmd.py similarity index 91% rename from commands/config_cmd.py rename to cheetahclaws/commands/config_cmd.py index 94ab0632..494eaa71 100644 --- a/commands/config_cmd.py +++ b/cheetahclaws/commands/config_cmd.py @@ -8,11 +8,11 @@ import json import os -from ui.render import clr, info, ok, warn, err +from cheetahclaws.ui.render import clr, info, ok, warn, err def cmd_model(args: str, _state, config) -> bool: - from providers import PROVIDERS, detect_provider + from cheetahclaws.providers import PROVIDERS, detect_provider if not args: model = config["model"] pname = detect_provider(model) @@ -21,7 +21,7 @@ def cmd_model(args: str, _state, config) -> bool: for pn, pdata in PROVIDERS.items(): if pn == "ollama": # Show live local models instead of hardcoded list - from providers import list_ollama_models + from cheetahclaws.providers import list_ollama_models base_url = ( os.environ.get("OLLAMA_BASE_URL") or config.get("ollama_base_url") @@ -55,15 +55,15 @@ def cmd_model(args: str, _state, config) -> bool: config["model"] = m pname = detect_provider(m) ok(f"Model set to {m} (provider: {pname})") - from config import save_config + from cheetahclaws.config import save_config save_config(config) return True def _interactive_ollama_picker(config: dict) -> bool: """Prompt the user to select from locally available Ollama models.""" - from providers import PROVIDERS, list_ollama_models - from tools import ask_input_interactive + from cheetahclaws.providers import PROVIDERS, list_ollama_models + from cheetahclaws.tools import ask_input_interactive prov = PROVIDERS.get("ollama", {}) base_url = ( os.environ.get("OLLAMA_BASE_URL") @@ -89,7 +89,7 @@ def _interactive_ollama_picker(config: dict) -> bool: if 0 <= idx < len(models): new_model = f"ollama/{models[idx]}" config["model"] = new_model - from config import save_config + from cheetahclaws.config import save_config save_config(config) ok(f"Model updated to {new_model}") return True @@ -101,7 +101,7 @@ def _interactive_ollama_picker(config: dict) -> bool: def cmd_config(args: str, _state, config) -> bool: - from config import save_config + from cheetahclaws.config import save_config if not args: _SECRETS = {"api_key", "anthropic_api_key", "telegram_token", "wechat_token"} display = {k: v for k, v in config.items() @@ -130,7 +130,7 @@ def cmd_config(args: str, _state, config) -> bool: # The override drives the prompt %, /context, AND the compaction # trigger. Warn if it exceeds the model's real window, since that # disables compaction and the API may reject oversized prompts. - from compaction import get_context_limit + from cheetahclaws.compaction import get_context_limit # Real window with the override forced off (keeps custom_base_url so # custom/vLLM endpoints still get their live lookup). real = get_context_limit(config.get("model", ""), {**config, "context_window": 0}) @@ -148,7 +148,7 @@ def cmd_config(args: str, _state, config) -> bool: def cmd_verbose(_args: str, _state, config) -> bool: - from config import save_config + from cheetahclaws.config import save_config config["verbose"] = not config.get("verbose", False) state_str = "ON" if config["verbose"] else "OFF" ok(f"Verbose mode: {state_str}") @@ -157,7 +157,7 @@ def cmd_verbose(_args: str, _state, config) -> bool: def cmd_quiet(_args: str, _state, config) -> bool: - from config import save_config + from cheetahclaws.config import save_config config["quiet"] = not config.get("quiet", True) state_str = "ON" if config["quiet"] else "OFF" ok(f"Quiet mode: {state_str} " @@ -168,7 +168,7 @@ def cmd_quiet(_args: str, _state, config) -> bool: def cmd_thinking(_args: str, _state, config) -> bool: - from config import save_config + from cheetahclaws.config import save_config config["thinking"] = not config.get("thinking", False) state_str = "ON" if config["thinking"] else "OFF" ok(f"Extended thinking: {state_str}") @@ -177,8 +177,8 @@ def cmd_thinking(_args: str, _state, config) -> bool: def cmd_permissions(args: str, _state, config) -> bool: - from config import save_config - from tools import ask_input_interactive + from cheetahclaws.config import save_config + from cheetahclaws.tools import ask_input_interactive modes = ["auto", "accept-all", "manual"] mode_desc = { "auto": "Prompt for each tool call (default)", diff --git a/commands/core.py b/cheetahclaws/commands/core.py similarity index 96% rename from commands/core.py rename to cheetahclaws/commands/core.py index 25b2665f..e00f89b9 100644 --- a/commands/core.py +++ b/cheetahclaws/commands/core.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import Union -from ui.render import clr, info, ok, warn, err +from cheetahclaws.ui.render import clr, info, ok, warn, err # VERSION is imported lazily from cheetahclaws to avoid circular imports _VERSION_STR = "" @@ -33,7 +33,9 @@ def _get_version() -> str: def cmd_help(_args: str, _state, config) -> bool: try: - import cheetahclaws + # The usage help lives on the CLI module's docstring, not the + # (deliberately light) package __init__. + from cheetahclaws import cli as cheetahclaws except Exception: info("CheetahClaws — type /model, /save, /load, /history, /context, /exit for commands.") return True @@ -103,8 +105,8 @@ def cmd_context(_args: str, state, config) -> bool: the estimated token cost and percentage of each component. """ import sys as _sys - from compaction import estimate_tokens, get_context_limit - from providers import detect_provider + from cheetahclaws.compaction import estimate_tokens, get_context_limit + from cheetahclaws.providers import detect_provider model = config.get("model", "unknown") provider = detect_provider(model) if model else "" @@ -119,36 +121,36 @@ def _est(text: str) -> int: # mirror Claude Code's category split). sys_tokens = 0 try: - import context as _ctx - from prompts import pick_base_prompt + from cheetahclaws import context as _ctx + from cheetahclaws.prompts import pick_base_prompt base = pick_base_prompt(provider, model) if model else pick_base_prompt() sys_tokens = (_est(base) + _est(_ctx._render_env_block(config)) + _est(_ctx._render_commands_block())) except Exception: try: - import context as _ctx + from cheetahclaws import context as _ctx sys_tokens = _est(_ctx.build_system_prompt(config)) except Exception: sys_tokens = 0 mem_tokens = 0 try: - from memory import get_memory_context + from cheetahclaws.memory import get_memory_context mem_tokens = _est(get_memory_context()) except Exception: mem_tokens = 0 tool_tokens = 0 try: - from tool_registry import get_tool_schemas + from cheetahclaws.tool_registry import get_tool_schemas tool_tokens = _est(json.dumps(get_tool_schemas())) except Exception: tool_tokens = 0 skill_tokens = 0 try: - from skill import load_skills + from cheetahclaws.skill import load_skills blob = "\n".join( f"{s.name}: {s.description} {' '.join(getattr(s, 'triggers', []) or [])}" for s in load_skills() @@ -211,7 +213,7 @@ def _est(text: str) -> int: def cmd_cost(_args: str, state, config) -> bool: - from config import calc_cost + from cheetahclaws.config import calc_cost cost = calc_cost(config["model"], state.total_input_tokens, state.total_output_tokens) @@ -236,8 +238,8 @@ def cmd_budget(args: str, state, config) -> bool: /budget daily $20 daily cost cap · /budget daily 2m daily tokens /budget clear remove all caps (unlimited) """ - import quota as _quota - from config import save_config + from cheetahclaws import quota as _quota + from cheetahclaws.config import save_config arg = args.strip() sid = config.get("_session_id", "default") @@ -303,7 +305,7 @@ def cmd_budget(args: str, state, config) -> bool: def cmd_compact(args: str, state, config) -> bool: """Manually compact conversation history.""" - from compaction import manual_compact + from cheetahclaws.compaction import manual_compact focus = args.strip() if focus: info(f"Compacting with focus: {focus}") @@ -429,8 +431,8 @@ def cmd_copy(args: str, state, config) -> bool: def cmd_status(args: str, state, config) -> bool: """Show current session status.""" - from providers import detect_provider - from compaction import estimate_tokens, get_context_limit + from cheetahclaws.providers import detect_provider + from cheetahclaws.compaction import estimate_tokens, get_context_limit model = config.get("model", "unknown") provider = detect_provider(model) @@ -459,7 +461,7 @@ def cmd_status(args: str, state, config) -> bool: def cmd_doctor(args: str, state, config) -> bool: """Diagnose installation health and connectivity.""" import subprocess as _sp - from providers import PROVIDERS, detect_provider, get_api_key + from cheetahclaws.providers import PROVIDERS, detect_provider, get_api_key ok_n = warn_n = fail_n = 0 @@ -668,8 +670,8 @@ def _fail(msg): def run_setup_wizard(config: dict) -> None: """Interactive first-run setup: pick provider, set API key, verify.""" - from config import save_config - from providers import PROVIDERS, detect_provider, get_api_key + from cheetahclaws.config import save_config + from cheetahclaws.providers import PROVIDERS, detect_provider, get_api_key print() info("Welcome to CheetahClaws! Let's get you set up.\n") @@ -708,7 +710,7 @@ def run_setup_wizard(config: dict) -> None: if chosen_pname == "ollama": # Check if Ollama is running and list local models try: - from providers import list_ollama_models + from cheetahclaws.providers import list_ollama_models base_url = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434") local_models = list_ollama_models(base_url) if local_models: @@ -840,7 +842,7 @@ def _proactive_daemon_running() -> bool: """ try: import os - from daemon import discovery + from cheetahclaws.daemon import discovery info_d = discovery.locate() if info_d is None: return False @@ -858,7 +860,7 @@ def _proactive_rpc(method: str, params: dict | None = None) -> dict | None: import http.client import json import os - from daemon import API_VERSION, API_VERSION_HEADER, discovery + from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER, discovery info_d = discovery.locate() if info_d is None: @@ -915,7 +917,7 @@ def cmd_proactive(args: str, state, config) -> bool: """ args = args.strip().lower() - import runtime + from cheetahclaws import runtime sctx = runtime.get_ctx(config) daemon_up = _proactive_daemon_running() @@ -1026,7 +1028,7 @@ def cmd_image(args: str, state, config) -> Union[bool, tuple]: size_kb = len(buf.getvalue()) / 1024 info(f"📷 Clipboard image captured ({size_kb:.0f} KB, {img.size[0]}x{img.size[1]})") - import runtime + from cheetahclaws import runtime runtime.get_ctx(config).pending_image = b64 prompt = args.strip() if args.strip() else "What do you see in this image? Describe it in detail." @@ -1089,7 +1091,7 @@ def cmd_web(args: str, state, config) -> bool: i += 1 try: - from web.server import start_web_server + from cheetahclaws.web.server import start_web_server except ImportError as e: err(f"Web module unavailable: {e}") return True @@ -1100,7 +1102,7 @@ def _run(): except SystemExit: pass except Exception as e: - import logging_utils as _log + from cheetahclaws import logging_utils as _log _log.error("web_server_crashed", error=str(e)[:200]) _web_thread = threading.Thread(target=_run, daemon=True, name="web-server") @@ -1117,7 +1119,7 @@ def cmd_circuit(args: str, state, config) -> bool: /circuit status [provider] — same as above, optionally filtered /circuit reset — force-close a breaker (or 'all') """ - import circuit_breaker as _cb + from cheetahclaws import circuit_breaker as _cb parts = args.strip().split() sub = parts[0].lower() if parts else "status" diff --git a/commands/daemon_cmd.py b/cheetahclaws/commands/daemon_cmd.py similarity index 97% rename from commands/daemon_cmd.py rename to cheetahclaws/commands/daemon_cmd.py index 9d1a2ad5..0bfd8708 100644 --- a/commands/daemon_cmd.py +++ b/cheetahclaws/commands/daemon_cmd.py @@ -20,8 +20,8 @@ from pathlib import Path from typing import Any, Optional, Tuple -from daemon import auth as _auth -from daemon import discovery as _discovery +from cheetahclaws.daemon import auth as _auth +from cheetahclaws.daemon import discovery as _discovery LOG_DIR_NAME = "logs" @@ -35,7 +35,7 @@ def _default_token_path() -> Path: - from daemon.cli import DEFAULT_TOKEN_PATH + from cheetahclaws.daemon.cli import DEFAULT_TOKEN_PATH return DEFAULT_TOKEN_PATH @@ -213,7 +213,7 @@ def _call_rpc(method: str, params: Any = None) -> Tuple[bool, Any]: address = info.get("address", "") # daemon's server enforces the API-version header; sending it lets # the request through the version gate. - from daemon import API_VERSION, API_VERSION_HEADER + from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER headers = {"Content-Type": "application/json", "Content-Length": str(len(body)), "Host": "localhost", @@ -302,7 +302,7 @@ def _post_unix(sock_path: str, path: str, body: bytes, # ── Helpers ──────────────────────────────────────────────────────────────── def _log_path() -> Path: - from config import CONFIG_DIR + from cheetahclaws.config import CONFIG_DIR return CONFIG_DIR / LOG_DIR_NAME / LOG_FILENAME diff --git a/commands/lab_cmd.py b/cheetahclaws/commands/lab_cmd.py similarity index 95% rename from commands/lab_cmd.py rename to cheetahclaws/commands/lab_cmd.py index 6e38ad2d..1da27e33 100644 --- a/commands/lab_cmd.py +++ b/cheetahclaws/commands/lab_cmd.py @@ -27,7 +27,7 @@ from pathlib import Path from typing import Optional -from ui.render import clr, info, ok, warn, err +from cheetahclaws.ui.render import clr, info, ok, warn, err # Per-run cancel flags. Run-id → threading.Event. @@ -94,8 +94,8 @@ def _cmd_start(topic: str, config: dict) -> bool: if not topic: err("Usage: /lab start ") return True - from research.lab.orchestrator import run_one_lab_session - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.orchestrator import run_one_lab_session + from cheetahclaws.research.lab.storage import LabStorage storage = LabStorage() # Read budget overrides from config (with sensible defaults). @@ -115,7 +115,7 @@ def _cmd_start(topic: str, config: dict) -> bool: cancel = threading.Event() _cancel_flags[rec.run_id] = cancel - from research.lab.storage import output_dir_for + from cheetahclaws.research.lab.storage import output_dir_for out_dir = output_dir_for(rec.run_id, rec.topic, rec.created_at) report_path = out_dir / "report.md" @@ -126,9 +126,9 @@ def _on_stage_change(stage): def _runner(): # Re-create the run inside the worker so we can pass cancel_check. - from research.lab.orchestrator import _drive, LabRun, LabState, Stage - from research.lab.roles import build_default_assignment - from research.lab.convergence import ConvergenceConfig + from cheetahclaws.research.lab.orchestrator import _drive, LabRun, LabState, Stage + from cheetahclaws.research.lab.roles import build_default_assignment + from cheetahclaws.research.lab.convergence import ConvergenceConfig roles = build_default_assignment(config, override=role_override) state = LabState(run_id=rec.run_id, topic=topic, stage=Stage.QUESTIONING) run = LabRun( @@ -175,7 +175,7 @@ def _runner(): def _cmd_status(arg: str) -> bool: - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.storage import LabStorage storage = LabStorage() arg = arg.strip() if arg: @@ -199,7 +199,7 @@ def _cmd_status(arg: str) -> bool: def _print_run_detail(rec, storage) -> None: - from research.lab.storage import output_dir_for, DEFAULT_OUTPUT_DIR + from cheetahclaws.research.lab.storage import output_dir_for, DEFAULT_OUTPUT_DIR new_dir = output_dir_for(rec.run_id, rec.topic, rec.created_at) legacy_dir = DEFAULT_OUTPUT_DIR / rec.run_id # Prefer the human-readable path; if a legacy run already wrote @@ -257,7 +257,7 @@ def _cmd_abort(arg: str) -> bool: def _cmd_logs(arg: str) -> bool: - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.storage import LabStorage args = arg.strip().split() if not args: err("Usage: /lab logs [n]") @@ -296,9 +296,9 @@ def _cmd_resume(arg: str, config: dict) -> bool: run_id = parts[0] stage_arg = parts[1].lower() if len(parts) > 1 else "" - from research.lab import resume as _resume - from research.lab.orchestrator import Stage - from research.lab.storage import LabStorage + from cheetahclaws.research.lab import resume as _resume + from cheetahclaws.research.lab.orchestrator import Stage + from cheetahclaws.research.lab.storage import LabStorage storage = LabStorage() rec = storage.get_run(run_id) @@ -361,8 +361,8 @@ def _cmd_iterate(arg: str, config: dict) -> bool: try: max_iter = int(tok.split("=", 1)[1]) except ValueError: pass - from research.lab import iterate as _it - from research.lab.storage import LabStorage + from cheetahclaws.research.lab import iterate as _it + from cheetahclaws.research.lab.storage import LabStorage storage = LabStorage() rec = storage.get_run(run_id) @@ -427,8 +427,8 @@ def _cmd_backlog(arg: str, config: dict) -> bool: sub = parts[0].lower() if parts else "list" rest = parts[1] if len(parts) > 1 else "" - from research.lab.backlog import BacklogManager - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.backlog import BacklogManager + from cheetahclaws.research.lab.storage import LabStorage mgr = BacklogManager(LabStorage()) if sub == "add": @@ -556,12 +556,12 @@ def _backlog_clear(mgr) -> bool: def _cmd_daemon(arg: str, config: dict) -> bool: sub = (arg.strip().split() or ["status"])[0].lower() - from research.lab import backlog as _bl + from cheetahclaws.research.lab import backlog as _bl if sub == "start": h = _bl.start_daemon(config=config) if h.thread.is_alive(): - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.storage import LabStorage pending = LabStorage().list_backlog(status="pending") ok("Lab daemon running. Pulls from /lab backlog continuously.") info(f" started_at : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(h.started_at))}") @@ -603,7 +603,7 @@ def _cmd_migrate_paths(arg: str, config: dict) -> bool: Default is **dry-run** so the user can review before committing — pass ``--apply`` to actually rename. """ - from research.lab.storage import ( + from cheetahclaws.research.lab.storage import ( DEFAULT_OUTPUT_DIR, LabStorage, output_dir_for, ) @@ -678,7 +678,7 @@ def _cmd_models(_arg: str, config: dict) -> bool: off a multi-day daemon (homogeneous review = same-source bias). """ import os - from research.lab.roles import build_default_assignment + from cheetahclaws.research.lab.roles import build_default_assignment override = config.get("lab_role_override") or {} assignment = build_default_assignment(config, override=override) diff --git a/commands/monitor_cmd.py b/cheetahclaws/commands/monitor_cmd.py similarity index 93% rename from commands/monitor_cmd.py rename to cheetahclaws/commands/monitor_cmd.py index b7e12f6e..cd36bb4d 100644 --- a/commands/monitor_cmd.py +++ b/cheetahclaws/commands/monitor_cmd.py @@ -40,7 +40,7 @@ """ from __future__ import annotations -from ui.render import clr, info, ok, warn, err +from cheetahclaws.ui.render import clr, info, ok, warn, err _BUILTIN_TOPICS = { "ai_research": "Latest arxiv papers (cs.AI, cs.LG, cs.CL)", @@ -102,7 +102,7 @@ def _parse_subscribe_args(args: str): def cmd_subscribe(args: str, state, config) -> bool: """Subscribe to a monitoring topic.""" - from monitor.store import add_subscription, get_subscription + from cheetahclaws.monitor.store import add_subscription, get_subscription if not args.strip(): info("Usage: /subscribe [schedule] [--telegram] [--slack]") @@ -131,8 +131,8 @@ def cmd_subscribe(args: str, state, config) -> bool: def cmd_subscriptions(args: str, state, config) -> bool: """List all active subscriptions.""" - from monitor.store import list_subscriptions - from monitor.scheduler import is_running + from cheetahclaws.monitor.store import list_subscriptions + from cheetahclaws.monitor.scheduler import is_running subs = list_subscriptions() if not subs: @@ -175,7 +175,7 @@ def cmd_subscriptions(args: str, state, config) -> bool: def cmd_unsubscribe(args: str, state, config) -> bool: """Remove a subscription.""" - from monitor.store import remove_subscription + from cheetahclaws.monitor.store import remove_subscription topic = args.strip() if not topic: @@ -191,8 +191,8 @@ def cmd_unsubscribe(args: str, state, config) -> bool: def cmd_monitor(args: str, state, config) -> bool: """Monitor management. No args → interactive setup wizard.""" - from monitor import scheduler as _sched - from monitor.store import list_subscriptions + from cheetahclaws.monitor import scheduler as _sched + from cheetahclaws.monitor.store import list_subscriptions parts = args.strip().split(None, 1) sub = parts[0].lower() if parts else "" @@ -212,7 +212,7 @@ def cmd_monitor(args: str, state, config) -> bool: # in REPL are still picked up by the daemon scheduler on its # next 60 s poll because both processes read the same SQLite. try: - from daemon import discovery as _disc + from cheetahclaws.daemon import discovery as _disc _live = _disc.locate() except Exception: _live = None @@ -229,7 +229,7 @@ def cmd_monitor(args: str, state, config) -> bool: elif sub == "stop": try: - from daemon import discovery as _disc + from cheetahclaws.daemon import discovery as _disc _live = _disc.locate() except Exception: _live = None @@ -284,7 +284,7 @@ def cmd_monitor(args: str, state, config) -> bool: def _wizard_ask(prompt: str, config: dict, menu_ctx: str = "") -> str: try: - from tools import ask_input_interactive + from cheetahclaws.tools import ask_input_interactive return ask_input_interactive(clr(prompt, "cyan"), config, menu_ctx).strip() except Exception: return input(prompt).strip() @@ -292,9 +292,9 @@ def _wizard_ask(prompt: str, config: dict, menu_ctx: str = "") -> str: def _cmd_monitor_wizard(config: dict) -> None: """Full interactive setup wizard — zero prior knowledge required.""" - from monitor.store import list_subscriptions, add_subscription, remove_subscription - from monitor import scheduler as _sched - from config import save_config + from cheetahclaws.monitor.store import list_subscriptions, add_subscription, remove_subscription + from cheetahclaws.monitor import scheduler as _sched + from cheetahclaws.config import save_config _BORDER = clr("─" * 52, "dim") @@ -356,8 +356,8 @@ def _footer(): def _wizard_add_subscription(config: dict, prefill: str = "") -> None: """Step-by-step: pick topic → schedule → channel → confirm.""" - from monitor.store import add_subscription - from monitor import scheduler as _sched + from cheetahclaws.monitor.store import add_subscription + from cheetahclaws.monitor import scheduler as _sched def _header(title: str): print() @@ -416,7 +416,7 @@ def _footer(): schedule = "daily" # ── Step 3: Push channel ────────────────────────────────────────────── - from monitor.notifier import auto_channels + from cheetahclaws.monitor.notifier import auto_channels auto_ch = auto_channels(config) _header("Step 3 / 3 — Where to send reports?") @@ -467,7 +467,7 @@ def _footer(): # Offer to run now run_now = _wizard_ask(" Run now to preview the first report? [Y/n]: ", config).lower() if run_now != "n": - from monitor.scheduler import run_one + from cheetahclaws.monitor.scheduler import run_one info(f"Fetching data for {topic} ...") report = run_one(topic, config, force=True) print() @@ -475,7 +475,7 @@ def _footer(): print() # Offer to start scheduler if not running - from monitor import scheduler as _sched + from cheetahclaws.monitor import scheduler as _sched if not _sched.is_running(): start_sched = _wizard_ask(" Start background scheduler so reports run automatically? [Y/n]: ", config).lower() if start_sched != "n": @@ -494,7 +494,7 @@ def _wizard_remove_subscription(config: dict, subs: list) -> None: raw = _wizard_ask(" » Which to remove: ", config) if raw.isdigit() and 1 <= int(raw) <= len(subs): - from monitor.store import remove_subscription + from cheetahclaws.monitor.store import remove_subscription topic = subs[int(raw) - 1]["topic"] remove_subscription(topic) ok(f"Removed: {topic}") @@ -504,7 +504,7 @@ def _wizard_remove_subscription(config: dict, subs: list) -> None: def _wizard_configure_notifications(config: dict) -> None: """Walk through Telegram / Slack setup.""" - from config import save_config + from cheetahclaws.config import save_config print() print(clr(" Push notification setup:", "bold")) @@ -535,7 +535,7 @@ def _wizard_configure_notifications(config: dict) -> None: # Send test message try: - from bridges.telegram import _tg_send + from cheetahclaws.bridges.telegram import _tg_send _tg_send(token, int(chat_id), "✅ CheetahClaws Monitor connected! You'll receive AI reports here.") ok("Telegram configured and test message sent!") @@ -564,7 +564,7 @@ def _wizard_configure_notifications(config: dict) -> None: save_config(config) try: - from bridges.slack import _slack_send + from cheetahclaws.bridges.slack import _slack_send _slack_send(token, channel, "✅ CheetahClaws Monitor connected! You'll receive AI reports here.") ok("Slack configured and test message sent!") except Exception as e: @@ -575,11 +575,11 @@ def _wizard_configure_notifications(config: dict) -> None: def _cmd_monitor_run(topic_arg: str, config: dict) -> None: - from monitor.store import list_subscriptions - from monitor.scheduler import run_one - from monitor.fetchers import fetch - from monitor.summarizer import summarize - from monitor.notifier import auto_channels, deliver + from cheetahclaws.monitor.store import list_subscriptions + from cheetahclaws.monitor.scheduler import run_one + from cheetahclaws.monitor.fetchers import fetch + from cheetahclaws.monitor.summarizer import summarize + from cheetahclaws.monitor.notifier import auto_channels, deliver if topic_arg: # Run a specific topic (even if not subscribed yet, for ad-hoc use) @@ -608,8 +608,8 @@ def _cmd_monitor_run(topic_arg: str, config: dict) -> None: def _cmd_monitor_status(config: dict) -> None: - from monitor import scheduler as _sched - from monitor.store import list_subscriptions + from cheetahclaws.monitor import scheduler as _sched + from cheetahclaws.monitor.store import list_subscriptions running = _sched.is_running() subs = list_subscriptions() @@ -655,7 +655,7 @@ def _cmd_monitor_status(config: dict) -> None: def _cmd_monitor_set(args: str, config: dict) -> None: - from config import save_config + from cheetahclaws.config import save_config parts = args.split() if not parts: diff --git a/commands/research_cmd.py b/cheetahclaws/commands/research_cmd.py similarity index 96% rename from commands/research_cmd.py rename to cheetahclaws/commands/research_cmd.py index b4556ef5..6371ea67 100644 --- a/commands/research_cmd.py +++ b/cheetahclaws/commands/research_cmd.py @@ -25,22 +25,22 @@ import shlex import time -from ui.render import clr, info, ok, warn, err +from cheetahclaws.ui.render import clr, info, ok, warn, err _VALID_DOMAINS = {"academic", "tech", "finance", "news", "social", "web"} def cmd_research(args: str, state, config) -> bool: - from research import research, build_time_range, compare - from research.citations import render_notable_section - from research.entities import render_entities_table - from research.sources import SOURCES - from research.synthesizer import ( + from cheetahclaws.research import research, build_time_range, compare + from cheetahclaws.research.citations import render_notable_section + from cheetahclaws.research.entities import render_entities_table + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.synthesizer import ( format_heat_table, format_publication_trend, format_publication_sparkline, render_citations, render_compare_brief, ) - from research import reports as _reports + from cheetahclaws.research import reports as _reports a = args.strip() if not a: @@ -398,7 +398,7 @@ def _cmd_compare(args: str, state, config, compare_fn, render_fn, err(f"Unknown flag in compare: {tok}") return True - from research import build_time_range + from cheetahclaws.research import build_time_range try: tr = build_time_range(range_token=time_range_token, since=since_str, until=until_str) @@ -452,7 +452,7 @@ def progress(name: str, status: str) -> None: if auto_save: try: # Use first topic as the saved report's topic slug; sidecar stores all - from research.types import Brief, SourceStatus + from cheetahclaws.research.types import Brief, SourceStatus fake_brief = Brief( topic=" vs ".join(topics), domains=list({d for b in result["briefs"] for d in b.domains}), @@ -472,7 +472,7 @@ def progress(name: str, status: str) -> None: def cmd_reports(args: str, state, config) -> bool: - from research import reports as _reports + from cheetahclaws.research import reports as _reports a = (args or "").strip() if not a or a in ("list", "ls"): @@ -541,7 +541,7 @@ def cmd_reports(args: str, state, config) -> bool: def _find(ref: str): - from research import reports as _reports + from cheetahclaws.research import reports as _reports try: rid = int(ref) return _reports.get_by_id(rid) @@ -550,7 +550,7 @@ def _find(ref: str): def _list_sources() -> None: - from research.sources import SOURCES + from cheetahclaws.research.sources import SOURCES info("Registered sources:") free = [s for s in SOURCES.values() if s.tier == "free"] optional = [s for s in SOURCES.values() if s.tier == "optional"] diff --git a/commands/session.py b/cheetahclaws/commands/session.py similarity index 95% rename from commands/session.py rename to cheetahclaws/commands/session.py index 9ae2e8be..9636b179 100644 --- a/commands/session.py +++ b/cheetahclaws/commands/session.py @@ -11,7 +11,7 @@ from datetime import datetime from pathlib import Path -from ui.render import clr, info, ok, warn, err +from cheetahclaws.ui.render import clr, info, ok, warn, err # ── Session format version ───────────────────────────────────────────────── # Increment when the on-disk structure changes in a backward-incompatible way. @@ -61,7 +61,7 @@ def _build_session_data(state, session_id: str | None = None) -> dict: # ── /save ────────────────────────────────────────────────────────────────── def cmd_save(args: str, state, config) -> bool: - from config import SESSIONS_DIR + from cheetahclaws.config import SESSIONS_DIR import uuid sid = uuid.uuid4().hex[:8] ts = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -77,7 +77,7 @@ def cmd_save(args: str, state, config) -> bool: def save_latest(args: str, state, config=None) -> bool: """Save session on exit: session_latest.json + daily/ copy + append to history.json.""" - from config import MR_SESSION_DIR, DAILY_DIR, SESSION_HIST_FILE + from cheetahclaws.config import MR_SESSION_DIR, DAILY_DIR, SESSION_HIST_FILE if not state.messages: return True @@ -141,7 +141,7 @@ def _atomic_write(path: Path, content: str): # Also save to SQLite for full-text search try: - from session_store import save_session as _db_save + from cheetahclaws.session_store import save_session as _db_save _db_save( session_id=sid, messages=data.get("messages", []), @@ -162,8 +162,8 @@ def _atomic_write(path: Path, content: str): # ── /load ────────────────────────────────────────────────────────────────── def cmd_load(args: str, state, config) -> bool: - from config import SESSIONS_DIR, MR_SESSION_DIR, DAILY_DIR - from tools import ask_input_interactive + from cheetahclaws.config import SESSIONS_DIR, MR_SESSION_DIR, DAILY_DIR + from cheetahclaws.tools import ask_input_interactive path = None if not args.strip(): @@ -203,7 +203,7 @@ def cmd_load(args: str, state, config) -> bool: print(clr(f" [{i+1:2d}] ", "yellow") + label) menu_buf += "\n" + clr(f" [{i+1:2d}] ", "yellow") + label - from config import SESSION_HIST_FILE + from cheetahclaws.config import SESSION_HIST_FILE has_history = SESSION_HIST_FILE.exists() if has_history: try: @@ -323,7 +323,7 @@ def cmd_load(args: str, state, config) -> bool: # ── /resume ──────────────────────────────────────────────────────────────── def cmd_resume(args: str, state, config) -> bool: - from config import MR_SESSION_DIR + from cheetahclaws.config import MR_SESSION_DIR if not args.strip(): path = MR_SESSION_DIR / "session_latest.json" @@ -345,7 +345,7 @@ def cmd_resume(args: str, state, config) -> bool: err(f"Session file is corrupted: {path}") warn(f" JSON error: {e}") # Try falling back to daily backups - from config import DAILY_DIR + from cheetahclaws.config import DAILY_DIR daily_files = sorted(DAILY_DIR.rglob("session_*.json"), key=lambda f: f.stat().st_mtime, reverse=True) if daily_files: warn(f" Try loading a recent backup: /load {daily_files[0]}") @@ -371,12 +371,12 @@ def cmd_search(args: str, state, config) -> bool: info("Search across all past session conversations.") return True - from session_store import search_sessions, session_count, import_json_sessions + from cheetahclaws.session_store import search_sessions, session_count, import_json_sessions # Auto-import legacy JSON sessions on first search count = session_count() if count == 0: - from config import SESSION_HIST_FILE + from cheetahclaws.config import SESSION_HIST_FILE imported = import_json_sessions(SESSION_HIST_FILE) if imported: info(f"Imported {imported} sessions from history.json into search index.") @@ -452,8 +452,8 @@ def cmd_cloudsave(args: str, state, config) -> bool: /cloudsave list — list your cheetahclaws Gists /cloudsave load — download and load a session from Gist """ - from cloudsave import validate_token, upload_session, list_sessions, download_session - from config import save_config + from cheetahclaws.cloudsave import validate_token, upload_session, list_sessions, download_session + from cheetahclaws.config import save_config parts = args.strip().split(None, 1) sub = parts[0].lower() if parts else "" @@ -561,8 +561,8 @@ def cmd_exit(_args: str, _state, config) -> bool: save_latest("", _state, config) if config.get("cloudsave_auto") and config.get("gist_token") and _state.messages: info("Auto cloud-sync: uploading session to Gist…") - from cloudsave import upload_session - from config import save_config + from cheetahclaws.cloudsave import upload_session + from cheetahclaws.config import save_config session_data = _build_session_data(_state) gist_id, err_msg = upload_session( session_data, config["gist_token"], diff --git a/commands/theme_cmd.py b/cheetahclaws/commands/theme_cmd.py similarity index 92% rename from commands/theme_cmd.py rename to cheetahclaws/commands/theme_cmd.py index 716649e4..222ac223 100644 --- a/commands/theme_cmd.py +++ b/cheetahclaws/commands/theme_cmd.py @@ -7,7 +7,7 @@ """ from __future__ import annotations -from ui.render import THEMES, apply_theme, clr, info, ok, warn, err, _rgb +from cheetahclaws.ui.render import THEMES, apply_theme, clr, info, ok, warn, err, _rgb _RESET = "\033[0m" @@ -53,7 +53,7 @@ def cmd_theme(args: str, _state, config) -> bool: config["theme"] = name try: - from config import save_config + from cheetahclaws.config import save_config save_config(config) except Exception as e: warn(f"Theme applied but could not be saved: {e}") diff --git a/compaction.py b/cheetahclaws/compaction.py similarity index 98% rename from compaction.py rename to cheetahclaws/compaction.py index 4e684168..770aa915 100644 --- a/compaction.py +++ b/cheetahclaws/compaction.py @@ -1,7 +1,7 @@ """Context window management: two-layer compression for long conversations.""" from __future__ import annotations -import providers +from cheetahclaws import providers # ── Token estimation ────────────────────────────────────────────────────── @@ -292,7 +292,7 @@ def compact_messages(messages: list, config: dict, focus: str = "") -> list: # If it fails (model unreachable, quota, etc.) fall back to returning the # original messages — the next layer (snip, dynamic cap) can still try. try: - from auxiliary import stream_auxiliary + from cheetahclaws.auxiliary import stream_auxiliary summary_text = stream_auxiliary( system="You are a concise summarizer.", messages=[{"role": "user", "content": summary_prompt}], @@ -300,7 +300,7 @@ def compact_messages(messages: list, config: dict, focus: str = "") -> list: ) except Exception as e: try: - import logging_utils as _log + from cheetahclaws import logging_utils as _log _log.warn("compaction_summary_failed", error_type=type(e).__name__, error=str(e)[:200]) except Exception: @@ -358,7 +358,7 @@ def maybe_compact(state, config: dict) -> bool: def _restore_plan_context(config: dict) -> list: """If in plan mode, return messages that restore plan file context.""" from pathlib import Path - import runtime + from cheetahclaws import runtime plan_file = runtime.get_ctx(config).plan_file or "" if not plan_file or config.get("permission_mode") != "plan": return [] diff --git a/config.py b/cheetahclaws/config.py similarity index 98% rename from config.py rename to cheetahclaws/config.py index 7876e390..cd5c881b 100644 --- a/config.py +++ b/cheetahclaws/config.py @@ -160,18 +160,18 @@ def save_config(cfg: dict): def current_provider(cfg: dict) -> str: - from providers import detect_provider + from cheetahclaws.providers import detect_provider return detect_provider(cfg.get("model", "claude-opus-4-6")) def has_api_key(cfg: dict) -> bool: """Check whether the active provider has an API key configured.""" - from providers import get_api_key + from cheetahclaws.providers import get_api_key pname = current_provider(cfg) key = get_api_key(pname, cfg) return bool(key) def calc_cost(model: str, in_tokens: int, out_tokens: int) -> float: - from providers import calc_cost as _cc + from cheetahclaws.providers import calc_cost as _cc return _cc(model, in_tokens, out_tokens) diff --git a/context.py b/cheetahclaws/context.py similarity index 97% rename from context.py rename to cheetahclaws/context.py index 1ab9f1b7..051f0409 100644 --- a/context.py +++ b/cheetahclaws/context.py @@ -29,8 +29,8 @@ from pathlib import Path from datetime import datetime -from memory import get_memory_context -from prompts import pick_base_prompt, load_fragment +from cheetahclaws.memory import get_memory_context +from cheetahclaws.prompts import pick_base_prompt, load_fragment # Short-TTL caches: each turn rebuilds the system prompt, and shelling out to @@ -209,7 +209,7 @@ def _render_env_block(config: dict | None = None) -> str: def _render_plan_fragment(config: dict) -> str: """Load the plan-mode fragment and fill in {plan_file}.""" - import runtime + from cheetahclaws import runtime plan_file = runtime.get_ctx(config).plan_file or "" template = load_fragment("plan") return template.format(plan_file=plan_file) @@ -217,7 +217,7 @@ def _render_plan_fragment(config: dict) -> str: def _tmux_available() -> bool: try: - from tmux_tools import tmux_available + from cheetahclaws.tmux_tools import tmux_available return tmux_available() except ImportError: return False @@ -273,7 +273,7 @@ def build_system_prompt(config: dict | None = None) -> str: 6. Plan-mode fragment (if ``permission_mode == "plan"``) """ # Resolve provider lazily to avoid circular imports at module load. - from providers import detect_provider + from cheetahclaws.providers import detect_provider cfg = config or {} model_id = cfg.get("model", "") diff --git a/daemon/__init__.py b/cheetahclaws/daemon/__init__.py similarity index 100% rename from daemon/__init__.py rename to cheetahclaws/daemon/__init__.py diff --git a/daemon/agent_methods.py b/cheetahclaws/daemon/agent_methods.py similarity index 100% rename from daemon/agent_methods.py rename to cheetahclaws/daemon/agent_methods.py diff --git a/daemon/auth.py b/cheetahclaws/daemon/auth.py similarity index 100% rename from daemon/auth.py rename to cheetahclaws/daemon/auth.py diff --git a/daemon/bridge_methods.py b/cheetahclaws/daemon/bridge_methods.py similarity index 100% rename from daemon/bridge_methods.py rename to cheetahclaws/daemon/bridge_methods.py diff --git a/daemon/bridge_supervisor.py b/cheetahclaws/daemon/bridge_supervisor.py similarity index 98% rename from daemon/bridge_supervisor.py rename to cheetahclaws/daemon/bridge_supervisor.py index 140989ad..29f4e9dd 100644 --- a/daemon/bridge_supervisor.py +++ b/cheetahclaws/daemon/bridge_supervisor.py @@ -161,7 +161,7 @@ def _resolve_sender(kind: str) -> Callable[[dict, str], bool]: doesn't pay for the WeChat transport at import time. """ if kind == "telegram": - from bridges import telegram as _tg + from cheetahclaws.bridges import telegram as _tg def _send(cfg: dict, text: str) -> bool: token = cfg.get("telegram_token", "") chat_id = cfg.get("telegram_chat_id", 0) @@ -174,7 +174,7 @@ def _send(cfg: dict, text: str) -> bool: return False return _send if kind == "slack": - from bridges import slack as _sk + from cheetahclaws.bridges import slack as _sk def _send(cfg: dict, text: str) -> bool: token = cfg.get("slack_token", "") channel = cfg.get("slack_channel", "") @@ -188,7 +188,7 @@ def _send(cfg: dict, text: str) -> bool: return False return _send if kind == "wechat": - from bridges import wechat as _wc + from cheetahclaws.bridges import wechat as _wc def _send(cfg: dict, text: str) -> bool: # WeChat send is per-user: ``wechat_user_id`` in config names # the destination contact (set by the WeChat poll loop when @@ -241,7 +241,7 @@ def _bridge_worker(handle: BridgeHandle) -> None: return if handle.kind == "telegram": - from bridges import telegram as _tg + from cheetahclaws.bridges import telegram as _tg # Tie our stop_event to the bridge's module-level Event so # ``stop()`` here is observed by the existing supervisor. _tg._telegram_stop = handle.stop_event @@ -251,7 +251,7 @@ def _bridge_worker(handle: BridgeHandle) -> None: handle.config, ) elif handle.kind == "slack": - from bridges import slack as _sk + from cheetahclaws.bridges import slack as _sk _sk._slack_stop = handle.stop_event _sk._slack_supervisor( handle.config.get("slack_token", ""), @@ -259,7 +259,7 @@ def _bridge_worker(handle: BridgeHandle) -> None: handle.config, ) elif handle.kind == "wechat": - from bridges import wechat as _wc + from cheetahclaws.bridges import wechat as _wc _wc._wechat_stop = handle.stop_event # WeChat starts its own auth path (QR login) inside # ``_wx_start_bridge``; the supervisor expects token+base_url @@ -627,7 +627,7 @@ def _outbound_loop(): def _phase2_telegram_inbound(handle, bus, session_id, origin_str): - from bridges import telegram as _tg + from cheetahclaws.bridges import telegram as _tg token = handle.config.get("telegram_token", "") chat_id = int(handle.config.get("telegram_chat_id", 0) or 0) if not token or not chat_id: @@ -687,7 +687,7 @@ def _phase2_slack_inbound(handle, bus, session_id, origin_str): Phase 2 restart would re-announce every recent message). * Update ``cursor`` to each message's ``ts`` as we process it. """ - from bridges import slack as _sk + from cheetahclaws.bridges import slack as _sk token = handle.config.get("slack_token", "") channel = handle.config.get("slack_channel", "") if not token or not channel: @@ -743,7 +743,7 @@ def _phase2_wechat_inbound(handle, bus, session_id, origin_str): response as the next iteration's sync token so the long-poll only returns new messages. """ - from bridges import wechat as _wc + from cheetahclaws.bridges import wechat as _wc token = handle.config.get("wechat_token", "") base_url = handle.config.get("wechat_base_url", "") user_id = handle.config.get("wechat_user_id", "") diff --git a/daemon/cli.py b/cheetahclaws/daemon/cli.py similarity index 97% rename from daemon/cli.py rename to cheetahclaws/daemon/cli.py index 7a7f6c86..bf47457f 100644 --- a/daemon/cli.py +++ b/cheetahclaws/daemon/cli.py @@ -175,8 +175,8 @@ def cmd_serve(args: argparse.Namespace) -> int: return 1 # ── Load config + bootstrap (logging, tool registry) ────────────────── - from config import load_config - from bootstrap import bootstrap as _bootstrap + from cheetahclaws.config import load_config + from cheetahclaws.bootstrap import bootstrap as _bootstrap config = load_config() if not config.get("log_file"): log_dir = data_dir / "logs" @@ -199,7 +199,7 @@ def cmd_serve(args: argparse.Namespace) -> int: # Pin the health.py module-level config so default-arg payload helpers # see the model name on every call. - import health as _health + from cheetahclaws import health as _health _health.install_config(config) audit_enabled = not args.no_audit @@ -241,7 +241,7 @@ def cmd_serve(args: argparse.Namespace) -> int: kernel_db = Path(args.kernel_db).expanduser() if args.kernel_db \ else (data_dir / "kernel.db") try: - from kernel import register_with_daemon as _register_kernel + from cheetahclaws.kernel import register_with_daemon as _register_kernel _register_kernel( server.daemon_state, kernel_db, recovery=args.kernel_recovery, @@ -285,7 +285,7 @@ def cmd_serve(args: argparse.Namespace) -> int: # REPL-side step-aside check (otherwise the daemon would defer to its # own discovery entry and never run a subscription). try: - from monitor.scheduler import start as _monitor_start + from cheetahclaws.monitor.scheduler import start as _monitor_start _monitor_start(config, on_report=None, owned_by_daemon=True) except Exception as exc: print(f"warning: monitor scheduler did not start: {exc}", @@ -311,7 +311,7 @@ def cmd_serve(args: argparse.Namespace) -> int: def _watch_shutdown(): server.daemon_state.shutdown_event.wait() try: - from monitor.scheduler import stop as _monitor_stop + from cheetahclaws.monitor.scheduler import stop as _monitor_stop _monitor_stop() except Exception: pass @@ -442,7 +442,7 @@ def main(argv: Optional[list[str]] = None) -> int: if cmd in ("status", "stop", "logs", "rotate-token"): # daemon_cmd.dispatch reads cmd from argv[0] - from commands.daemon_cmd import dispatch as _daemon_dispatch + from cheetahclaws.commands.daemon_cmd import dispatch as _daemon_dispatch return _daemon_dispatch(argv) print(f"unknown subcommand: {cmd}\n", file=sys.stderr) diff --git a/daemon/discovery.py b/cheetahclaws/daemon/discovery.py similarity index 99% rename from daemon/discovery.py rename to cheetahclaws/daemon/discovery.py index e5714501..0be7cc2b 100644 --- a/daemon/discovery.py +++ b/cheetahclaws/daemon/discovery.py @@ -35,7 +35,7 @@ def get_default_path() -> Path: """Default discovery-file location: ``~/.cheetahclaws/daemon.json``.""" - from config import CONFIG_DIR + from cheetahclaws.config import CONFIG_DIR return CONFIG_DIR / DEFAULT_FILENAME diff --git a/daemon/events.py b/cheetahclaws/daemon/events.py similarity index 100% rename from daemon/events.py rename to cheetahclaws/daemon/events.py diff --git a/daemon/methods.py b/cheetahclaws/daemon/methods.py similarity index 100% rename from daemon/methods.py rename to cheetahclaws/daemon/methods.py diff --git a/daemon/monitor_methods.py b/cheetahclaws/daemon/monitor_methods.py similarity index 91% rename from daemon/monitor_methods.py rename to cheetahclaws/daemon/monitor_methods.py index 87d5e910..3bb7cb78 100644 --- a/daemon/monitor_methods.py +++ b/cheetahclaws/daemon/monitor_methods.py @@ -47,7 +47,7 @@ def monitor_subscribe(params: dict, _ctx) -> dict: channels = params.get("channels") if channels is not None and not isinstance(channels, list): raise TypeError("'channels' must be a list of strings") - from monitor.store import add_subscription + from cheetahclaws.monitor.store import add_subscription sub = add_subscription(topic, schedule=schedule, channels=channels) return sub @@ -55,18 +55,18 @@ def monitor_unsubscribe(params: dict, _ctx) -> dict: topic = params.get("topic") if not isinstance(topic, str) or not topic: raise TypeError("monitor.unsubscribe requires non-empty 'topic'") - from monitor.store import remove_subscription + from cheetahclaws.monitor.store import remove_subscription return {"topic": topic, "removed": remove_subscription(topic)} def monitor_list(_params: dict, _ctx) -> dict: - from monitor.store import list_subscriptions + from cheetahclaws.monitor.store import list_subscriptions return {"subscriptions": list_subscriptions()} def monitor_run(params: dict, _ctx) -> dict: topic = params.get("topic") if not isinstance(topic, str) or not topic: raise TypeError("monitor.run requires non-empty 'topic'") - from monitor.scheduler import run_one + from cheetahclaws.monitor.scheduler import run_one report = run_one(topic, config=daemon_state.config or {}) return {"topic": topic, "report": report} diff --git a/daemon/originator.py b/cheetahclaws/daemon/originator.py similarity index 100% rename from daemon/originator.py rename to cheetahclaws/daemon/originator.py diff --git a/daemon/permission.py b/cheetahclaws/daemon/permission.py similarity index 100% rename from daemon/permission.py rename to cheetahclaws/daemon/permission.py diff --git a/daemon/proactive_methods.py b/cheetahclaws/daemon/proactive_methods.py similarity index 100% rename from daemon/proactive_methods.py rename to cheetahclaws/daemon/proactive_methods.py diff --git a/daemon/proactive_scheduler.py b/cheetahclaws/daemon/proactive_scheduler.py similarity index 100% rename from daemon/proactive_scheduler.py rename to cheetahclaws/daemon/proactive_scheduler.py diff --git a/daemon/proactive_state.py b/cheetahclaws/daemon/proactive_state.py similarity index 100% rename from daemon/proactive_state.py rename to cheetahclaws/daemon/proactive_state.py diff --git a/daemon/rpc.py b/cheetahclaws/daemon/rpc.py similarity index 100% rename from daemon/rpc.py rename to cheetahclaws/daemon/rpc.py diff --git a/daemon/runner_ipc.py b/cheetahclaws/daemon/runner_ipc.py similarity index 95% rename from daemon/runner_ipc.py rename to cheetahclaws/daemon/runner_ipc.py index d1bc47aa..f69db5d8 100644 --- a/daemon/runner_ipc.py +++ b/cheetahclaws/daemon/runner_ipc.py @@ -28,6 +28,6 @@ """ from __future__ import annotations -from kernel.runner.ipc import IpcReadTimeout, JsonLineChannel +from cheetahclaws.kernel.runner.ipc import IpcReadTimeout, JsonLineChannel __all__ = ["IpcReadTimeout", "JsonLineChannel"] diff --git a/daemon/runner_supervisor.py b/cheetahclaws/daemon/runner_supervisor.py similarity index 99% rename from daemon/runner_supervisor.py rename to cheetahclaws/daemon/runner_supervisor.py index 09d3b8b0..5b26fb77 100644 --- a/daemon/runner_supervisor.py +++ b/cheetahclaws/daemon/runner_supervisor.py @@ -346,7 +346,7 @@ def start( log_dir = _LOG_DIR / name log_dir.mkdir(parents=True, exist_ok=True) - cmd = [python, "-u", "-m", "agent_runner", "--pipe", "--name", name] + cmd = [python, "-u", "-m", "cheetahclaws.agent_runner", "--pipe", "--name", name] proc = subprocess.Popen( cmd, stdin=subprocess.PIPE, diff --git a/daemon/schema.py b/cheetahclaws/daemon/schema.py similarity index 99% rename from daemon/schema.py rename to cheetahclaws/daemon/schema.py index 97ed4b2d..718f4821 100644 --- a/daemon/schema.py +++ b/cheetahclaws/daemon/schema.py @@ -142,7 +142,7 @@ def get_default_db_path() -> Path: """Return ``~/.cheetahclaws/sessions.db`` (shared with session_store).""" - from config import CONFIG_DIR + from cheetahclaws.config import CONFIG_DIR return CONFIG_DIR / "sessions.db" diff --git a/daemon/server.py b/cheetahclaws/daemon/server.py similarity index 99% rename from daemon/server.py rename to cheetahclaws/daemon/server.py index 27601ccd..8627bcee 100644 --- a/daemon/server.py +++ b/cheetahclaws/daemon/server.py @@ -143,7 +143,7 @@ def do_GET(self) -> None: def _serve_health(self, path: str) -> None: try: - import health as _health + from cheetahclaws import health as _health payload = _health.payload_for(path, self.state.config) except Exception: payload = {"status": "ok"} diff --git a/daemon/session_methods.py b/cheetahclaws/daemon/session_methods.py similarity index 100% rename from daemon/session_methods.py rename to cheetahclaws/daemon/session_methods.py diff --git a/daemon/spike_client.py b/cheetahclaws/daemon/spike_client.py similarity index 100% rename from daemon/spike_client.py rename to cheetahclaws/daemon/spike_client.py diff --git a/daemon/system_methods.py b/cheetahclaws/daemon/system_methods.py similarity index 100% rename from daemon/system_methods.py rename to cheetahclaws/daemon/system_methods.py diff --git a/error_classifier.py b/cheetahclaws/error_classifier.py similarity index 100% rename from error_classifier.py rename to cheetahclaws/error_classifier.py diff --git a/health.py b/cheetahclaws/health.py similarity index 95% rename from health.py rename to cheetahclaws/health.py index 6c2c14e5..68aed273 100644 --- a/health.py +++ b/cheetahclaws/health.py @@ -80,7 +80,7 @@ def uptime_seconds() -> float: def _circuit_states() -> dict[str, str]: try: - from circuit_breaker import _registry as _cb_reg, _registry_lock as _cb_lock + from cheetahclaws.circuit_breaker import _registry as _cb_reg, _registry_lock as _cb_lock with _cb_lock: return {p: b.state.value for p, b in _cb_reg.items()} except Exception: @@ -89,7 +89,7 @@ def _circuit_states() -> dict[str, str]: def _active_sessions() -> int: try: - from runtime import _registry as _rt_reg, _registry_lock as _rt_lock + from cheetahclaws.runtime import _registry as _rt_reg, _registry_lock as _rt_lock with _rt_lock: return len(_rt_reg) except Exception: @@ -128,7 +128,7 @@ def metrics_payload(config: dict | None = None) -> dict: # Today's quota usage (read from file — best effort) daily_tokens = daily_cost = 0 try: - from quota import _load_daily, _lock as _q_lock + from cheetahclaws.quota import _load_daily, _lock as _q_lock with _q_lock: daily_tokens, daily_cost = _load_daily() except Exception: @@ -183,7 +183,7 @@ def start_health_server(port: int, config: dict) -> None: server = HTTPServer(("", port), _HealthHandler) def _serve(): - import logging_utils as _log + from cheetahclaws import logging_utils as _log _log.info("health_server_listening", port=port) try: server.serve_forever() diff --git a/jobs.py b/cheetahclaws/jobs.py similarity index 97% rename from jobs.py rename to cheetahclaws/jobs.py index fcf01bfe..5d6779d2 100644 --- a/jobs.py +++ b/cheetahclaws/jobs.py @@ -6,7 +6,7 @@ Each Job records which tools were used (steps), result preview, and timing. Usage (by bridges): - import jobs + from cheetahclaws import jobs job = jobs.create("帮我总结实验", source="telegram") jobs.start(job.id) jobs.add_step(job.id, "Bash", "pytest tests/") @@ -178,7 +178,7 @@ def _ensure_migrated() -> None: global _migration_done_in_process if _migration_done_in_process: return - from daemon.schema import get_conn + from cheetahclaws.daemon.schema import get_conn conn = get_conn() row = conn.execute( "SELECT value FROM schema_meta WHERE key=?", (_MIGRATION_KEY,) @@ -229,7 +229,7 @@ def _row_to_job(row) -> Job: def _persist(job: Job, conn=None) -> None: """INSERT or UPDATE the row for *job*. Caller passes *conn* during migration to avoid re-entering ``get_conn`` from inside a transaction.""" - from daemon.schema import get_conn + from cheetahclaws.daemon.schema import get_conn c = conn if conn is not None else get_conn() c.execute( "INSERT INTO jobs (id, title, prompt, source, status, created_at, " @@ -256,7 +256,7 @@ def _persist(job: Job, conn=None) -> None: def _prune_to_max(conn=None) -> None: """Keep only the most-recent ``_MAX_JOBS`` rows.""" - from daemon.schema import get_conn + from cheetahclaws.daemon.schema import get_conn c = conn if conn is not None else get_conn() excess = c.execute( "SELECT COUNT(*) FROM jobs" @@ -274,7 +274,7 @@ def _prune_to_max(conn=None) -> None: def _get_all() -> list[Job]: _ensure_migrated() - from daemon.schema import get_conn + from cheetahclaws.daemon.schema import get_conn rows = get_conn().execute( "SELECT * FROM jobs ORDER BY created_at" ).fetchall() @@ -421,7 +421,7 @@ def cancel(job_id: str) -> None: def get(job_id: str) -> Optional[Job]: _ensure_migrated() - from daemon.schema import get_conn + from cheetahclaws.daemon.schema import get_conn row = get_conn().execute( "SELECT * FROM jobs WHERE id=?", (job_id,) ).fetchone() @@ -431,7 +431,7 @@ def get(job_id: str) -> Optional[Job]: def list_recent(n: int = 10) -> list[Job]: """Return last N jobs, newest first.""" _ensure_migrated() - from daemon.schema import get_conn + from cheetahclaws.daemon.schema import get_conn rows = get_conn().execute( "SELECT * FROM jobs ORDER BY created_at DESC LIMIT ?", (n,) ).fetchall() @@ -440,7 +440,7 @@ def list_recent(n: int = 10) -> list[Job]: def list_running() -> list[Job]: _ensure_migrated() - from daemon.schema import get_conn + from cheetahclaws.daemon.schema import get_conn rows = get_conn().execute( "SELECT * FROM jobs WHERE status='running' ORDER BY started_at" ).fetchall() diff --git a/kernel/__init__.py b/cheetahclaws/kernel/__init__.py similarity index 100% rename from kernel/__init__.py rename to cheetahclaws/kernel/__init__.py diff --git a/kernel/agentfs.py b/cheetahclaws/kernel/agentfs.py similarity index 99% rename from kernel/agentfs.py rename to cheetahclaws/kernel/agentfs.py index f11bd388..4814fcc2 100644 --- a/kernel/agentfs.py +++ b/cheetahclaws/kernel/agentfs.py @@ -28,7 +28,7 @@ ) if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry from .ledger import LedgerStore diff --git a/kernel/api.py b/cheetahclaws/kernel/api.py similarity index 99% rename from kernel/api.py rename to cheetahclaws/kernel/api.py index c01bfaef..88bc947c 100644 --- a/kernel/api.py +++ b/cheetahclaws/kernel/api.py @@ -122,7 +122,7 @@ def open( bus = None if publish_to_bus: try: - from daemon import events as _events + from cheetahclaws.daemon import events as _events bus = _events.get_bus() except Exception: bus = None diff --git a/kernel/bridge_mirror.py b/cheetahclaws/kernel/bridge_mirror.py similarity index 100% rename from kernel/bridge_mirror.py rename to cheetahclaws/kernel/bridge_mirror.py diff --git a/kernel/capability.py b/cheetahclaws/kernel/capability.py similarity index 99% rename from kernel/capability.py rename to cheetahclaws/kernel/capability.py index bc097237..2adcac30 100644 --- a/kernel/capability.py +++ b/cheetahclaws/kernel/capability.py @@ -32,7 +32,7 @@ ) if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # Reserved wildcard tokens (RFC 0005 §2 "Reserved tokens"). diff --git a/kernel/chaos.py b/cheetahclaws/kernel/chaos.py similarity index 100% rename from kernel/chaos.py rename to cheetahclaws/kernel/chaos.py diff --git a/kernel/cli.py b/cheetahclaws/kernel/cli.py similarity index 98% rename from kernel/cli.py rename to cheetahclaws/kernel/cli.py index 2fff4abf..3391e3d1 100644 --- a/kernel/cli.py +++ b/cheetahclaws/kernel/cli.py @@ -21,9 +21,9 @@ from pathlib import Path from typing import Any, Optional, Tuple -from daemon import API_VERSION, API_VERSION_HEADER -from daemon import auth as _auth -from daemon import discovery as _discovery +from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER +from cheetahclaws.daemon import auth as _auth +from cheetahclaws.daemon import discovery as _discovery RPC_TIMEOUT_S = 5.0 @@ -518,7 +518,7 @@ def _resolve_token_path(info: Optional[dict]) -> Path: recorded = info.get("token_path") if isinstance(recorded, str) and recorded: return Path(recorded).expanduser() - from daemon.cli import DEFAULT_TOKEN_PATH + from cheetahclaws.daemon.cli import DEFAULT_TOKEN_PATH return DEFAULT_TOKEN_PATH diff --git a/kernel/contract.py b/cheetahclaws/kernel/contract.py similarity index 98% rename from kernel/contract.py rename to cheetahclaws/kernel/contract.py index b03d4dd5..64a2f7ac 100644 --- a/kernel/contract.py +++ b/cheetahclaws/kernel/contract.py @@ -16,7 +16,7 @@ from .errors import InvalidPayload if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # ── Frozen v1.0 method registry ──────────────────────────────────────────── @@ -194,7 +194,7 @@ def api_version_info(params, ctx): # may not be importable in pure-kernel test setups, so we # fall back to None. try: - from daemon import API_VERSION as _api + from cheetahclaws.daemon import API_VERSION as _api api_version = _api except Exception: api_version = None diff --git a/kernel/errors.py b/cheetahclaws/kernel/errors.py similarity index 100% rename from kernel/errors.py rename to cheetahclaws/kernel/errors.py diff --git a/kernel/event_log.py b/cheetahclaws/kernel/event_log.py similarity index 100% rename from kernel/event_log.py rename to cheetahclaws/kernel/event_log.py diff --git a/kernel/integration.py b/cheetahclaws/kernel/integration.py similarity index 98% rename from kernel/integration.py rename to cheetahclaws/kernel/integration.py index d0323936..93b226e4 100644 --- a/kernel/integration.py +++ b/cheetahclaws/kernel/integration.py @@ -17,7 +17,7 @@ from .store import KernelStore, RECOVERY_SUSPEND, _RECOVERY_POLICIES if TYPE_CHECKING: - from daemon.server import DaemonState + from cheetahclaws.daemon.server import DaemonState log = logging.getLogger(__name__) @@ -48,7 +48,7 @@ def register_with_daemon( bus = None if publish_to_bus: # Lazy import: only the kernel-enabled path pulls in the bus. - from daemon import events as _events + from cheetahclaws.daemon import events as _events bus = _events.get_bus() store = KernelStore.open(db_path, bus=bus) diff --git a/kernel/ledger.py b/cheetahclaws/kernel/ledger.py similarity index 99% rename from kernel/ledger.py rename to cheetahclaws/kernel/ledger.py index af7a9b3c..3eb297e3 100644 --- a/kernel/ledger.py +++ b/cheetahclaws/kernel/ledger.py @@ -26,7 +26,7 @@ ) if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # Standard dimension names — documented for the supervisor / runtime, diff --git a/kernel/mailbox.py b/cheetahclaws/kernel/mailbox.py similarity index 99% rename from kernel/mailbox.py rename to cheetahclaws/kernel/mailbox.py index fd33fc68..c64a0370 100644 --- a/kernel/mailbox.py +++ b/cheetahclaws/kernel/mailbox.py @@ -26,7 +26,7 @@ ) if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # ── Dataclasses ──────────────────────────────────────────────────────────── diff --git a/kernel/methods.py b/cheetahclaws/kernel/methods.py similarity index 99% rename from kernel/methods.py rename to cheetahclaws/kernel/methods.py index 13b35778..46c2e29d 100644 --- a/kernel/methods.py +++ b/cheetahclaws/kernel/methods.py @@ -30,7 +30,7 @@ from .store import KernelStore if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # ── Param coercion helpers ───────────────────────────────────────────────── diff --git a/kernel/observability.py b/cheetahclaws/kernel/observability.py similarity index 99% rename from kernel/observability.py rename to cheetahclaws/kernel/observability.py index c202f7a8..b221382a 100644 --- a/kernel/observability.py +++ b/cheetahclaws/kernel/observability.py @@ -16,7 +16,7 @@ from .schema import EXPECTED_SCHEMA_VERSION if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry from .agentfs import AgentFSStore from .capability import CapabilityStore from .ledger import LedgerStore diff --git a/kernel/orchestrator/__init__.py b/cheetahclaws/kernel/orchestrator/__init__.py similarity index 100% rename from kernel/orchestrator/__init__.py rename to cheetahclaws/kernel/orchestrator/__init__.py diff --git a/kernel/orchestrator/dialogue.py b/cheetahclaws/kernel/orchestrator/dialogue.py similarity index 99% rename from kernel/orchestrator/dialogue.py rename to cheetahclaws/kernel/orchestrator/dialogue.py index 47752734..3519d20a 100644 --- a/kernel/orchestrator/dialogue.py +++ b/cheetahclaws/kernel/orchestrator/dialogue.py @@ -36,7 +36,7 @@ HISTORY_SCHEMA_VERSION = 1 -_DEFAULT_RUNNER_ARGV = (sys.executable, "-m", "kernel.runner.llm") +_DEFAULT_RUNNER_ARGV = (sys.executable, "-m", "cheetahclaws.kernel.runner.llm") # ── Errors ──────────────────────────────────────────────────────────────── diff --git a/kernel/process.py b/cheetahclaws/kernel/process.py similarity index 100% rename from kernel/process.py rename to cheetahclaws/kernel/process.py diff --git a/kernel/registry.py b/cheetahclaws/kernel/registry.py similarity index 99% rename from kernel/registry.py rename to cheetahclaws/kernel/registry.py index f063590e..6422b25a 100644 --- a/kernel/registry.py +++ b/cheetahclaws/kernel/registry.py @@ -23,7 +23,7 @@ ) if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # Validation: 256-byte cap, no NUL/control characters. Path syntax diff --git a/kernel/runner/__init__.py b/cheetahclaws/kernel/runner/__init__.py similarity index 100% rename from kernel/runner/__init__.py rename to cheetahclaws/kernel/runner/__init__.py diff --git a/kernel/runner/ipc.py b/cheetahclaws/kernel/runner/ipc.py similarity index 100% rename from kernel/runner/ipc.py rename to cheetahclaws/kernel/runner/ipc.py diff --git a/kernel/runner/llm/__init__.py b/cheetahclaws/kernel/runner/llm/__init__.py similarity index 100% rename from kernel/runner/llm/__init__.py rename to cheetahclaws/kernel/runner/llm/__init__.py diff --git a/kernel/runner/llm/__main__.py b/cheetahclaws/kernel/runner/llm/__main__.py similarity index 100% rename from kernel/runner/llm/__main__.py rename to cheetahclaws/kernel/runner/llm/__main__.py diff --git a/kernel/runner/llm/anthropic_provider.py b/cheetahclaws/kernel/runner/llm/anthropic_provider.py similarity index 100% rename from kernel/runner/llm/anthropic_provider.py rename to cheetahclaws/kernel/runner/llm/anthropic_provider.py diff --git a/kernel/runner/llm/litellm_provider.py b/cheetahclaws/kernel/runner/llm/litellm_provider.py similarity index 100% rename from kernel/runner/llm/litellm_provider.py rename to cheetahclaws/kernel/runner/llm/litellm_provider.py diff --git a/kernel/runner/llm/provider.py b/cheetahclaws/kernel/runner/llm/provider.py similarity index 100% rename from kernel/runner/llm/provider.py rename to cheetahclaws/kernel/runner/llm/provider.py diff --git a/kernel/runner/runner_main.py b/cheetahclaws/kernel/runner/runner_main.py similarity index 100% rename from kernel/runner/runner_main.py rename to cheetahclaws/kernel/runner/runner_main.py diff --git a/kernel/runner/supervisor.py b/cheetahclaws/kernel/runner/supervisor.py similarity index 100% rename from kernel/runner/supervisor.py rename to cheetahclaws/kernel/runner/supervisor.py diff --git a/kernel/sandbox.py b/cheetahclaws/kernel/sandbox.py similarity index 100% rename from kernel/sandbox.py rename to cheetahclaws/kernel/sandbox.py diff --git a/kernel/scheduler.py b/cheetahclaws/kernel/scheduler.py similarity index 99% rename from kernel/scheduler.py rename to cheetahclaws/kernel/scheduler.py index 7574d449..25e4fbf9 100644 --- a/kernel/scheduler.py +++ b/cheetahclaws/kernel/scheduler.py @@ -32,7 +32,7 @@ ) if TYPE_CHECKING: - from daemon.rpc import CallContext, RpcRegistry + from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # Queue states (also referenced from RPC param validation) diff --git a/kernel/schema.py b/cheetahclaws/kernel/schema.py similarity index 100% rename from kernel/schema.py rename to cheetahclaws/kernel/schema.py diff --git a/kernel/store.py b/cheetahclaws/kernel/store.py similarity index 100% rename from kernel/store.py rename to cheetahclaws/kernel/store.py diff --git a/kernel/tools/__init__.py b/cheetahclaws/kernel/tools/__init__.py similarity index 100% rename from kernel/tools/__init__.py rename to cheetahclaws/kernel/tools/__init__.py diff --git a/kernel/tools/ast_tool.py b/cheetahclaws/kernel/tools/ast_tool.py similarity index 100% rename from kernel/tools/ast_tool.py rename to cheetahclaws/kernel/tools/ast_tool.py diff --git a/kernel/tools/builtin.py b/cheetahclaws/kernel/tools/builtin.py similarity index 100% rename from kernel/tools/builtin.py rename to cheetahclaws/kernel/tools/builtin.py diff --git a/kernel/tools/diff_tool.py b/cheetahclaws/kernel/tools/diff_tool.py similarity index 100% rename from kernel/tools/diff_tool.py rename to cheetahclaws/kernel/tools/diff_tool.py diff --git a/kernel/tools/exec_tool.py b/cheetahclaws/kernel/tools/exec_tool.py similarity index 100% rename from kernel/tools/exec_tool.py rename to cheetahclaws/kernel/tools/exec_tool.py diff --git a/kernel/tools/fetch_tool.py b/cheetahclaws/kernel/tools/fetch_tool.py similarity index 100% rename from kernel/tools/fetch_tool.py rename to cheetahclaws/kernel/tools/fetch_tool.py diff --git a/kernel/tools/git_tool.py b/cheetahclaws/kernel/tools/git_tool.py similarity index 100% rename from kernel/tools/git_tool.py rename to cheetahclaws/kernel/tools/git_tool.py diff --git a/kernel/tools/registry.py b/cheetahclaws/kernel/tools/registry.py similarity index 100% rename from kernel/tools/registry.py rename to cheetahclaws/kernel/tools/registry.py diff --git a/kernel/worker.py b/cheetahclaws/kernel/worker.py similarity index 100% rename from kernel/worker.py rename to cheetahclaws/kernel/worker.py diff --git a/logging_utils.py b/cheetahclaws/logging_utils.py similarity index 100% rename from logging_utils.py rename to cheetahclaws/logging_utils.py diff --git a/mcp_client/__init__.py b/cheetahclaws/mcp_client/__init__.py similarity index 100% rename from mcp_client/__init__.py rename to cheetahclaws/mcp_client/__init__.py diff --git a/mcp_client/client.py b/cheetahclaws/mcp_client/client.py similarity index 99% rename from mcp_client/client.py rename to cheetahclaws/mcp_client/client.py index 59b71b87..408aa7c4 100644 --- a/mcp_client/client.py +++ b/cheetahclaws/mcp_client/client.py @@ -226,7 +226,7 @@ def _ensure_oauth(self) -> None: """ with self._oauth_lock: if self._oauth is None: - from mcp_client.oauth import OAuthSession + from cheetahclaws.mcp_client.oauth import OAuthSession # Pass any non-auth headers already configured (e.g. custom SAP headers) extra = {k: v for k, v in self._config.headers.items() if not k.lower().startswith("authorization")} diff --git a/mcp_client/config.py b/cheetahclaws/mcp_client/config.py similarity index 100% rename from mcp_client/config.py rename to cheetahclaws/mcp_client/config.py diff --git a/mcp_client/oauth.py b/cheetahclaws/mcp_client/oauth.py similarity index 99% rename from mcp_client/oauth.py rename to cheetahclaws/mcp_client/oauth.py index 6add7cc1..8f4b9cd0 100644 --- a/mcp_client/oauth.py +++ b/cheetahclaws/mcp_client/oauth.py @@ -9,7 +9,7 @@ - Token persistence to ~/.cheetahclaws/mcp_oauth.json Usage (from HttpTransport): - from mcp_client.oauth import OAuthSession + from cheetahclaws.mcp_client.oauth import OAuthSession session = OAuthSession(server_name, resource_url, headers_config) token = session.get_token() # blocks for browser auth on first call # then inject: {"Authorization": f"Bearer {token}"} diff --git a/mcp_client/tools.py b/cheetahclaws/mcp_client/tools.py similarity index 98% rename from mcp_client/tools.py rename to cheetahclaws/mcp_client/tools.py index f2dc5c08..fe34dccc 100644 --- a/mcp_client/tools.py +++ b/cheetahclaws/mcp_client/tools.py @@ -16,7 +16,7 @@ import threading from typing import Dict, List, Optional -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool from .client import MCPClient, MCPManager, get_mcp_manager from .config import load_mcp_configs from .types import MCPServerConfig, MCPTool diff --git a/mcp_client/types.py b/cheetahclaws/mcp_client/types.py similarity index 100% rename from mcp_client/types.py rename to cheetahclaws/mcp_client/types.py diff --git a/memory/__init__.py b/cheetahclaws/memory/__init__.py similarity index 100% rename from memory/__init__.py rename to cheetahclaws/memory/__init__.py diff --git a/memory/consolidator.py b/cheetahclaws/memory/consolidator.py similarity index 98% rename from memory/consolidator.py rename to cheetahclaws/memory/consolidator.py index 3573c2d3..377cd9e7 100644 --- a/memory/consolidator.py +++ b/cheetahclaws/memory/consolidator.py @@ -58,7 +58,7 @@ def consolidate_session(messages: list, config: dict) -> list[str]: return [] try: - from providers import stream, AssistantTurn + from cheetahclaws.providers import stream, AssistantTurn from .store import MemoryEntry, save_memory, check_conflict import json diff --git a/memory/context.py b/cheetahclaws/memory/context.py similarity index 99% rename from memory/context.py rename to cheetahclaws/memory/context.py index f48799ab..d875f38f 100644 --- a/memory/context.py +++ b/cheetahclaws/memory/context.py @@ -164,7 +164,7 @@ def _ai_select_memories( Falls back to keyword results on any error. """ try: - from providers import stream, AssistantTurn + from cheetahclaws.providers import stream, AssistantTurn from .scan import scan_all_memories headers = scan_all_memories() diff --git a/memory/scan.py b/cheetahclaws/memory/scan.py similarity index 100% rename from memory/scan.py rename to cheetahclaws/memory/scan.py diff --git a/memory/store.py b/cheetahclaws/memory/store.py similarity index 100% rename from memory/store.py rename to cheetahclaws/memory/store.py diff --git a/memory/tools.py b/cheetahclaws/memory/tools.py similarity index 99% rename from memory/tools.py rename to cheetahclaws/memory/tools.py index 858086e3..ad3cbb54 100644 --- a/memory/tools.py +++ b/cheetahclaws/memory/tools.py @@ -6,7 +6,7 @@ from datetime import datetime -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool from .store import MemoryEntry, save_memory, delete_memory, load_index, check_conflict, touch_last_used from .context import find_relevant_memories from .scan import scan_all_memories, format_memory_manifest diff --git a/memory/types.py b/cheetahclaws/memory/types.py similarity index 100% rename from memory/types.py rename to cheetahclaws/memory/types.py diff --git a/modular/__init__.py b/cheetahclaws/modular/__init__.py similarity index 95% rename from modular/__init__.py rename to cheetahclaws/modular/__init__.py index 473c6224..34d2cf84 100644 --- a/modular/__init__.py +++ b/cheetahclaws/modular/__init__.py @@ -60,7 +60,7 @@ def load_all_commands() -> dict[str, dict]: cmd_path = mod_dir / "cmd.py" if not cmd_path.exists(): continue - fqn = f"modular.{mod_dir.name}.cmd" + fqn = f"cheetahclaws.modular.{mod_dir.name}.cmd" try: mod = importlib.import_module(fqn) for cmd_name, cmd_def in getattr(mod, "COMMAND_DEFS", {}).items(): @@ -83,7 +83,7 @@ def load_all_tools() -> list: tools_path = mod_dir / "tools.py" if not tools_path.exists(): continue - fqn = f"modular.{mod_dir.name}.tools" + fqn = f"cheetahclaws.modular.{mod_dir.name}.tools" try: mod = importlib.import_module(fqn) result.extend(getattr(mod, "TOOL_DEFS", [])) @@ -99,7 +99,7 @@ def list_modules() -> list[dict]: info: dict = {"name": mod_dir.name, "has_cmd": False, "has_tools": False} if (mod_dir / "cmd.py").exists(): info["has_cmd"] = True - fqn = f"modular.{mod_dir.name}.cmd" + fqn = f"cheetahclaws.modular.{mod_dir.name}.cmd" try: mod = importlib.import_module(fqn) info["commands"] = list(getattr(mod, "COMMAND_DEFS", {}).keys()) diff --git a/modular/base.py b/cheetahclaws/modular/base.py similarity index 100% rename from modular/base.py rename to cheetahclaws/modular/base.py diff --git a/modular/trading/PLUGIN.md b/cheetahclaws/modular/trading/PLUGIN.md similarity index 100% rename from modular/trading/PLUGIN.md rename to cheetahclaws/modular/trading/PLUGIN.md diff --git a/modular/trading/__init__.py b/cheetahclaws/modular/trading/__init__.py similarity index 100% rename from modular/trading/__init__.py rename to cheetahclaws/modular/trading/__init__.py diff --git a/modular/trading/agent_templates/trading_agent.md b/cheetahclaws/modular/trading/agent_templates/trading_agent.md similarity index 100% rename from modular/trading/agent_templates/trading_agent.md rename to cheetahclaws/modular/trading/agent_templates/trading_agent.md diff --git a/modular/trading/agents/__init__.py b/cheetahclaws/modular/trading/agents/__init__.py similarity index 100% rename from modular/trading/agents/__init__.py rename to cheetahclaws/modular/trading/agents/__init__.py diff --git a/modular/trading/agents/analyst.py b/cheetahclaws/modular/trading/agents/analyst.py similarity index 100% rename from modular/trading/agents/analyst.py rename to cheetahclaws/modular/trading/agents/analyst.py diff --git a/modular/trading/agents/memory.py b/cheetahclaws/modular/trading/agents/memory.py similarity index 100% rename from modular/trading/agents/memory.py rename to cheetahclaws/modular/trading/agents/memory.py diff --git a/modular/trading/agents/portfolio_manager.py b/cheetahclaws/modular/trading/agents/portfolio_manager.py similarity index 100% rename from modular/trading/agents/portfolio_manager.py rename to cheetahclaws/modular/trading/agents/portfolio_manager.py diff --git a/modular/trading/agents/reflection.py b/cheetahclaws/modular/trading/agents/reflection.py similarity index 100% rename from modular/trading/agents/reflection.py rename to cheetahclaws/modular/trading/agents/reflection.py diff --git a/modular/trading/agents/researcher.py b/cheetahclaws/modular/trading/agents/researcher.py similarity index 100% rename from modular/trading/agents/researcher.py rename to cheetahclaws/modular/trading/agents/researcher.py diff --git a/modular/trading/agents/risk_manager.py b/cheetahclaws/modular/trading/agents/risk_manager.py similarity index 100% rename from modular/trading/agents/risk_manager.py rename to cheetahclaws/modular/trading/agents/risk_manager.py diff --git a/modular/trading/alt_data/__init__.py b/cheetahclaws/modular/trading/alt_data/__init__.py similarity index 100% rename from modular/trading/alt_data/__init__.py rename to cheetahclaws/modular/trading/alt_data/__init__.py diff --git a/modular/trading/alt_data/insider.py b/cheetahclaws/modular/trading/alt_data/insider.py similarity index 100% rename from modular/trading/alt_data/insider.py rename to cheetahclaws/modular/trading/alt_data/insider.py diff --git a/modular/trading/alt_data/sentiment.py b/cheetahclaws/modular/trading/alt_data/sentiment.py similarity index 98% rename from modular/trading/alt_data/sentiment.py rename to cheetahclaws/modular/trading/alt_data/sentiment.py index d0fe5bf7..0ee6cc0e 100644 --- a/modular/trading/alt_data/sentiment.py +++ b/cheetahclaws/modular/trading/alt_data/sentiment.py @@ -57,7 +57,7 @@ def _score_with_aux_model(symbol: str, headlines: list[dict[str, Any]]) -> dict[ ) try: - from auxiliary import stream_auxiliary + from cheetahclaws.auxiliary import stream_auxiliary except ImportError: return {} diff --git a/modular/trading/alt_data/trends.py b/cheetahclaws/modular/trading/alt_data/trends.py similarity index 100% rename from modular/trading/alt_data/trends.py rename to cheetahclaws/modular/trading/alt_data/trends.py diff --git a/modular/trading/broker/__init__.py b/cheetahclaws/modular/trading/broker/__init__.py similarity index 100% rename from modular/trading/broker/__init__.py rename to cheetahclaws/modular/trading/broker/__init__.py diff --git a/modular/trading/broker/base.py b/cheetahclaws/modular/trading/broker/base.py similarity index 100% rename from modular/trading/broker/base.py rename to cheetahclaws/modular/trading/broker/base.py diff --git a/modular/trading/broker/ibkr_backend.py b/cheetahclaws/modular/trading/broker/ibkr_backend.py similarity index 100% rename from modular/trading/broker/ibkr_backend.py rename to cheetahclaws/modular/trading/broker/ibkr_backend.py diff --git a/modular/trading/broker/paper_backend.py b/cheetahclaws/modular/trading/broker/paper_backend.py similarity index 100% rename from modular/trading/broker/paper_backend.py rename to cheetahclaws/modular/trading/broker/paper_backend.py diff --git a/modular/trading/calibration.py b/cheetahclaws/modular/trading/calibration.py similarity index 100% rename from modular/trading/calibration.py rename to cheetahclaws/modular/trading/calibration.py diff --git a/modular/trading/cmd.py b/cheetahclaws/modular/trading/cmd.py similarity index 99% rename from modular/trading/cmd.py rename to cheetahclaws/modular/trading/cmd.py index 1492fc5e..6f52e93a 100644 --- a/modular/trading/cmd.py +++ b/cheetahclaws/modular/trading/cmd.py @@ -16,7 +16,7 @@ from datetime import datetime from pathlib import Path -from ui.render import info, ok, warn, err, clr +from cheetahclaws.ui.render import info, ok, warn, err, clr # ── History storage ──────────────────────────────────────────────────────── diff --git a/modular/trading/data/__init__.py b/cheetahclaws/modular/trading/data/__init__.py similarity index 100% rename from modular/trading/data/__init__.py rename to cheetahclaws/modular/trading/data/__init__.py diff --git a/modular/trading/data/fetchers.py b/cheetahclaws/modular/trading/data/fetchers.py similarity index 100% rename from modular/trading/data/fetchers.py rename to cheetahclaws/modular/trading/data/fetchers.py diff --git a/modular/trading/data/indicators.py b/cheetahclaws/modular/trading/data/indicators.py similarity index 100% rename from modular/trading/data/indicators.py rename to cheetahclaws/modular/trading/data/indicators.py diff --git a/modular/trading/discover/__init__.py b/cheetahclaws/modular/trading/discover/__init__.py similarity index 100% rename from modular/trading/discover/__init__.py rename to cheetahclaws/modular/trading/discover/__init__.py diff --git a/modular/trading/discover/anomaly.py b/cheetahclaws/modular/trading/discover/anomaly.py similarity index 100% rename from modular/trading/discover/anomaly.py rename to cheetahclaws/modular/trading/discover/anomaly.py diff --git a/modular/trading/discover/earnings_beat.py b/cheetahclaws/modular/trading/discover/earnings_beat.py similarity index 100% rename from modular/trading/discover/earnings_beat.py rename to cheetahclaws/modular/trading/discover/earnings_beat.py diff --git a/modular/trading/discover/insider_cluster.py b/cheetahclaws/modular/trading/discover/insider_cluster.py similarity index 100% rename from modular/trading/discover/insider_cluster.py rename to cheetahclaws/modular/trading/discover/insider_cluster.py diff --git a/modular/trading/discover/momentum_quality.py b/cheetahclaws/modular/trading/discover/momentum_quality.py similarity index 100% rename from modular/trading/discover/momentum_quality.py rename to cheetahclaws/modular/trading/discover/momentum_quality.py diff --git a/modular/trading/discover/orchestrator.py b/cheetahclaws/modular/trading/discover/orchestrator.py similarity index 100% rename from modular/trading/discover/orchestrator.py rename to cheetahclaws/modular/trading/discover/orchestrator.py diff --git a/modular/trading/discover/sector_rotation.py b/cheetahclaws/modular/trading/discover/sector_rotation.py similarity index 100% rename from modular/trading/discover/sector_rotation.py rename to cheetahclaws/modular/trading/discover/sector_rotation.py diff --git a/modular/trading/discover/types.py b/cheetahclaws/modular/trading/discover/types.py similarity index 100% rename from modular/trading/discover/types.py rename to cheetahclaws/modular/trading/discover/types.py diff --git a/modular/trading/earnings.py b/cheetahclaws/modular/trading/earnings.py similarity index 100% rename from modular/trading/earnings.py rename to cheetahclaws/modular/trading/earnings.py diff --git a/modular/trading/engines/__init__.py b/cheetahclaws/modular/trading/engines/__init__.py similarity index 100% rename from modular/trading/engines/__init__.py rename to cheetahclaws/modular/trading/engines/__init__.py diff --git a/modular/trading/engines/base.py b/cheetahclaws/modular/trading/engines/base.py similarity index 100% rename from modular/trading/engines/base.py rename to cheetahclaws/modular/trading/engines/base.py diff --git a/modular/trading/engines/crypto.py b/cheetahclaws/modular/trading/engines/crypto.py similarity index 100% rename from modular/trading/engines/crypto.py rename to cheetahclaws/modular/trading/engines/crypto.py diff --git a/modular/trading/engines/equity.py b/cheetahclaws/modular/trading/engines/equity.py similarity index 100% rename from modular/trading/engines/equity.py rename to cheetahclaws/modular/trading/engines/equity.py diff --git a/modular/trading/factors.py b/cheetahclaws/modular/trading/factors.py similarity index 100% rename from modular/trading/factors.py rename to cheetahclaws/modular/trading/factors.py diff --git a/modular/trading/macro.py b/cheetahclaws/modular/trading/macro.py similarity index 100% rename from modular/trading/macro.py rename to cheetahclaws/modular/trading/macro.py diff --git a/modular/trading/managed.py b/cheetahclaws/modular/trading/managed.py similarity index 100% rename from modular/trading/managed.py rename to cheetahclaws/modular/trading/managed.py diff --git a/modular/trading/ml/__init__.py b/cheetahclaws/modular/trading/ml/__init__.py similarity index 100% rename from modular/trading/ml/__init__.py rename to cheetahclaws/modular/trading/ml/__init__.py diff --git a/modular/trading/ml/features.py b/cheetahclaws/modular/trading/ml/features.py similarity index 100% rename from modular/trading/ml/features.py rename to cheetahclaws/modular/trading/ml/features.py diff --git a/modular/trading/ml/stacker.py b/cheetahclaws/modular/trading/ml/stacker.py similarity index 100% rename from modular/trading/ml/stacker.py rename to cheetahclaws/modular/trading/ml/stacker.py diff --git a/modular/trading/monitor.py b/cheetahclaws/modular/trading/monitor.py similarity index 98% rename from modular/trading/monitor.py rename to cheetahclaws/modular/trading/monitor.py index da22b07b..bce0ecd7 100644 --- a/modular/trading/monitor.py +++ b/cheetahclaws/modular/trading/monitor.py @@ -285,7 +285,7 @@ def dispatch_to_bridges(alerts: list[Alert], for ch in bridges: try: if ch == "telegram": - from bridges import telegram as tg + from cheetahclaws.bridges import telegram as tg token = config.get("telegram_token") chat = config.get("telegram_chat_id") if token and chat: @@ -293,7 +293,7 @@ def dispatch_to_bridges(alerts: list[Alert], out["sent"] += 1 out["channels"].append("telegram") elif ch == "slack": - from bridges import slack as sl + from cheetahclaws.bridges import slack as sl token = config.get("slack_token") ch_id = config.get("slack_channel") if token and ch_id: @@ -301,7 +301,7 @@ def dispatch_to_bridges(alerts: list[Alert], out["sent"] += 1 out["channels"].append("slack") elif ch == "wechat": - from bridges import wechat as wc + from cheetahclaws.bridges import wechat as wc if config.get("wechat_token"): # Push to filehelper as a self-message if hasattr(wc, "_wx_send"): diff --git a/modular/trading/paper_trader.py b/cheetahclaws/modular/trading/paper_trader.py similarity index 100% rename from modular/trading/paper_trader.py rename to cheetahclaws/modular/trading/paper_trader.py diff --git a/modular/trading/portfolio.py b/cheetahclaws/modular/trading/portfolio.py similarity index 100% rename from modular/trading/portfolio.py rename to cheetahclaws/modular/trading/portfolio.py diff --git a/modular/trading/ranker.py b/cheetahclaws/modular/trading/ranker.py similarity index 100% rename from modular/trading/ranker.py rename to cheetahclaws/modular/trading/ranker.py diff --git a/modular/trading/skills/analyze_market.md b/cheetahclaws/modular/trading/skills/analyze_market.md similarity index 100% rename from modular/trading/skills/analyze_market.md rename to cheetahclaws/modular/trading/skills/analyze_market.md diff --git a/modular/trading/skills/backtest_strategy.md b/cheetahclaws/modular/trading/skills/backtest_strategy.md similarity index 100% rename from modular/trading/skills/backtest_strategy.md rename to cheetahclaws/modular/trading/skills/backtest_strategy.md diff --git a/modular/trading/skills/generate_strategy.md b/cheetahclaws/modular/trading/skills/generate_strategy.md similarity index 100% rename from modular/trading/skills/generate_strategy.md rename to cheetahclaws/modular/trading/skills/generate_strategy.md diff --git a/modular/trading/tools.py b/cheetahclaws/modular/trading/tools.py similarity index 99% rename from modular/trading/tools.py rename to cheetahclaws/modular/trading/tools.py index 2df9671a..a1fe9263 100644 --- a/modular/trading/tools.py +++ b/cheetahclaws/modular/trading/tools.py @@ -8,7 +8,7 @@ from __future__ import annotations import json -from tool_registry import ToolDef +from cheetahclaws.tool_registry import ToolDef from .data.fetchers import fetch_market_data, fetch_current_price, fetch_fundamentals, fetch_news from .data.indicators import compute_all, format_indicators_report diff --git a/modular/trading/universe.py b/cheetahclaws/modular/trading/universe.py similarity index 100% rename from modular/trading/universe.py rename to cheetahclaws/modular/trading/universe.py diff --git a/modular/trading/verifier.py b/cheetahclaws/modular/trading/verifier.py similarity index 100% rename from modular/trading/verifier.py rename to cheetahclaws/modular/trading/verifier.py diff --git a/modular/video/PLUGIN.md b/cheetahclaws/modular/video/PLUGIN.md similarity index 100% rename from modular/video/PLUGIN.md rename to cheetahclaws/modular/video/PLUGIN.md diff --git a/modular/video/__init__.py b/cheetahclaws/modular/video/__init__.py similarity index 100% rename from modular/video/__init__.py rename to cheetahclaws/modular/video/__init__.py diff --git a/modular/video/assembly.py b/cheetahclaws/modular/video/assembly.py similarity index 100% rename from modular/video/assembly.py rename to cheetahclaws/modular/video/assembly.py diff --git a/modular/video/cmd.py b/cheetahclaws/modular/video/cmd.py similarity index 99% rename from modular/video/cmd.py rename to cheetahclaws/modular/video/cmd.py index 4f420700..25273396 100644 --- a/modular/video/cmd.py +++ b/cheetahclaws/modular/video/cmd.py @@ -32,7 +32,7 @@ def _err(msg: str): print(_clr(f"Error: {msg}", "red"), file=sys.stderr) def _ask(prompt: str, config) -> str: """Thin wrapper: use ask_input_interactive from tools if available.""" try: - from tools import ask_input_interactive + from cheetahclaws.tools import ask_input_interactive return ask_input_interactive(prompt, config) except ImportError: import re as _re @@ -152,7 +152,7 @@ def cmd_video(args: str, _state, config) -> bool: topic_parts.append(tokens[i]); i += 1 topic_from_args = " ".join(topic_parts).strip() - from tools import _is_in_tg_turn + from cheetahclaws.tools import _is_in_tg_turn is_tg = _is_in_tg_turn(config) # ════════════════════════════════════════════════════════════════════════════ diff --git a/modular/video/images.py b/cheetahclaws/modular/video/images.py similarity index 99% rename from modular/video/images.py rename to cheetahclaws/modular/video/images.py index 182f3b27..bc32dddd 100644 --- a/modular/video/images.py +++ b/cheetahclaws/modular/video/images.py @@ -192,7 +192,7 @@ def _ai_search_query(image_prompt: str, story_context: str, Returns '+'-joined keywords, or "" on failure. """ try: - from providers import stream, TextChunk # type: ignore + from cheetahclaws.providers import stream, TextChunk # type: ignore prompt = ( f"Story context: {story_context[:300]}\n" f"Scene to illustrate: {image_prompt[:200]}\n\n" diff --git a/modular/video/niches.py b/cheetahclaws/modular/video/niches.py similarity index 100% rename from modular/video/niches.py rename to cheetahclaws/modular/video/niches.py diff --git a/modular/video/pipeline.py b/cheetahclaws/modular/video/pipeline.py similarity index 100% rename from modular/video/pipeline.py rename to cheetahclaws/modular/video/pipeline.py diff --git a/modular/video/source.py b/cheetahclaws/modular/video/source.py similarity index 99% rename from modular/video/source.py rename to cheetahclaws/modular/video/source.py index 11c4452b..8ed44495 100644 --- a/modular/video/source.py +++ b/cheetahclaws/modular/video/source.py @@ -146,7 +146,7 @@ def _model_select_images(images: list[str], story_data: dict, n: int, model: str, config: dict) -> list[str]: """Use the LLM to select the most story-relevant images by filename.""" try: - from providers import stream, TextChunk # type: ignore + from cheetahclaws.providers import stream, TextChunk # type: ignore filenames = [os.path.basename(p) for p in images] story_brief = (story_data.get('title', '') + '\n\n' + story_data.get('story', '')[:400]) diff --git a/modular/video/story.py b/cheetahclaws/modular/video/story.py similarity index 98% rename from modular/video/story.py rename to cheetahclaws/modular/video/story.py index eca3ad0d..e53394b4 100644 --- a/modular/video/story.py +++ b/cheetahclaws/modular/video/story.py @@ -23,7 +23,7 @@ def generate_story(topic: str, model: str, config: dict, image_prompts: list of {'prompt': str, 'timestamp': str|None, 'seconds': int|None} sfx_cues: list of {'timestamp': str, 'seconds': int, 'name': str} """ - from providers import stream, TextChunk # type: ignore + from cheetahclaws.providers import stream, TextChunk # type: ignore niche_id, niche = select_niche(niche_name) print(f" Target niche: {niche['nombre']}") @@ -156,7 +156,7 @@ def _story_too_short(result: dict) -> bool: def _stream_text(model, system, prompt, config) -> str: """Helper: stream a model call and return the full response string.""" - from providers import stream, TextChunk # type: ignore + from cheetahclaws.providers import stream, TextChunk # type: ignore chunks: list[str] = [] try: for event in stream(model, system, [{"role": "user", "content": prompt}], [], config): diff --git a/modular/video/subtitles.py b/cheetahclaws/modular/video/subtitles.py similarity index 100% rename from modular/video/subtitles.py rename to cheetahclaws/modular/video/subtitles.py diff --git a/modular/video/tts.py b/cheetahclaws/modular/video/tts.py similarity index 100% rename from modular/video/tts.py rename to cheetahclaws/modular/video/tts.py diff --git a/modular/voice/PLUGIN.md b/cheetahclaws/modular/voice/PLUGIN.md similarity index 100% rename from modular/voice/PLUGIN.md rename to cheetahclaws/modular/voice/PLUGIN.md diff --git a/modular/voice/__init__.py b/cheetahclaws/modular/voice/__init__.py similarity index 100% rename from modular/voice/__init__.py rename to cheetahclaws/modular/voice/__init__.py diff --git a/modular/voice/cmd.py b/cheetahclaws/modular/voice/cmd.py similarity index 97% rename from modular/voice/cmd.py rename to cheetahclaws/modular/voice/cmd.py index c4efb61d..232b1a7f 100644 --- a/modular/voice/cmd.py +++ b/cheetahclaws/modular/voice/cmd.py @@ -26,7 +26,7 @@ def _err(msg: str): print(_clr(f"Error: {msg}", "red"), file=sys.stderr) def _ask(prompt: str, config) -> str: try: - from tools import ask_input_interactive + from cheetahclaws.tools import ask_input_interactive return ask_input_interactive(prompt, config) except ImportError: import re as _re @@ -46,7 +46,7 @@ def cmd_voice(args: str, state, config): /voice lang — set STT language (e.g. zh, en, ja; 'auto' to reset) /voice device — list and select input microphone """ - import runtime as _rt + from cheetahclaws import runtime as _rt global _voice_language subcmd = args.strip().lower().split()[0] if args.strip() else "" @@ -55,7 +55,7 @@ def cmd_voice(args: str, state, config): # ── /voice device ────────────────────────────────────────────────────────── if subcmd == "device": try: - from modular.voice import list_input_devices + from cheetahclaws.modular.voice import list_input_devices except ImportError: _err("sounddevice not available. Install with: pip install sounddevice") return True @@ -96,10 +96,10 @@ def cmd_voice(args: str, state, config): # ── /voice status ────────────────────────────────────────────────────────── if subcmd == "status": try: - from modular.voice import ( + from cheetahclaws.modular.voice import ( check_voice_deps, check_recording_availability, check_stt_availability ) - from modular.voice.stt import get_stt_backend_name + from cheetahclaws.modular.voice.stt import get_stt_backend_name except ImportError as e: _err(f"voice package not available: {e}") return True @@ -119,7 +119,7 @@ def cmd_voice(args: str, state, config): dev_idx = _rt.get_ctx(config).voice_device_index if dev_idx is not None: try: - from modular.voice import list_input_devices + from cheetahclaws.modular.voice import list_input_devices devs = list_input_devices() dev_name = next((d["name"] for d in devs if d["index"] == dev_idx), f"#{dev_idx}") except Exception: @@ -133,7 +133,7 @@ def cmd_voice(args: str, state, config): # ── /voice [start] — record once and submit ──────────────────────────────── try: - from modular.voice import check_voice_deps, voice_input as _voice_input + from cheetahclaws.modular.voice import check_voice_deps, voice_input as _voice_input except ImportError: _err("voice/ package not found") return True @@ -210,7 +210,7 @@ def cmd_tts(args: str, _state, config) -> bool: /tts status Show dependency status """ import os as _os - from modular.voice.tts_gen import ( + from cheetahclaws.modular.voice.tts_gen import ( VOICE_STYLES, EDGE_VOICES, GEMINI_VOICES, check_tts_deps, run_tts_pipeline, ) diff --git a/modular/voice/keyterms.py b/cheetahclaws/modular/voice/keyterms.py similarity index 100% rename from modular/voice/keyterms.py rename to cheetahclaws/modular/voice/keyterms.py diff --git a/modular/voice/recorder.py b/cheetahclaws/modular/voice/recorder.py similarity index 100% rename from modular/voice/recorder.py rename to cheetahclaws/modular/voice/recorder.py diff --git a/modular/voice/stt.py b/cheetahclaws/modular/voice/stt.py similarity index 100% rename from modular/voice/stt.py rename to cheetahclaws/modular/voice/stt.py diff --git a/modular/voice/tts_gen.py b/cheetahclaws/modular/voice/tts_gen.py similarity index 99% rename from modular/voice/tts_gen.py rename to cheetahclaws/modular/voice/tts_gen.py index 2d6b4c88..99cf2b81 100644 --- a/modular/voice/tts_gen.py +++ b/cheetahclaws/modular/voice/tts_gen.py @@ -172,7 +172,7 @@ def generate_tts_text( Returns the generated text string. """ - from providers import stream + from cheetahclaws.providers import stream style = VOICE_STYLES.get(style_key, VOICE_STYLES["narrator"]) style_instr = custom_style_prompt if style_key == "custom" else style["prompt"] @@ -218,7 +218,7 @@ def create_tts_audio( Returns True on success. """ - from modular.video.tts import generate_audio + from cheetahclaws.modular.video.tts import generate_audio return generate_audio( text, output_path, diff --git a/monitor/__init__.py b/cheetahclaws/monitor/__init__.py similarity index 100% rename from monitor/__init__.py rename to cheetahclaws/monitor/__init__.py diff --git a/monitor/fetchers.py b/cheetahclaws/monitor/fetchers.py similarity index 98% rename from monitor/fetchers.py rename to cheetahclaws/monitor/fetchers.py index 7f1aa548..a5650d5e 100644 --- a/monitor/fetchers.py +++ b/cheetahclaws/monitor/fetchers.py @@ -372,8 +372,8 @@ def fetch_research(query: str, time_range_token: str = "7d") -> dict: weekly covers the week's new material). """ try: - from research import research, build_time_range - from research.synthesizer import ( + from cheetahclaws.research import research, build_time_range + from cheetahclaws.research.synthesizer import ( format_heat_table, format_publication_trend, format_publication_sparkline, render_citations, ) @@ -448,7 +448,7 @@ def fetch(topic: str) -> dict: # research: OR research:: (e.g. research:30d:LLM) body = topic[9:] maybe_range, _, rest = body.partition(":") - from research.time_range import _PRESET_DAYS + from cheetahclaws.research.time_range import _PRESET_DAYS if maybe_range in _PRESET_DAYS and rest: data = fetch_research(rest, time_range_token=maybe_range) else: diff --git a/monitor/notifier.py b/cheetahclaws/monitor/notifier.py similarity index 95% rename from monitor/notifier.py rename to cheetahclaws/monitor/notifier.py index ab1359be..a90c707b 100644 --- a/monitor/notifier.py +++ b/cheetahclaws/monitor/notifier.py @@ -21,7 +21,7 @@ def _send_telegram(text: str, config: dict) -> str | None: if not token or not chat_id: return "Telegram not configured (set monitor_telegram_token + monitor_telegram_chat_id)" try: - from bridges.telegram import _tg_send + from cheetahclaws.bridges.telegram import _tg_send _tg_send(token, int(chat_id), text) return None except Exception as e: @@ -35,7 +35,7 @@ def _send_slack(text: str, config: dict) -> str | None: if not token or not channel: return "Slack not configured (set monitor_slack_token + monitor_slack_channel)" try: - from bridges.slack import _slack_send + from cheetahclaws.bridges.slack import _slack_send _slack_send(token, channel, text) return None except Exception as e: diff --git a/monitor/scheduler.py b/cheetahclaws/monitor/scheduler.py similarity index 95% rename from monitor/scheduler.py rename to cheetahclaws/monitor/scheduler.py index ab2d17b0..a3aeecfd 100644 --- a/monitor/scheduler.py +++ b/cheetahclaws/monitor/scheduler.py @@ -20,12 +20,12 @@ from datetime import datetime, timedelta from typing import Callable -from monitor.store import ( +from cheetahclaws.monitor.store import ( list_subscriptions, update_last_run, save_report, ) -from monitor.fetchers import fetch -from monitor.summarizer import summarize -from monitor.notifier import deliver, auto_channels +from cheetahclaws.monitor.fetchers import fetch +from cheetahclaws.monitor.summarizer import summarize +from cheetahclaws.monitor.notifier import deliver, auto_channels _scheduler_thread: threading.Thread | None = None _scheduler_stop = threading.Event() @@ -49,7 +49,7 @@ def _foreign_daemon_running() -> bool: return False try: import os - from daemon import discovery + from cheetahclaws.daemon import discovery info = discovery.locate() if info is None: return False @@ -140,7 +140,7 @@ def run_one(topic: str, config: dict, force: bool = False) -> str: except Exception: pass try: - from daemon import events as _events + from cheetahclaws.daemon import events as _events _events.get_bus().publish( "monitor_report", { diff --git a/monitor/store.py b/cheetahclaws/monitor/store.py similarity index 99% rename from monitor/store.py rename to cheetahclaws/monitor/store.py index 3e6e4a90..dda4e1d4 100644 --- a/monitor/store.py +++ b/cheetahclaws/monitor/store.py @@ -31,7 +31,7 @@ def _conn(): - from daemon.schema import get_conn + from cheetahclaws.daemon.schema import get_conn return get_conn() diff --git a/monitor/summarizer.py b/cheetahclaws/monitor/summarizer.py similarity index 98% rename from monitor/summarizer.py rename to cheetahclaws/monitor/summarizer.py index f09b10ee..8bad85e5 100644 --- a/monitor/summarizer.py +++ b/cheetahclaws/monitor/summarizer.py @@ -88,7 +88,7 @@ def summarize(raw: dict, config: dict) -> str: # Try AI summarization try: - from providers import stream, AssistantTurn, TextChunk + from cheetahclaws.providers import stream, AssistantTurn, TextChunk system = _system_prompt_for(topic) messages = [{"role": "user", "content": data_text}] diff --git a/multi_agent/__init__.py b/cheetahclaws/multi_agent/__init__.py similarity index 100% rename from multi_agent/__init__.py rename to cheetahclaws/multi_agent/__init__.py diff --git a/multi_agent/fanout.py b/cheetahclaws/multi_agent/fanout.py similarity index 99% rename from multi_agent/fanout.py rename to cheetahclaws/multi_agent/fanout.py index 9912f77b..fca38735 100644 --- a/multi_agent/fanout.py +++ b/cheetahclaws/multi_agent/fanout.py @@ -231,7 +231,7 @@ def make_llm_caller(config: dict) -> Callable[[str, str], str]: provider/model as the parent agent. Tools are disabled for the sub-call so the response is plain text and one round-trip — no nested tool loops. """ - import providers + from cheetahclaws import providers model = config.get("model", "") provider = providers.detect_provider(model) diff --git a/multi_agent/subagent.py b/cheetahclaws/multi_agent/subagent.py similarity index 99% rename from multi_agent/subagent.py rename to cheetahclaws/multi_agent/subagent.py index 32e08021..ab3dcc02 100644 --- a/multi_agent/subagent.py +++ b/cheetahclaws/multi_agent/subagent.py @@ -261,7 +261,7 @@ def _agent_run(prompt, state, config, system_prompt, depth=0, cancel_check=None) Uses absolute import so this works whether called from inside or outside the multi_agent package (sys.path includes the project root). """ - import agent as _agent_mod + from cheetahclaws import agent as _agent_mod return _agent_mod.run(prompt, state, config, system_prompt, depth=depth, cancel_check=cancel_check) @@ -363,7 +363,7 @@ def spawn( return task def _run(): - import agent as _agent_mod; AgentState = _agent_mod.AgentState + from cheetahclaws import agent as _agent_mod; AgentState = _agent_mod.AgentState task.status = "running" try: state = AgentState() diff --git a/multi_agent/tools.py b/cheetahclaws/multi_agent/tools.py similarity index 99% rename from multi_agent/tools.py rename to cheetahclaws/multi_agent/tools.py index cfa10c7a..ac4eee36 100644 --- a/multi_agent/tools.py +++ b/cheetahclaws/multi_agent/tools.py @@ -9,7 +9,7 @@ """ from __future__ import annotations -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool from .subagent import SubAgentManager, get_agent_definition, load_agent_definitions diff --git a/plugin/__init__.py b/cheetahclaws/plugin/__init__.py similarity index 100% rename from plugin/__init__.py rename to cheetahclaws/plugin/__init__.py diff --git a/plugin/loader.py b/cheetahclaws/plugin/loader.py similarity index 99% rename from plugin/loader.py rename to cheetahclaws/plugin/loader.py index 4a0b846f..9b117722 100644 --- a/plugin/loader.py +++ b/cheetahclaws/plugin/loader.py @@ -58,7 +58,7 @@ def register_plugin_tools(scope: PluginScope | None = None) -> int: Import tool modules from enabled plugins and register them into tool_registry. Returns number of tools registered. """ - from tool_registry import register_tool, ToolDef + from cheetahclaws.tool_registry import register_tool, ToolDef count = 0 for entry in load_all_plugins(scope): if not entry.manifest or not entry.manifest.tools: diff --git a/plugin/recommend.py b/cheetahclaws/plugin/recommend.py similarity index 100% rename from plugin/recommend.py rename to cheetahclaws/plugin/recommend.py diff --git a/plugin/store.py b/cheetahclaws/plugin/store.py similarity index 100% rename from plugin/store.py rename to cheetahclaws/plugin/store.py diff --git a/plugin/types.py b/cheetahclaws/plugin/types.py similarity index 100% rename from plugin/types.py rename to cheetahclaws/plugin/types.py diff --git a/prompts/README.md b/cheetahclaws/prompts/README.md similarity index 100% rename from prompts/README.md rename to cheetahclaws/prompts/README.md diff --git a/prompts/__init__.py b/cheetahclaws/prompts/__init__.py similarity index 90% rename from prompts/__init__.py rename to cheetahclaws/prompts/__init__.py index 9b35144d..af56e5bf 100644 --- a/prompts/__init__.py +++ b/cheetahclaws/prompts/__init__.py @@ -15,6 +15,6 @@ Selection logic is in :mod:`prompts.select`. Callers should not read .md files directly — always go through ``pick_base_prompt`` / ``load_fragment``. """ -from prompts.select import pick_base_prompt, load_fragment # noqa: F401 +from cheetahclaws.prompts.select import pick_base_prompt, load_fragment # noqa: F401 __all__ = ["pick_base_prompt", "load_fragment"] diff --git a/prompts/base/default.md b/cheetahclaws/prompts/base/default.md similarity index 100% rename from prompts/base/default.md rename to cheetahclaws/prompts/base/default.md diff --git a/prompts/fragments/plan.md b/cheetahclaws/prompts/fragments/plan.md similarity index 100% rename from prompts/fragments/plan.md rename to cheetahclaws/prompts/fragments/plan.md diff --git a/prompts/fragments/tmux.md b/cheetahclaws/prompts/fragments/tmux.md similarity index 100% rename from prompts/fragments/tmux.md rename to cheetahclaws/prompts/fragments/tmux.md diff --git a/prompts/overlays/claude.md b/cheetahclaws/prompts/overlays/claude.md similarity index 100% rename from prompts/overlays/claude.md rename to cheetahclaws/prompts/overlays/claude.md diff --git a/prompts/overlays/gemini.md b/cheetahclaws/prompts/overlays/gemini.md similarity index 100% rename from prompts/overlays/gemini.md rename to cheetahclaws/prompts/overlays/gemini.md diff --git a/prompts/overlays/openai-reasoning.md b/cheetahclaws/prompts/overlays/openai-reasoning.md similarity index 100% rename from prompts/overlays/openai-reasoning.md rename to cheetahclaws/prompts/overlays/openai-reasoning.md diff --git a/prompts/overlays/qwen.md b/cheetahclaws/prompts/overlays/qwen.md similarity index 100% rename from prompts/overlays/qwen.md rename to cheetahclaws/prompts/overlays/qwen.md diff --git a/prompts/select.py b/cheetahclaws/prompts/select.py similarity index 100% rename from prompts/select.py rename to cheetahclaws/prompts/select.py diff --git a/providers.py b/cheetahclaws/providers.py similarity index 99% rename from providers.py rename to cheetahclaws/providers.py index 58987973..f6d39744 100644 --- a/providers.py +++ b/cheetahclaws/providers.py @@ -490,7 +490,7 @@ def dynamic_cap_max_tokens( should already have fired; returning a tiny floor lets the API call surface a clear error rather than silently sending an oversized request. """ - import compaction # local import: compaction imports providers, avoid cycle + from cheetahclaws import compaction # local import: compaction imports providers, avoid cycle msg_tok = compaction.estimate_tokens(messages or []) sys_tok = 0 if isinstance(system, str): @@ -1574,8 +1574,8 @@ def stream( - Circuit breaker: fails fast when a provider has repeated errors. - Structured logging: logs api_call_start / api_call_done / api_call_error. """ - import logging_utils as _log - import circuit_breaker as _cb + from cheetahclaws import logging_utils as _log + from cheetahclaws import circuit_breaker as _cb provider_name = detect_provider(model) model_name = bare_model(model) diff --git a/quota.py b/cheetahclaws/quota.py similarity index 98% rename from quota.py rename to cheetahclaws/quota.py index 62579474..344889e9 100644 --- a/quota.py +++ b/cheetahclaws/quota.py @@ -47,7 +47,7 @@ def __init__(self, reason: str, *, key=None, scope=None, unit=None, limit=None): # ── Daily file helpers ───────────────────────────────────────────────────── def _quota_dir() -> Path: - from config import CONFIG_DIR + from cheetahclaws.config import CONFIG_DIR d = CONFIG_DIR / "quota" d.mkdir(parents=True, exist_ok=True) return d @@ -153,7 +153,7 @@ def output_room(session_id: str, config: dict, if config.get("daily_token_budget"): rooms.append(int(config["daily_token_budget"]) - u["daily_tokens"] - pt) try: - from providers import COSTS, bare_model + from cheetahclaws.providers import COSTS, bare_model _ic, oc = COSTS.get(bare_model(config.get("model", "")), (0.0, 0.0)) except Exception: oc = 0.0 @@ -174,7 +174,7 @@ def record_usage(session_id: str, model: str, in_tokens: int, out_tokens: int) - Record token usage after a successful API call. Updates in-memory session counters and the on-disk daily record. """ - from providers import calc_cost + from cheetahclaws.providers import calc_cost tokens = in_tokens + out_tokens cost = calc_cost(model, in_tokens, out_tokens) @@ -184,7 +184,7 @@ def record_usage(session_id: str, model: str, in_tokens: int, out_tokens: int) - dt, dc = _load_daily() _save_daily(dt + tokens, dc + cost) - import logging_utils as _log + from cheetahclaws import logging_utils as _log _log.info("usage_recorded", session_id=session_id, model=model, diff --git a/research/__init__.py b/cheetahclaws/research/__init__.py similarity index 94% rename from research/__init__.py rename to cheetahclaws/research/__init__.py index 8013d47e..31e18e20 100644 --- a/research/__init__.py +++ b/cheetahclaws/research/__init__.py @@ -1,7 +1,7 @@ """CheetahClaws Research — multi-source topic research with engagement scoring. Public API: - from research import research, Brief, SourceStatus + from cheetahclaws.research import research, Brief, SourceStatus brief = research( topic="transformer inference efficiency", diff --git a/research/aggregator.py b/cheetahclaws/research/aggregator.py similarity index 99% rename from research/aggregator.py rename to cheetahclaws/research/aggregator.py index 4429d905..c4a2881e 100644 --- a/research/aggregator.py +++ b/cheetahclaws/research/aggregator.py @@ -322,7 +322,7 @@ def _expand_subqueries(topic: str, n: int, config: dict) -> list[str]: return [] try: - from providers import stream, TextChunk, AssistantTurn + from cheetahclaws.providers import stream, TextChunk, AssistantTurn except ImportError: return [] diff --git a/research/cache.py b/cheetahclaws/research/cache.py similarity index 100% rename from research/cache.py rename to cheetahclaws/research/cache.py diff --git a/research/citations.py b/cheetahclaws/research/citations.py similarity index 100% rename from research/citations.py rename to cheetahclaws/research/citations.py diff --git a/research/classifier.py b/cheetahclaws/research/classifier.py similarity index 100% rename from research/classifier.py rename to cheetahclaws/research/classifier.py diff --git a/research/entities.py b/cheetahclaws/research/entities.py similarity index 100% rename from research/entities.py rename to cheetahclaws/research/entities.py diff --git a/research/http.py b/cheetahclaws/research/http.py similarity index 100% rename from research/http.py rename to cheetahclaws/research/http.py diff --git a/research/lab/__init__.py b/cheetahclaws/research/lab/__init__.py similarity index 98% rename from research/lab/__init__.py rename to cheetahclaws/research/lab/__init__.py index 12c6d726..a6a9ce4e 100644 --- a/research/lab/__init__.py +++ b/cheetahclaws/research/lab/__init__.py @@ -8,7 +8,7 @@ Public surface (what callers should import): - from research.lab import ( + from cheetahclaws.research.lab import ( LabRun, LabState, Stage, RoleAssignment, orchestrator, storage, verifier, output, ) diff --git a/research/lab/backlog.py b/cheetahclaws/research/lab/backlog.py similarity index 98% rename from research/lab/backlog.py rename to cheetahclaws/research/lab/backlog.py index 102a532d..5e3a4494 100644 --- a/research/lab/backlog.py +++ b/cheetahclaws/research/lab/backlog.py @@ -118,7 +118,7 @@ def run_backlog_worker(*, config: dict, # REPL shows live activity without the user polling /lab status. # Caller can override via config["lab_daemon_silent"]=True. from pathlib import Path as _Path - from ui.render import clr as _clr + from cheetahclaws.ui.render import clr as _clr silent = bool(config.get("lab_daemon_silent", False)) def _stage_pr(stage): @@ -163,7 +163,7 @@ def _stage_pr(stage): run_id = run.state.run_id if not silent: - from research.lab.storage import output_dir_for + from cheetahclaws.research.lab.storage import output_dir_for rec = storage.get_run(run_id) if rec is not None: report_path = output_dir_for( diff --git a/research/lab/convergence.py b/cheetahclaws/research/lab/convergence.py similarity index 100% rename from research/lab/convergence.py rename to cheetahclaws/research/lab/convergence.py diff --git a/research/lab/iterate.py b/cheetahclaws/research/lab/iterate.py similarity index 100% rename from research/lab/iterate.py rename to cheetahclaws/research/lab/iterate.py diff --git a/research/lab/orchestrator.py b/cheetahclaws/research/lab/orchestrator.py similarity index 99% rename from research/lab/orchestrator.py rename to cheetahclaws/research/lab/orchestrator.py index 1b4fc1ba..6696e80a 100644 --- a/research/lab/orchestrator.py +++ b/cheetahclaws/research/lab/orchestrator.py @@ -105,7 +105,7 @@ def _default_call_llm(*, role_name: str, model: str, tests on a network-less CI box don't hang). """ try: - import providers + from cheetahclaws import providers except Exception: return LLMResponse(text=f"[{role_name} stub: providers unavailable]") @@ -130,7 +130,7 @@ def _default_call_llm(*, role_name: str, model: str, t_out = int(getattr(last_turn, "tokens_out", 0) or 0) cost = 0 try: - from config import calc_cost + from cheetahclaws.config import calc_cost cost = int(round(calc_cost(model, t_in, t_out) * 100)) except Exception: pass @@ -431,7 +431,7 @@ def _gather_search_context(run: LabRun, rq: str, *, when we couldn't tell missing keys from a real bug. """ try: - from research.aggregator import research as _research + from cheetahclaws.research.aggregator import research as _research # Bias toward academic + tech, the buckets that matter for survey. # We deliberately don't pass --sources so the classifier can pick # the strongest available ones (arxiv / openalex / semantic_scholar diff --git a/research/lab/output.py b/cheetahclaws/research/lab/output.py similarity index 100% rename from research/lab/output.py rename to cheetahclaws/research/lab/output.py diff --git a/research/lab/resume.py b/cheetahclaws/research/lab/resume.py similarity index 100% rename from research/lab/resume.py rename to cheetahclaws/research/lab/resume.py diff --git a/research/lab/roles.py b/cheetahclaws/research/lab/roles.py similarity index 99% rename from research/lab/roles.py rename to cheetahclaws/research/lab/roles.py index 97f68b7f..6b931eeb 100644 --- a/research/lab/roles.py +++ b/cheetahclaws/research/lab/roles.py @@ -121,7 +121,7 @@ def _default_reviewer_models(config: dict) -> list[str]: def _default_aux_model(config: dict) -> str: """Cheap model for surveyor / questioner / lay_reader (low-stakes role).""" try: - from auxiliary import get_auxiliary_model + from cheetahclaws.auxiliary import get_auxiliary_model return get_auxiliary_model(config) except Exception: return config.get("model", "claude-sonnet-4-6") diff --git a/research/lab/sandbox.py b/cheetahclaws/research/lab/sandbox.py similarity index 100% rename from research/lab/sandbox.py rename to cheetahclaws/research/lab/sandbox.py diff --git a/research/lab/storage.py b/cheetahclaws/research/lab/storage.py similarity index 100% rename from research/lab/storage.py rename to cheetahclaws/research/lab/storage.py diff --git a/research/lab/verifier.py b/cheetahclaws/research/lab/verifier.py similarity index 100% rename from research/lab/verifier.py rename to cheetahclaws/research/lab/verifier.py diff --git a/research/ranker.py b/cheetahclaws/research/ranker.py similarity index 100% rename from research/ranker.py rename to cheetahclaws/research/ranker.py diff --git a/research/reports.py b/cheetahclaws/research/reports.py similarity index 100% rename from research/reports.py rename to cheetahclaws/research/reports.py diff --git a/research/sources/__init__.py b/cheetahclaws/research/sources/__init__.py similarity index 100% rename from research/sources/__init__.py rename to cheetahclaws/research/sources/__init__.py diff --git a/research/sources/alphaxiv.py b/cheetahclaws/research/sources/alphaxiv.py similarity index 100% rename from research/sources/alphaxiv.py rename to cheetahclaws/research/sources/alphaxiv.py diff --git a/research/sources/arxiv.py b/cheetahclaws/research/sources/arxiv.py similarity index 100% rename from research/sources/arxiv.py rename to cheetahclaws/research/sources/arxiv.py diff --git a/research/sources/bilibili.py b/cheetahclaws/research/sources/bilibili.py similarity index 100% rename from research/sources/bilibili.py rename to cheetahclaws/research/sources/bilibili.py diff --git a/research/sources/brave.py b/cheetahclaws/research/sources/brave.py similarity index 100% rename from research/sources/brave.py rename to cheetahclaws/research/sources/brave.py diff --git a/research/sources/github.py b/cheetahclaws/research/sources/github.py similarity index 100% rename from research/sources/github.py rename to cheetahclaws/research/sources/github.py diff --git a/research/sources/google_news.py b/cheetahclaws/research/sources/google_news.py similarity index 100% rename from research/sources/google_news.py rename to cheetahclaws/research/sources/google_news.py diff --git a/research/sources/google_scholar.py b/cheetahclaws/research/sources/google_scholar.py similarity index 100% rename from research/sources/google_scholar.py rename to cheetahclaws/research/sources/google_scholar.py diff --git a/research/sources/hackernews.py b/cheetahclaws/research/sources/hackernews.py similarity index 100% rename from research/sources/hackernews.py rename to cheetahclaws/research/sources/hackernews.py diff --git a/research/sources/huggingface_papers.py b/cheetahclaws/research/sources/huggingface_papers.py similarity index 100% rename from research/sources/huggingface_papers.py rename to cheetahclaws/research/sources/huggingface_papers.py diff --git a/research/sources/openalex.py b/cheetahclaws/research/sources/openalex.py similarity index 100% rename from research/sources/openalex.py rename to cheetahclaws/research/sources/openalex.py diff --git a/research/sources/polymarket.py b/cheetahclaws/research/sources/polymarket.py similarity index 100% rename from research/sources/polymarket.py rename to cheetahclaws/research/sources/polymarket.py diff --git a/research/sources/reddit.py b/cheetahclaws/research/sources/reddit.py similarity index 100% rename from research/sources/reddit.py rename to cheetahclaws/research/sources/reddit.py diff --git a/research/sources/sec_edgar.py b/cheetahclaws/research/sources/sec_edgar.py similarity index 100% rename from research/sources/sec_edgar.py rename to cheetahclaws/research/sources/sec_edgar.py diff --git a/research/sources/semantic_scholar.py b/cheetahclaws/research/sources/semantic_scholar.py similarity index 100% rename from research/sources/semantic_scholar.py rename to cheetahclaws/research/sources/semantic_scholar.py diff --git a/research/sources/stackoverflow.py b/cheetahclaws/research/sources/stackoverflow.py similarity index 100% rename from research/sources/stackoverflow.py rename to cheetahclaws/research/sources/stackoverflow.py diff --git a/research/sources/tavily.py b/cheetahclaws/research/sources/tavily.py similarity index 100% rename from research/sources/tavily.py rename to cheetahclaws/research/sources/tavily.py diff --git a/research/sources/twitter.py b/cheetahclaws/research/sources/twitter.py similarity index 100% rename from research/sources/twitter.py rename to cheetahclaws/research/sources/twitter.py diff --git a/research/sources/weibo.py b/cheetahclaws/research/sources/weibo.py similarity index 100% rename from research/sources/weibo.py rename to cheetahclaws/research/sources/weibo.py diff --git a/research/sources/xiaohongshu.py b/cheetahclaws/research/sources/xiaohongshu.py similarity index 100% rename from research/sources/xiaohongshu.py rename to cheetahclaws/research/sources/xiaohongshu.py diff --git a/research/sources/zhihu.py b/cheetahclaws/research/sources/zhihu.py similarity index 100% rename from research/sources/zhihu.py rename to cheetahclaws/research/sources/zhihu.py diff --git a/research/synthesizer.py b/cheetahclaws/research/synthesizer.py similarity index 99% rename from research/synthesizer.py rename to cheetahclaws/research/synthesizer.py index 36f5de45..5fd8f72b 100644 --- a/research/synthesizer.py +++ b/cheetahclaws/research/synthesizer.py @@ -121,7 +121,7 @@ def synthesize(brief: Brief, config: dict | None = None) -> str: # Stream through the provider try: - from providers import stream, TextChunk, AssistantTurn + from cheetahclaws.providers import stream, TextChunk, AssistantTurn except ImportError: return render_without_llm(brief) @@ -246,7 +246,7 @@ def synthesize_comparison( """ try: - from providers import stream, TextChunk, AssistantTurn + from cheetahclaws.providers import stream, TextChunk, AssistantTurn except ImportError: return render_compare_fallback(topics, briefs) diff --git a/research/time_range.py b/cheetahclaws/research/time_range.py similarity index 100% rename from research/time_range.py rename to cheetahclaws/research/time_range.py diff --git a/research/types.py b/cheetahclaws/research/types.py similarity index 100% rename from research/types.py rename to cheetahclaws/research/types.py diff --git a/runtime.py b/cheetahclaws/runtime.py similarity index 99% rename from runtime.py rename to cheetahclaws/runtime.py index d91de9c8..0fd8aa07 100644 --- a/runtime.py +++ b/cheetahclaws/runtime.py @@ -20,7 +20,7 @@ from typing import Callable, Optional, TYPE_CHECKING if TYPE_CHECKING: - from agent import AgentState + from cheetahclaws.agent import AgentState @dataclass diff --git a/session_store.py b/cheetahclaws/session_store.py similarity index 99% rename from session_store.py rename to cheetahclaws/session_store.py index 5d912320..8c443b98 100644 --- a/session_store.py +++ b/cheetahclaws/session_store.py @@ -27,7 +27,7 @@ def _get_db_path() -> Path: global _DB_PATH if _DB_PATH is None: - from config import CONFIG_DIR + from cheetahclaws.config import CONFIG_DIR _DB_PATH = CONFIG_DIR / "sessions.db" return _DB_PATH diff --git a/skill/__init__.py b/cheetahclaws/skill/__init__.py similarity index 100% rename from skill/__init__.py rename to cheetahclaws/skill/__init__.py diff --git a/skill/builtin.py b/cheetahclaws/skill/builtin.py similarity index 100% rename from skill/builtin.py rename to cheetahclaws/skill/builtin.py diff --git a/skill/executor.py b/cheetahclaws/skill/executor.py similarity index 95% rename from skill/executor.py rename to cheetahclaws/skill/executor.py index bc15a387..6aa37166 100644 --- a/skill/executor.py +++ b/cheetahclaws/skill/executor.py @@ -38,7 +38,7 @@ def execute_skill( def _execute_inline(message: str, state, config: dict, system_prompt: str) -> Generator: """Run skill prompt inline in the current conversation.""" - import agent as _agent + from cheetahclaws import agent as _agent yield from _agent.run(message, state, config, system_prompt) @@ -49,7 +49,7 @@ def _execute_forked( system_prompt: str, ) -> Generator: """Run skill as an isolated sub-agent (separate conversation context).""" - import agent as _agent + from cheetahclaws import agent as _agent # Build a sub-agent config with depth tracking depth = config.get("_depth", 0) + 1 diff --git a/skill/loader.py b/cheetahclaws/skill/loader.py similarity index 100% rename from skill/loader.py rename to cheetahclaws/skill/loader.py diff --git a/skill/tools.py b/cheetahclaws/skill/tools.py similarity index 96% rename from skill/tools.py rename to cheetahclaws/skill/tools.py index 61217127..e63ad651 100644 --- a/skill/tools.py +++ b/cheetahclaws/skill/tools.py @@ -1,7 +1,7 @@ """Skill tool: lets the model invoke skills by name via tool call.""" from __future__ import annotations -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool from .loader import find_skill, load_skills, substitute_arguments @@ -60,7 +60,7 @@ def _skill_tool(params: dict, config: dict) -> str: message = f"[Skill: {skill.name}]\n\n{rendered}" # Run inline via agent and collect text output - import agent as _agent + from cheetahclaws import agent as _agent system_prompt = config.get("_system_prompt", "") # Collect output text diff --git a/skills.py b/cheetahclaws/skills.py similarity index 59% rename from skills.py rename to cheetahclaws/skills.py index da6c3701..cbabe89e 100644 --- a/skills.py +++ b/cheetahclaws/skills.py @@ -1,5 +1,5 @@ """Backward-compatibility shim — real implementation is in skill/ package.""" -from skill.loader import ( # noqa: F401 +from cheetahclaws.skill.loader import ( # noqa: F401 SkillDef, load_skills, find_skill, @@ -7,8 +7,8 @@ _parse_skill_file, _parse_list_field, ) -from skill.executor import execute_skill # noqa: F401 +from cheetahclaws.skill.executor import execute_skill # noqa: F401 # Legacy constant — kept for tests that patch it -from skill.loader import _get_skill_paths as _gsp +from cheetahclaws.skill.loader import _get_skill_paths as _gsp SKILL_PATHS = _gsp() diff --git a/subagent.py b/cheetahclaws/subagent.py similarity index 81% rename from subagent.py rename to cheetahclaws/subagent.py index 9ca5d860..b8a55dd2 100644 --- a/subagent.py +++ b/cheetahclaws/subagent.py @@ -1,5 +1,5 @@ """Backward-compatibility shim — real implementation is in multi_agent/subagent.py.""" -from multi_agent.subagent import ( # noqa: F401 +from cheetahclaws.multi_agent.subagent import ( # noqa: F401 AgentDefinition, SubAgentTask, SubAgentManager, diff --git a/task/__init__.py b/cheetahclaws/task/__init__.py similarity index 100% rename from task/__init__.py rename to cheetahclaws/task/__init__.py diff --git a/task/store.py b/cheetahclaws/task/store.py similarity index 100% rename from task/store.py rename to cheetahclaws/task/store.py diff --git a/task/tools.py b/cheetahclaws/task/tools.py similarity index 99% rename from task/tools.py rename to cheetahclaws/task/tools.py index cef83bba..933d1e69 100644 --- a/task/tools.py +++ b/cheetahclaws/task/tools.py @@ -1,7 +1,7 @@ """Task tools: TaskCreate, TaskUpdate, TaskGet, TaskList — registered into tool_registry.""" from __future__ import annotations -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool from .store import create_task, get_task, list_tasks, update_task, delete_task from .types import TaskStatus diff --git a/task/types.py b/cheetahclaws/task/types.py similarity index 100% rename from task/types.py rename to cheetahclaws/task/types.py diff --git a/tmux_tools.py b/cheetahclaws/tmux_tools.py similarity index 99% rename from tmux_tools.py rename to cheetahclaws/tmux_tools.py index 63f2074a..1e85a5fc 100644 --- a/tmux_tools.py +++ b/cheetahclaws/tmux_tools.py @@ -9,7 +9,7 @@ import sys import subprocess import shutil -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool # ── Detection ──────────────────────────────────────────────────────────────── diff --git a/tool_registry.py b/cheetahclaws/tool_registry.py similarity index 98% rename from tool_registry.py rename to cheetahclaws/tool_registry.py index 018faa18..a0d3c197 100644 --- a/tool_registry.py +++ b/cheetahclaws/tool_registry.py @@ -135,7 +135,7 @@ def execute_tool( # conservative ceiling (handles 32K-context models like qwen2.5-72b # behind a `custom/` provider that lies about context_limit). try: - from compaction import get_context_limit + from cheetahclaws.compaction import get_context_limit model = config.get("model", "") if config else "" declared_ctx = get_context_limit(model) or 32768 # Reserve 16K for system prompt + tool schemas + framing + headroom. diff --git a/tools/__init__.py b/cheetahclaws/tools/__init__.py similarity index 95% rename from tools/__init__.py rename to cheetahclaws/tools/__init__.py index 575bc71b..39e461d6 100644 --- a/tools/__init__.py +++ b/cheetahclaws/tools/__init__.py @@ -19,34 +19,34 @@ # ── Re-exports (backward compat) ────────────────────────────────────────── -from tools.security import _check_path_allowed, _is_safe_bash # noqa: F401 +from cheetahclaws.tools.security import _check_path_allowed, _is_safe_bash # noqa: F401 -from tools.fs import ( # noqa: F401 +from cheetahclaws.tools.fs import ( # noqa: F401 _read, _write, _edit, _glob, generate_unified_diff, maybe_truncate_diff, ) -from tools.shell import _bash, _grep, _kill_proc_tree, _has_rg # noqa: F401 +from cheetahclaws.tools.shell import _bash, _grep, _kill_proc_tree, _has_rg # noqa: F401 -from tools.web import _webfetch, _websearch # noqa: F401 +from cheetahclaws.tools.web import _webfetch, _websearch # noqa: F401 -from tools.research import _research # noqa: F401 +from cheetahclaws.tools.research import _research # noqa: F401 -from tools.notebook import _notebook_edit, _parse_cell_id # noqa: F401 +from cheetahclaws.tools.notebook import _notebook_edit, _parse_cell_id # noqa: F401 -from tools.diagnostics import ( # noqa: F401 +from cheetahclaws.tools.diagnostics import ( # noqa: F401 _get_diagnostics, _detect_language, _run_quietly, ) -from tools.interaction import ( # noqa: F401 +from cheetahclaws.tools.interaction import ( # noqa: F401 _tg_thread_local, _wx_thread_local, _slack_thread_local, _qq_thread_local, _is_in_tg_turn, _is_in_wx_turn, _is_in_slack_turn, _is_in_qq_turn, _is_in_web_turn, _ask_user_question, ask_input_interactive, _sleeptimer, _INPUT_WAIT_TIMEOUT, ) -from tool_registry import ToolDef, register_tool -from tool_registry import execute_tool as _registry_execute +from cheetahclaws.tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import execute_tool as _registry_execute # ── Tool JSON schemas (sent to the LLM API) ─────────────────────────────── @@ -500,7 +500,7 @@ def _read_with_overflow_check(p: dict, c: dict) -> str: # Skip redirect for already-small results (errors, empty, etc.) if not result or len(result) < 8000: return result - from tools.files import _maybe_redirect_to_summarize + from cheetahclaws.tools.files import _maybe_redirect_to_summarize redirect = _maybe_redirect_to_summarize(result, p["file_path"], c) return redirect if redirect else result @@ -662,11 +662,11 @@ def _read_with_overflow_check(p: dict, c: dict) -> str: # Each module self-registers its tools on import. Failures are best-effort. _EXTENSION_MODULES = [ - "memory.tools", - "multi_agent.tools", - "skill.tools", - "mcp_client.tools", - "task.tools", + "cheetahclaws.memory.tools", + "cheetahclaws.multi_agent.tools", + "cheetahclaws.skill.tools", + "cheetahclaws.mcp_client.tools", + "cheetahclaws.task.tools", ] for _mod_name in _EXTENSION_MODULES: @@ -675,16 +675,16 @@ def _read_with_overflow_check(p: dict, c: dict) -> str: except Exception: pass # Extension loading is best-effort; never crash startup -from multi_agent.tools import get_agent_manager as _get_agent_manager # noqa: F401 +from cheetahclaws.multi_agent.tools import get_agent_manager as _get_agent_manager # noqa: F401 try: - from plugin.loader import register_plugin_tools as _reg_plugin_tools + from cheetahclaws.plugin.loader import register_plugin_tools as _reg_plugin_tools _reg_plugin_tools() except Exception: pass # Plugin loading is best-effort; never crash startup try: - from checkpoint.hooks import install_hooks as _install_checkpoint_hooks + from cheetahclaws.checkpoint.hooks import install_hooks as _install_checkpoint_hooks _install_checkpoint_hooks() except Exception: pass @@ -693,7 +693,7 @@ def _read_with_overflow_check(p: dict, c: dict) -> str: import importlib as _il for _sub in ("browser", "email", "files"): try: - _il.import_module(f"tools.{_sub}") + _il.import_module(f"cheetahclaws.tools.{_sub}") except Exception: pass @@ -716,7 +716,7 @@ def _enter_plan_mode(params: dict, config: dict) -> str: header = f"# Plan: {task_desc}\n\n" if task_desc else "# Plan\n\n" plan_path.write_text(header, encoding="utf-8") - import runtime + from cheetahclaws import runtime sctx = runtime.get_ctx(config) sctx.prev_permission_mode = config.get("permission_mode", "auto") config["permission_mode"] = "plan" @@ -730,7 +730,7 @@ def _enter_plan_mode(params: dict, config: dict) -> str: def _exit_plan_mode(params: dict, config: dict) -> str: if config.get("permission_mode") != "plan": return "Not in plan mode." - import runtime + from cheetahclaws import runtime sctx = runtime.get_ctx(config) plan_file = sctx.plan_file or "" plan_content = "" diff --git a/tools/browser.py b/cheetahclaws/tools/browser.py similarity index 98% rename from tools/browser.py rename to cheetahclaws/tools/browser.py index cc03810a..fd9eb0d7 100644 --- a/tools/browser.py +++ b/cheetahclaws/tools/browser.py @@ -5,7 +5,7 @@ """ from __future__ import annotations -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool _INSTALL_HINT = ( "Browser tool requires playwright. Install with:\n" diff --git a/tools/diagnostics.py b/cheetahclaws/tools/diagnostics.py similarity index 100% rename from tools/diagnostics.py rename to cheetahclaws/tools/diagnostics.py diff --git a/tools/email.py b/cheetahclaws/tools/email.py similarity index 99% rename from tools/email.py rename to cheetahclaws/tools/email.py index 7aacf2cb..23c6a25e 100644 --- a/tools/email.py +++ b/cheetahclaws/tools/email.py @@ -12,7 +12,7 @@ from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool _CONFIG_HINT = ( "Email not configured. Set these in the REPL:\n" diff --git a/tools/files.py b/cheetahclaws/tools/files.py similarity index 99% rename from tools/files.py rename to cheetahclaws/tools/files.py index 1080e1f8..fcdb7a58 100644 --- a/tools/files.py +++ b/cheetahclaws/tools/files.py @@ -11,7 +11,7 @@ import io from pathlib import Path -from tool_registry import ToolDef, register_tool +from cheetahclaws.tool_registry import ToolDef, register_tool def _read_pdf(params: dict, config: dict) -> str: @@ -388,7 +388,7 @@ def _maybe_redirect_to_summarize(text: str, file_path: str, """ if not text: return None - from compaction import get_context_limit + from cheetahclaws.compaction import get_context_limit model = config.get("model", "") declared_ctx = get_context_limit(model) or 32768 @@ -465,7 +465,7 @@ def _summarize_chunk_via_llm(text: str, focus: str, config: dict, Returns the summary text, or an `[error: ...]` marker string on failure (so a single chunk failure doesn't sink the whole map-reduce).""" - from providers import stream, TextChunk + from cheetahclaws.providers import stream, TextChunk focus_clause = f" Focus on: {focus}." if focus else "" @@ -570,7 +570,7 @@ def _summarize_large_file(params: dict, config: dict) -> str: `[chunk N: error]` markers so one flaky source doesn't sink the whole job.""" from concurrent.futures import ThreadPoolExecutor - from compaction import get_context_limit + from cheetahclaws.compaction import get_context_limit file_path = params.get("file_path", "") if not file_path: diff --git a/tools/fs.py b/cheetahclaws/tools/fs.py similarity index 100% rename from tools/fs.py rename to cheetahclaws/tools/fs.py diff --git a/tools/interaction.py b/cheetahclaws/tools/interaction.py similarity index 97% rename from tools/interaction.py rename to cheetahclaws/tools/interaction.py index 042c0beb..c66a9a47 100644 --- a/tools/interaction.py +++ b/cheetahclaws/tools/interaction.py @@ -16,30 +16,30 @@ def _is_in_tg_turn(config: dict) -> bool: - import runtime + from cheetahclaws import runtime return (getattr(_tg_thread_local, "active", False) or bool(runtime.get_ctx(config).in_telegram_turn)) def _is_in_wx_turn(config: dict) -> bool: - import runtime + from cheetahclaws import runtime return (getattr(_wx_thread_local, "active", False) or bool(runtime.get_ctx(config).in_wechat_turn)) def _is_in_slack_turn(config: dict) -> bool: - import runtime + from cheetahclaws import runtime return (getattr(_slack_thread_local, "active", False) or bool(runtime.get_ctx(config).in_slack_turn)) def _is_in_web_turn(config: dict) -> bool: - import runtime + from cheetahclaws import runtime return bool(getattr(runtime.get_ctx(config), 'in_web_turn', False)) def _is_in_qq_turn(config: dict) -> bool: - import runtime + from cheetahclaws import runtime return (getattr(_qq_thread_local, "active", False) or bool(runtime.get_ctx(config).in_qq_turn)) @@ -131,7 +131,7 @@ def _ask_user_question( options = options or [] import re as _re - from ui.render import clr + from cheetahclaws.ui.render import clr _clean = _re.sub(r'\*\*(.+?)\*\*', r'\1', question) _clean = _re.sub(r'`(.+?)`', r'\1', _clean) _clean = _re.sub(r'\*(.+?)\*', r'\1', _clean) @@ -194,7 +194,7 @@ def ask_input_interactive(prompt: str, config: dict, """ import re as _re import threading as _threading - import runtime as _runtime + from cheetahclaws import runtime as _runtime _session_ctx = _runtime.get_session_ctx(config.get("_session_id", "default")) @@ -315,7 +315,7 @@ def ask_input_interactive(prompt: str, config: dict, # carries a short prompt id so a click on a stale prompt cannot # deliver to the current waiting agent. import uuid as _uuid - from bridges.telegram import _tg_send_keyboard + from cheetahclaws.bridges.telegram import _tg_send_keyboard prompt_id = _uuid.uuid4().hex[:8] keyboard = [ [{"text": str(label), @@ -368,7 +368,7 @@ def ask_input_interactive(prompt: str, config: dict, # ── SleepTimer ──────────────────────────────────────────────────────────── def _sleeptimer(seconds: int, config: dict) -> str: - import runtime + from cheetahclaws import runtime session_ctx = runtime.get_session_ctx(config.get("_session_id", "default")) cb = session_ctx.run_query if not cb: diff --git a/tools/notebook.py b/cheetahclaws/tools/notebook.py similarity index 100% rename from tools/notebook.py rename to cheetahclaws/tools/notebook.py diff --git a/tools/research.py b/cheetahclaws/tools/research.py similarity index 93% rename from tools/research.py rename to cheetahclaws/tools/research.py index efdf4463..3f4c44ef 100644 --- a/tools/research.py +++ b/cheetahclaws/tools/research.py @@ -32,14 +32,14 @@ def _research( citing authors (if analyzed), and numbered citations. Failures on individual sources surface in a "Missed" footer. """ - from research import research, build_time_range - from research.citations import render_notable_section - from research.entities import render_entities_table - from research.synthesizer import ( + from cheetahclaws.research import research, build_time_range + from cheetahclaws.research.citations import render_notable_section + from cheetahclaws.research.entities import render_entities_table + from cheetahclaws.research.synthesizer import ( format_heat_table, format_publication_trend, format_publication_sparkline, render_citations, ) - from research import reports as _reports + from cheetahclaws.research import reports as _reports try: tr = build_time_range(range_token=time_range, since=since, until=until) diff --git a/tools/security.py b/cheetahclaws/tools/security.py similarity index 100% rename from tools/security.py rename to cheetahclaws/tools/security.py diff --git a/tools/shell.py b/cheetahclaws/tools/shell.py similarity index 100% rename from tools/shell.py rename to cheetahclaws/tools/shell.py diff --git a/tools/web.py b/cheetahclaws/tools/web.py similarity index 100% rename from tools/web.py rename to cheetahclaws/tools/web.py diff --git a/ui/__init__.py b/cheetahclaws/ui/__init__.py similarity index 100% rename from ui/__init__.py rename to cheetahclaws/ui/__init__.py diff --git a/ui/input.py b/cheetahclaws/ui/input.py similarity index 100% rename from ui/input.py rename to cheetahclaws/ui/input.py diff --git a/ui/render.py b/cheetahclaws/ui/render.py similarity index 100% rename from ui/render.py rename to cheetahclaws/ui/render.py diff --git a/video/__init__.py b/cheetahclaws/video/__init__.py similarity index 67% rename from video/__init__.py rename to cheetahclaws/video/__init__.py index 1618f191..866b68c2 100644 --- a/video/__init__.py +++ b/cheetahclaws/video/__init__.py @@ -8,16 +8,16 @@ import sys as _sys # Import the real package — triggers its own __init__ -_real = _il.import_module("modular.video") +_real = _il.import_module("cheetahclaws.modular.video") # Re-export everything from the real package -from modular.video import * # noqa: F401, F403 +from cheetahclaws.modular.video import * # noqa: F401, F403 # Register submodules so 'from video.X import Y' works for _sub in ["pipeline", "story", "tts", "images", "subtitles", "assembly", "source", "niches", "cmd"]: try: - _m = _il.import_module(f"modular.video.{_sub}") - _sys.modules.setdefault(f"video.{_sub}", _m) + _m = _il.import_module(f"cheetahclaws.modular.video.{_sub}") + _sys.modules.setdefault(f"{__name__}.{_sub}", _m) except ImportError: pass diff --git a/voice/__init__.py b/cheetahclaws/voice/__init__.py similarity index 64% rename from voice/__init__.py rename to cheetahclaws/voice/__init__.py index 2ece8da5..59b081db 100644 --- a/voice/__init__.py +++ b/cheetahclaws/voice/__init__.py @@ -8,15 +8,15 @@ import sys as _sys # Import the real package — triggers its own __init__ -_real = _il.import_module("modular.voice") +_real = _il.import_module("cheetahclaws.modular.voice") # Re-export everything from the real package -from modular.voice import * # noqa: F401, F403 +from cheetahclaws.modular.voice import * # noqa: F401, F403 # Register submodules so 'from voice.X import Y' works for _sub in ["recorder", "stt", "keyterms", "cmd"]: try: - _m = _il.import_module(f"modular.voice.{_sub}") - _sys.modules.setdefault(f"voice.{_sub}", _m) + _m = _il.import_module(f"cheetahclaws.modular.voice.{_sub}") + _sys.modules.setdefault(f"{__name__}.{_sub}", _m) except ImportError: pass diff --git a/web/__init__.py b/cheetahclaws/web/__init__.py similarity index 100% rename from web/__init__.py rename to cheetahclaws/web/__init__.py diff --git a/web/addon-fit.min.js b/cheetahclaws/web/addon-fit.min.js similarity index 100% rename from web/addon-fit.min.js rename to cheetahclaws/web/addon-fit.min.js diff --git a/web/addon-web-links.min.js b/cheetahclaws/web/addon-web-links.min.js similarity index 100% rename from web/addon-web-links.min.js rename to cheetahclaws/web/addon-web-links.min.js diff --git a/web/api.py b/cheetahclaws/web/api.py similarity index 97% rename from web/api.py rename to cheetahclaws/web/api.py index 9025f37a..1b935d70 100644 --- a/web/api.py +++ b/cheetahclaws/web/api.py @@ -195,7 +195,7 @@ class ChatSession: def __init__(self, base_config: dict, user_id: int, *, session_id: Optional[str] = None, title: Optional[str] = None): - from web import db as _db + from cheetahclaws.web import db as _db _db.init_db() # Hydrate-from-DB path vs new-session path @@ -249,8 +249,8 @@ def __init__(self, base_config: dict, user_id: int, *, def _init_runtime(self): """Initialize RuntimeContext and AgentState.""" - from agent import AgentState - import runtime + from cheetahclaws.agent import AgentState + from cheetahclaws import runtime self._agent_state = AgentState() ctx = runtime.get_session_ctx(self.session_id) @@ -446,7 +446,7 @@ def _handle_slash(self, line: str) -> bool: def _run_ssj(): self._busy.set() - import runtime + from cheetahclaws import runtime ctx = runtime.get_session_ctx(self.session_id) ctx.in_web_turn = True try: @@ -516,7 +516,7 @@ def encoding(self): def _run_long(): _target_thread_id[0] = threading.current_thread().ident self._busy.set() - import runtime + from cheetahclaws import runtime ctx = runtime.get_session_ctx(self.session_id) ctx.in_web_turn = True wrapper = _ThreadLocalStdout(session_ref._broadcast, @@ -709,10 +709,10 @@ def _handle_slash_inner(self, line: str): def _run_agent(self, prompt: str): """Iterate agent.run() generator, broadcast events.""" - from agent import (run, TextChunk, ThinkingChunk, ToolStart, + from cheetahclaws.agent import (run, TextChunk, ThinkingChunk, ToolStart, ToolEnd, TurnDone, PermissionRequest) - from context import build_system_prompt - import runtime + from cheetahclaws.context import build_system_prompt + from cheetahclaws import runtime ctx = runtime.get_session_ctx(self.session_id) ctx.in_web_turn = True @@ -813,7 +813,7 @@ def _run_agent(self, prompt: str): def approve_permission(self, granted: bool): """Respond to a pending PermissionRequest.""" - import runtime + from cheetahclaws import runtime ctx = runtime.get_session_ctx(self.session_id) evt = ctx.web_input_event if evt: @@ -827,7 +827,7 @@ def _append_msg(self, msg: dict): self.messages.append(msg) # Persist to DB (best-effort; don't break streaming on DB failure) try: - from web import db as _db + from cheetahclaws.web import db as _db _db.repo.append_message( self.session_id, msg.get("role", "system"), @@ -839,7 +839,7 @@ def _append_msg(self, msg: dict): if sess and sess["title"] != self.title: self.title = sess["title"] except Exception as exc: # noqa: BLE001 - from web.logging_setup import get_logger + from cheetahclaws.web.logging_setup import get_logger get_logger("api").exception("message persist failed", extra={"session_id": self.session_id, "err": str(exc)}) @@ -868,7 +868,7 @@ def update_config(self, updates: dict) -> dict: self.config[k] = v # Persist non-secret config keys to DB (secrets stay session-only) try: - from web import db as _db + from cheetahclaws.web import db as _db _db.repo.upsert_session( self.session_id, self.user_id, title=self.title, @@ -876,7 +876,7 @@ def update_config(self, updates: dict) -> dict: if k in _SAFE_CONFIG_KEYS}, ) except Exception as exc: # noqa: BLE001 - from web.logging_setup import get_logger + from cheetahclaws.web.logging_setup import get_logger get_logger("api").exception("config persist failed", extra={"session_id": self.session_id, "err": str(exc)}) @@ -891,7 +891,7 @@ def is_stale(self) -> bool: # ── Cleanup ──────────────────────────────────────────────────────── def cleanup(self): - import runtime + from cheetahclaws import runtime runtime.release_session_ctx(self.session_id) @@ -930,7 +930,7 @@ def get_chat_session(sid: str, return None # Try to hydrate from DB try: - from web import db as _db + from cheetahclaws.web import db as _db row = _db.repo.get_session(sid, user_id) except Exception: # noqa: BLE001 return None @@ -949,10 +949,10 @@ def get_chat_session(sid: str, def list_chat_sessions(user_id: int) -> list[dict]: """List this user's sessions (DB is the source of truth, not memory).""" try: - from web import db as _db + from cheetahclaws.web import db as _db rows = _db.repo.list_sessions(user_id) except Exception as exc: # noqa: BLE001 - from web.logging_setup import get_logger + from cheetahclaws.web.logging_setup import get_logger get_logger("api").exception("list_sessions failed", extra={"user_id": user_id, "err": str(exc)}) @@ -968,10 +968,10 @@ def list_chat_sessions(user_id: int) -> list[dict]: def remove_chat_session(sid: str, user_id: int) -> bool: """Remove session from DB and in-memory cache. Returns True if removed.""" try: - from web import db as _db + from cheetahclaws.web import db as _db deleted = _db.repo.delete_session(sid, user_id) except Exception as exc: # noqa: BLE001 - from web.logging_setup import get_logger + from cheetahclaws.web.logging_setup import get_logger get_logger("api").exception("delete_session failed", extra={"session_id": sid, "user_id": user_id, @@ -985,28 +985,28 @@ def remove_chat_session(sid: str, user_id: int) -> bool: def list_folders(user_id: int) -> list[dict]: - from web import db as _db + from cheetahclaws.web import db as _db return _db.repo.list_folders(user_id) def create_folder(user_id: int, name: str) -> Optional[dict]: - from web import db as _db + from cheetahclaws.web import db as _db return _db.repo.create_folder(user_id, name) def rename_folder(folder_id: int, user_id: int, name: str) -> bool: - from web import db as _db + from cheetahclaws.web import db as _db return _db.repo.rename_folder(folder_id, user_id, name) def remove_folder(folder_id: int, user_id: int) -> bool: - from web import db as _db + from cheetahclaws.web import db as _db return _db.repo.delete_folder(folder_id, user_id) def move_session_to_folder(sid: str, user_id: int, folder_id: Optional[int]) -> bool: - from web import db as _db + from cheetahclaws.web import db as _db return _db.repo.move_session_to_folder(sid, user_id, folder_id) @@ -1054,7 +1054,7 @@ def batch_export_chat_sessions_markdown(sids: list, def rename_chat_session(sid: str, user_id: int, title: str) -> bool: try: - from web import db as _db + from cheetahclaws.web import db as _db ok = _db.repo.rename_session(sid, user_id, title) except Exception: # noqa: BLE001 return False @@ -1069,7 +1069,7 @@ def rename_chat_session(sid: str, user_id: int, title: str) -> bool: def export_chat_session_markdown(sid: str, user_id: int) -> Optional[str]: """Render a session's messages as Markdown. Returns None if not found.""" try: - from web import db as _db + from cheetahclaws.web import db as _db meta = _db.repo.get_session(sid, user_id) if not meta: return None @@ -1114,7 +1114,7 @@ def export_chat_session_markdown(sid: str, user_id: int) -> Optional[str]: def get_available_models() -> list[dict]: """Return all providers and their models for the UI model picker.""" try: - from providers import PROVIDERS + from cheetahclaws.providers import PROVIDERS except ImportError: return [] result = [] diff --git a/web/auth.py b/cheetahclaws/web/auth.py similarity index 100% rename from web/auth.py rename to cheetahclaws/web/auth.py diff --git a/web/chat.html b/cheetahclaws/web/chat.html similarity index 100% rename from web/chat.html rename to cheetahclaws/web/chat.html diff --git a/web/db.py b/cheetahclaws/web/db.py similarity index 99% rename from web/db.py rename to cheetahclaws/web/db.py index 958a25d6..688069e6 100644 --- a/web/db.py +++ b/cheetahclaws/web/db.py @@ -23,7 +23,7 @@ "Install it with: pip install 'cheetahclaws[web]'" ) from exc -from web.models import ( +from cheetahclaws.web.models import ( ApiCredential, Base, ChatSessionRow, Folder, Message, User, ) diff --git a/web/favicon.ico b/cheetahclaws/web/favicon.ico similarity index 100% rename from web/favicon.ico rename to cheetahclaws/web/favicon.ico diff --git a/web/lab.html b/cheetahclaws/web/lab.html similarity index 100% rename from web/lab.html rename to cheetahclaws/web/lab.html diff --git a/web/lab_api.py b/cheetahclaws/web/lab_api.py similarity index 94% rename from web/lab_api.py rename to cheetahclaws/web/lab_api.py index 34796f78..10ae753e 100644 --- a/web/lab_api.py +++ b/cheetahclaws/web/lab_api.py @@ -115,12 +115,12 @@ def _start_run(body: dict, config: dict) -> tuple[int, str, bytes]: config.get("lab_max_rounds", 5))) role_override = body.get("role_override") or config.get("lab_role_override") or {} - from research.lab.storage import LabStorage - from research.lab.orchestrator import ( + from cheetahclaws.research.lab.storage import LabStorage + from cheetahclaws.research.lab.orchestrator import ( _drive, LabRun, LabState, Stage, ) - from research.lab.roles import build_default_assignment - from research.lab.convergence import ConvergenceConfig + from cheetahclaws.research.lab.roles import build_default_assignment + from cheetahclaws.research.lab.convergence import ConvergenceConfig storage = LabStorage() rec = storage.create_run( @@ -168,7 +168,7 @@ def _runner(): def _list_runs(query: dict) -> tuple[int, str, bytes]: - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.storage import LabStorage s = LabStorage() limit = int(query.get("limit", 50)) status_filter = query.get("status") or None @@ -193,7 +193,7 @@ def _list_runs(query: dict) -> tuple[int, str, bytes]: def _run_detail(run_id: str) -> tuple[int, str, bytes]: - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.storage import LabStorage s = LabStorage() r = s.get_run(run_id) if r is None: @@ -227,7 +227,7 @@ def _run_detail(run_id: str) -> tuple[int, str, bytes]: def _get_messages(run_id: str, query: dict) -> tuple[int, str, bytes]: - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.storage import LabStorage s = LabStorage() if s.get_run(run_id) is None: return _err(404, f"no such run: {run_id}") @@ -251,7 +251,7 @@ def _get_messages(run_id: str, query: dict) -> tuple[int, str, bytes]: def _get_report(run_id: str) -> tuple[int, str, bytes]: - from research.lab.storage import LabStorage, DEFAULT_OUTPUT_DIR + from cheetahclaws.research.lab.storage import LabStorage, DEFAULT_OUTPUT_DIR s = LabStorage() r = s.get_run(run_id) if r is None: @@ -269,7 +269,7 @@ def _get_report(run_id: str) -> tuple[int, str, bytes]: def _get_experiments(run_id: str) -> tuple[int, str, bytes]: - from research.lab.storage import LabStorage + from cheetahclaws.research.lab.storage import LabStorage s = LabStorage() if s.get_run(run_id) is None: return _err(404, f"no such run: {run_id}") @@ -293,7 +293,7 @@ def _get_experiments(run_id: str) -> tuple[int, str, bytes]: def _get_artifact(run_id: str, filename: str) -> tuple[int, str, bytes]: """Serve a file from the run's workspace dir. Read-only, sandboxed to the workspace; basic path-traversal guard.""" - from research.lab.storage import DEFAULT_OUTPUT_DIR + from cheetahclaws.research.lab.storage import DEFAULT_OUTPUT_DIR if "/" in filename or "\\" in filename or filename.startswith("."): return _err(400, "invalid filename") path = DEFAULT_OUTPUT_DIR / run_id / "workspace" / filename diff --git a/web/logging_setup.py b/cheetahclaws/web/logging_setup.py similarity index 100% rename from web/logging_setup.py rename to cheetahclaws/web/logging_setup.py diff --git a/web/marked.min.js b/cheetahclaws/web/marked.min.js similarity index 100% rename from web/marked.min.js rename to cheetahclaws/web/marked.min.js diff --git a/web/models.py b/cheetahclaws/web/models.py similarity index 100% rename from web/models.py rename to cheetahclaws/web/models.py diff --git a/web/server.py b/cheetahclaws/web/server.py similarity index 96% rename from web/server.py rename to cheetahclaws/web/server.py index e9ade63f..89208819 100644 --- a/web/server.py +++ b/cheetahclaws/web/server.py @@ -41,14 +41,14 @@ def _resolve_web_dir() -> Path: """Locate the directory containing chat.html, marked.min.js, static/, etc. - Prefer ``importlib.resources.files("web")`` so the package's data files are - found whichever way the package was installed (editable, non-editable wheel, - zipapp, PEX). Fall back to ``Path(__file__).parent`` for unusual layouts - where the resource API can't return a real filesystem path. + Prefer ``importlib.resources.files("cheetahclaws.web")`` so the package's + data files are found whichever way the package was installed (editable, + non-editable wheel, zipapp, PEX). Fall back to ``Path(__file__).parent`` + for unusual layouts where the resource API can't return a real path. """ try: from importlib.resources import files as _resource_files - candidate = Path(str(_resource_files("web"))) + candidate = Path(str(_resource_files("cheetahclaws.web"))) if candidate.is_dir(): return candidate.resolve() except (ImportError, ModuleNotFoundError, TypeError, @@ -460,7 +460,7 @@ def _emit_access_log(status_code: int) -> None: return _req_ctx.logged = True try: - from web.logging_setup import get_logger, incr + from cheetahclaws.web.logging_setup import get_logger, incr start = getattr(_req_ctx, "start_ts", None) dur_ms = (int((time.monotonic() - start) * 1000) if start is not None else 0) @@ -620,7 +620,7 @@ def _jwt_user_id(cookie_str: str) -> Optional[int]: uid = 1 else: try: - from web.auth import decode_token + from cheetahclaws.web.auth import decode_token except ImportError: return None if not cookie_str: @@ -932,8 +932,8 @@ def _handle_chat_websocket(sock: socket.socket, extra: bytes, except (json.JSONDecodeError, KeyError): pass - from web.api import get_chat_session - from config import load_config + from cheetahclaws.web.api import get_chat_session + from cheetahclaws.config import load_config chat_session = get_chat_session(session_id, user_id, load_config()) if not chat_session: try: @@ -1117,13 +1117,13 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: db_ok = True db_err: Optional[str] = None try: - from web.db import init_db, repo as dbrepo + from cheetahclaws.web.db import init_db, repo as dbrepo init_db() dbrepo.user_count() # touches the DB except Exception as exc: # noqa: BLE001 db_ok = False db_err = str(exc) - from web.logging_setup import uptime_seconds + from cheetahclaws.web.logging_setup import uptime_seconds payload = { "ok": db_ok, "db": "ok" if db_ok else "error", @@ -1141,13 +1141,13 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: if path == "/metrics" and method == "GET": # Prometheus text exposition format (v0.0.4) try: - from web.logging_setup import snapshot, uptime_seconds + from cheetahclaws.web.logging_setup import snapshot, uptime_seconds counters = snapshot() except Exception: # noqa: BLE001 counters = {} extras: dict[str, int] = {} try: - from web.db import init_db, repo as dbrepo + from cheetahclaws.web.db import init_db, repo as dbrepo init_db() extras["users_total"] = dbrepo.user_count() except Exception: # noqa: BLE001 @@ -1172,7 +1172,7 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: # GET /api/auth/bootstrap — does any user exist yet? if path == "/api/auth/bootstrap" and method == "GET": try: - from web.db import init_db, repo as dbrepo + from cheetahclaws.web.db import init_db, repo as dbrepo init_db() has_users = dbrepo.user_count() > 0 except Exception as exc: # noqa: BLE001 @@ -1196,8 +1196,8 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: sock.close() return try: - from web.db import init_db, repo as dbrepo - from web.auth import hash_password, issue_token, build_cookie + from cheetahclaws.web.db import init_db, repo as dbrepo + from cheetahclaws.web.auth import hash_password, issue_token, build_cookie init_db() if dbrepo.get_user_by_username(username) is not None: _send_http(sock, "409 Conflict", "application/json", @@ -1209,7 +1209,7 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: user = dbrepo.create_user(username, hash_password(password), is_admin=is_admin) token = issue_token(user["id"], user["username"]) - from web.logging_setup import get_logger, incr + from cheetahclaws.web.logging_setup import get_logger, incr incr("auth_registrations_total") get_logger("auth").info("register", extra={ "username": user["username"], "user_id": user["id"], @@ -1232,14 +1232,14 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: username = (body_json.get("username") or "").strip() password = body_json.get("password") or "" try: - from web.db import init_db, repo as dbrepo - from web.auth import (verify_password, issue_token, + from cheetahclaws.web.db import init_db, repo as dbrepo + from cheetahclaws.web.auth import (verify_password, issue_token, build_cookie) init_db() rec = dbrepo.get_user_by_username(username) if not rec or not verify_password(password, rec["password_hash"]): - from web.logging_setup import incr, get_logger + from cheetahclaws.web.logging_setup import incr, get_logger incr("auth_logins_failed") get_logger("auth").warning("login_failed", extra={"username": username}) @@ -1249,7 +1249,7 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: sock.close() return token = issue_token(rec["id"], rec["username"]) - from web.logging_setup import incr, get_logger + from cheetahclaws.web.logging_setup import incr, get_logger incr("auth_logins_total") get_logger("auth").info("login", extra={ "user_id": rec["id"], "username": rec["username"], @@ -1289,7 +1289,7 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: sock.close() return try: - from web.db import init_db, repo as dbrepo + from cheetahclaws.web.db import init_db, repo as dbrepo init_db() user = dbrepo.get_user(uid) except Exception: # noqa: BLE001 @@ -1482,8 +1482,8 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: # ── /api/lab/* routes (research lab) ──────────────────────── if path.startswith("/api/lab"): try: - from web.lab_api import dispatch as _lab_dispatch - from config import load_config as _load_cfg + from cheetahclaws.web.lab_api import dispatch as _lab_dispatch + from cheetahclaws.config import load_config as _load_cfg from urllib.parse import parse_qs q_dict = {k: v[0] for k, v in parse_qs(query).items()} status, ctype, raw = _lab_dispatch( @@ -1507,8 +1507,8 @@ def _handle_connection(sock: socket.socket, addr: tuple) -> None: uid = _require_user(sock, cookie, origin) if uid is None: return - from web.api import create_chat_session, get_chat_session - from config import load_config + from cheetahclaws.web.api import create_chat_session, get_chat_session + from cheetahclaws.config import load_config sid = body_json.get("session_id", "") chat_sess = (get_chat_session(sid, uid, load_config()) if sid else None) @@ -1622,8 +1622,8 @@ def _sse_callback(evt_dict): uid = _require_user(sock, cookie, origin) if uid is None: return - from web.api import get_chat_session - from config import load_config + from cheetahclaws.web.api import get_chat_session + from cheetahclaws.config import load_config sid = body_json.get("session_id", "") granted = body_json.get("granted", False) chat_sess = get_chat_session(sid, uid, load_config()) @@ -1641,7 +1641,7 @@ def _sse_callback(evt_dict): uid = _require_user(sock, cookie, origin) if uid is None: return - from web.api import (list_folders, create_folder, + from cheetahclaws.web.api import (list_folders, create_folder, rename_folder, remove_folder) parts_f = path.rstrip("/").split("/") # GET /api/folders @@ -1712,13 +1712,13 @@ def _sse_callback(evt_dict): uid = _require_user(sock, cookie, origin) if uid is None: return - from web.api import (list_chat_sessions, get_chat_session, + from cheetahclaws.web.api import (list_chat_sessions, get_chat_session, rename_chat_session, remove_chat_session, export_chat_session_markdown, batch_remove_chat_sessions, batch_export_chat_sessions_markdown, move_session_to_folder) - from config import load_config + from cheetahclaws.config import load_config # POST /api/sessions/batch_delete body: {ids: [...]} if path == "/api/sessions/batch_delete" and method == "POST": ids = body_json.get("ids") or [] @@ -1859,8 +1859,8 @@ def _sse_callback(evt_dict): uid = _require_user(sock, cookie, origin) if uid is None: return - from web.api import get_chat_session - from config import load_config + from cheetahclaws.web.api import get_chat_session + from cheetahclaws.config import load_config sid = body_json.get("session_id", "") or \ (query.split("sid=")[1].split("&")[0] if "sid=" in query else "") @@ -1881,7 +1881,7 @@ def _sse_callback(evt_dict): if path == "/api/models" and method == "GET": if _require_user(sock, cookie, origin) is None: return - from web.api import get_available_models + from cheetahclaws.web.api import get_available_models _send_json(sock, {"providers": get_available_models()}, request_origin=origin) sock.close() @@ -1950,7 +1950,7 @@ def _sse_callback(evt_dict): except (TimeoutError, ConnectionResetError, BrokenPipeError): pass # normal for idle/dropped connections except Exception as exc: - from web.logging_setup import get_logger + from cheetahclaws.web.logging_setup import get_logger get_logger("server").exception("connection handler crashed", extra={"peer": str(addr), "err": str(exc)}) @@ -1981,7 +1981,7 @@ def _reap_stale_sessions() -> None: sess.close() # Chat sessions (structured API) try: - from web.api import reap_stale_chat_sessions + from cheetahclaws.web.api import reap_stale_chat_sessions reap_stale_chat_sessions() except ImportError: pass @@ -2030,7 +2030,7 @@ def start_web_server( # Install structured logging first so later errors are captured correctly. try: - from web.logging_setup import setup_logging, get_logger + from cheetahclaws.web.logging_setup import setup_logging, get_logger setup_logging() _log = get_logger("server") except ImportError: @@ -2039,7 +2039,7 @@ def start_web_server( # Initialize chat-UI database (graceful fallback if deps missing) global _chat_ui_ready try: - from web.db import init_db, repo as dbrepo + from cheetahclaws.web.db import init_db, repo as dbrepo init_db() _chat_ui_ready = True _chat_user_count = dbrepo.user_count() diff --git a/web/static/favicon.ico b/cheetahclaws/web/static/favicon.ico similarity index 100% rename from web/static/favicon.ico rename to cheetahclaws/web/static/favicon.ico diff --git a/web/static/favicon.png b/cheetahclaws/web/static/favicon.png similarity index 100% rename from web/static/favicon.png rename to cheetahclaws/web/static/favicon.png diff --git a/web/static/js/approval.js b/cheetahclaws/web/static/js/approval.js similarity index 100% rename from web/static/js/approval.js rename to cheetahclaws/web/static/js/approval.js diff --git a/web/static/js/auth.js b/cheetahclaws/web/static/js/auth.js similarity index 100% rename from web/static/js/auth.js rename to cheetahclaws/web/static/js/auth.js diff --git a/web/static/js/chat.js b/cheetahclaws/web/static/js/chat.js similarity index 100% rename from web/static/js/chat.js rename to cheetahclaws/web/static/js/chat.js diff --git a/web/static/js/csrf.js b/cheetahclaws/web/static/js/csrf.js similarity index 100% rename from web/static/js/csrf.js rename to cheetahclaws/web/static/js/csrf.js diff --git a/web/static/js/init.js b/cheetahclaws/web/static/js/init.js similarity index 100% rename from web/static/js/init.js rename to cheetahclaws/web/static/js/init.js diff --git a/web/static/js/settings.js b/cheetahclaws/web/static/js/settings.js similarity index 100% rename from web/static/js/settings.js rename to cheetahclaws/web/static/js/settings.js diff --git a/web/static/js/sidebar.js b/cheetahclaws/web/static/js/sidebar.js similarity index 100% rename from web/static/js/sidebar.js rename to cheetahclaws/web/static/js/sidebar.js diff --git a/web/static/js/tools.js b/cheetahclaws/web/static/js/tools.js similarity index 100% rename from web/static/js/tools.js rename to cheetahclaws/web/static/js/tools.js diff --git a/web/static/js/util.js b/cheetahclaws/web/static/js/util.js similarity index 100% rename from web/static/js/util.js rename to cheetahclaws/web/static/js/util.js diff --git a/web/static/js/welcome.js b/cheetahclaws/web/static/js/welcome.js similarity index 100% rename from web/static/js/welcome.js rename to cheetahclaws/web/static/js/welcome.js diff --git a/web/xterm.min.css b/cheetahclaws/web/xterm.min.css similarity index 100% rename from web/xterm.min.css rename to cheetahclaws/web/xterm.min.css diff --git a/web/xterm.min.js b/cheetahclaws/web/xterm.min.js similarity index 100% rename from web/xterm.min.js rename to cheetahclaws/web/xterm.min.js diff --git a/demo.py b/demo.py index b350feaa..1a30f447 100644 --- a/demo.py +++ b/demo.py @@ -12,9 +12,9 @@ # Add parent path for imports sys.path.insert(0, os.path.dirname(__file__)) -from config import load_config -from context import build_system_prompt -from agent import AgentState, run, TextChunk, ThinkingChunk, ToolStart, ToolEnd, TurnDone, PermissionRequest +from cheetahclaws.config import load_config +from cheetahclaws.context import build_system_prompt +from cheetahclaws.agent import AgentState, run, TextChunk, ThinkingChunk, ToolStart, ToolEnd, TurnDone, PermissionRequest def demo(): config = load_config() diff --git a/docs/news.md b/docs/news.md index 7328f27b..a3e4eb61 100644 --- a/docs/news.md +++ b/docs/news.md @@ -3,7 +3,7 @@ ## 🔥🔥🔥 News (Pacific Time) -- June 16, 2026 (latest): **Internal modules lose the `cc_` prefix.** The four `cc_`-prefixed modules are renamed for readability: `cc_config.py → config.py`, `cc_daemon/ → daemon/`, `cc_kernel/ → kernel/`, and `cc_mcp/ → mcp_client/`. The MCP client is deliberately `mcp_client/` rather than bare `mcp/` — a top-level `mcp` would shadow Python's namespace and the official `modelcontextprotocol` package, an easy-to-introduce, hard-to-debug import shadow. References were updated across **every `.py` source**, `docs/` + all RFCs, `README`, `CONTRIBUTING`, `pyproject.toml` (both `py-modules` and `packages.find`), and `.github/workflows/ci.yml`, using **whole-word matching** so lookalike tokens were left untouched — `cc_bin` / `cc_script` (web server locals), `cv_acc_mean` / `acc_clean` (trading-model fields), `cc_tool_call_debug` (a `/tmp` log path), and the `cc_test_` / `cc_subs_` `tempfile` prefixes. The 18 `tests/test_cc_daemon_*.py` files become `test_daemon_*.py`. **Two name-collision regressions** surfaced where the now-generic module name clashed with a same-scope local variable, both fixed: (1) `tests/test_setup_wizard.py` ran `import config`, which shadowed the wizard helper's `config` **dict param** — the module object then reached `run_setup_wizard` and raised `TypeError: 'module' object does not support item assignment`; the module is now imported as `_config_mod`. (2) `examples/kernel_e2e_smoke.py` both self-shadowed via `with kernel.Kernel.open(...) as kernel` (an `UnboundLocalError` because the `as` binding made `kernel` local for the whole line) **and** collided inside `_run_demo`, whose `kernel` **instance** param sat next to module-level `kernel.ScheduleSpec` / `kernel.SandboxPolicy` (an `AttributeError` once the param won) — the context var is now bound as `kern` and the two classes are imported at module level. An AST sweep over the whole tree confirmed no other module-name/local-variable shadowing remains. **This is breaking only for code that imports these modules directly** (`import cc_kernel` → `import kernel`; `from cc_mcp.client import get_mcp_manager` → `from mcp_client.client import get_mcp_manager`); the `cheetahclaws` CLI, the Web UI, and all bridges are unaffected. Full suite: **2449 passed, 3 skipped, 0 failed**. +- June 16, 2026 (latest): **All internal modules move into a single `cheetahclaws` package.** Previously the importable modules lived flat at the top level (`config.py`, `daemon/`, `kernel/`, `mcp_client/`, `providers.py`, …). That works when you run from the repo dir but breaks once CheetahClaws is *installed* and launched from its entry point: a generic top-level name like `config` or `daemon` gets shadowed by whatever else is on `sys.path` — another project's `config/` directory, the PyPI `python-daemon` package — and `cheetahclaws` dies at startup with `ImportError: cannot import name … from 'config' (unknown location)`. (An earlier pass that merely dropped a `cc_` prefix from four of these modules re-introduced exactly this collision, which the prefix had originally been added to prevent — so this change supersedes it.) The fix is the standard one: own a single namespace. All 21 single-file modules and 20 sub-packages now live under `cheetahclaws/` and are imported as `cheetahclaws.`; the entry script `cheetahclaws.py` became `cheetahclaws/cli.py`, with a deliberately light `cheetahclaws/__init__.py` (defines `VERSION`, lazily proxies CLI entry symbols via PEP 562 `__getattr__` so importing a submodule never drags in the heavy CLI) and a `cheetahclaws/__main__.py` for `python -m cheetahclaws`. Imports were rewritten across all 448 `.py` files — 1269 `from NAME` + 126 `import NAME` + 41 dotted `import NAME.sub` statements, 118 string `patch` / `mock` / `import_module` targets, subprocess `-m` argv paths, the modular plugin f-string loaders, the voice/video back-compat shims, and embedded driver-script strings — all prefixed with `cheetahclaws.`, using whole-word matching so RPC method names, filenames, and unrelated tokens were left alone. `pyproject.toml` now ships a single `cheetahclaws*` package (no `py-modules`) with entry point `cheetahclaws.cli:main`; `agent_templates/` moved into the package so it ships as data. Triage of the move surfaced and fixed seven regression classes — kernel/daemon subprocess `-m` argv paths, the `test_packaging` import contract, the voice shim's submodule registration, the daemon e2e launcher, tests that patched the package object instead of the `cli` module, tests with hardcoded repo-root data paths, and a `sys.modules` stub-restore leak between `test_research` and `test_setup_wizard`. **Breaking only for code that imports CheetahClaws internals directly** — `import kernel` → `from cheetahclaws import kernel`, `from mcp_client.client import get_mcp_manager` → `from cheetahclaws.mcp_client.client import get_mcp_manager`; the `cheetahclaws` CLI, `python -m cheetahclaws`, the Web UI, and all bridges are unaffected. Verified end-to-end: `python -m cheetahclaws --version` and `from cheetahclaws import config` both work from outside the repo (the original crash), a built wheel contains `cheetahclaws/*` with all data files (web, prompts, agent_templates) and zero bare top-level modules, and the full suite is **2449 passed, 3 skipped, 0 failed**. - June 6, 2026 (**v3.5.82**): **macOS install reliably puts `cheetahclaws` on PATH, and local Ollama models that emit tool calls as text now actually execute them.** Two fixes reported in issue #131. **(1) Install / PATH on macOS.** On macOS the installer creates a dedicated venv (`~/.cheetahclaws-venv`) and `source`s it, so the post-install verification `if command -v cheetahclaws` succeeded *inside the script's own activated shell* — it printed "cheetahclaws is on PATH" and **short-circuited past the entire rc-file block**, including the `touch ~/.zshrc` that was supposed to create the file. Result: `~/.zshrc` was never created/updated, and in a fresh terminal (no venv active) the binary was unreachable, so users had to hunt for the install location by hand. The verification step no longer trusts the venv-polluted `command -v`: it confirms the binary at the expected `BIN_DIR`, then (for venv installs) **symlinks only the `cheetahclaws` entry point into `~/.local/bin`** — pipx-style, so the venv's `python`/`pip` never get prepended to PATH and can't shadow the user's own — creates the right rc file if missing (`~/.zshrc` for zsh, `~/.bash_profile` for bash on macOS, `config.fish` for fish), and appends the exposure dir to PATH there. The fish branch now also writes fish (`set -gx PATH …`) syntax instead of `export`, and the reload hint points bash-on-macOS at `.bash_profile` (`scripts/install.sh`). **(2) Ollama tool calls (the "model just keeps talking" bug).** The Ollama streaming path (`stream_ollama`) only read tool calls from Ollama's structured `message.tool_calls` field, whereas the OpenAI-compatible cloud path (`stream_openai_compat`) *also* recovers tool calls a model emits as **text** via `_find_native_tool_marker` + `_extract_native_tool_calls`. Many local models — Qwen-coder, Gemma, Mistral — emit calls as `{…}` / `<|tool_call|>…` / `[TOOL_CALLS][…]` inside `content`; on the Ollama path that markup was streamed straight to the screen as chat and never executed, so the agent loop saw no tool calls and ended the turn — exactly the reported "tool-calling-style chat that never runs." `stream_ollama` now mirrors the cloud path: when a native marker appears in the streamed content it **buffers from that point** (so the user never sees raw markup), and at end-of-stream parses the buffer into real tool calls (falling back to surfacing the buffered text if parsing fails, so nothing is silently swallowed). Note: Ollama's native `/api/chat` does not accept a `tool_choice` parameter, so the fix is the text-format recovery, not a request-param change. Existing provider + cache-token suites stay green. See [docs/guides/usage.md](guides/usage.md#usage-open-source-models-local) · [docs/guides/faq.md](guides/faq.md). - June 5, 2026 (**v3.5.82**): **User-controllable token / cost budgets — set a spend cap; on hit the session auto-saves and you can resume or raise it.** The quota engine (`quota.py`: per-session + per-day token/cost counters, enforced before each model call) already existed but had no friendly surface — you had to know four config keys (`session_token_budget` / `session_cost_budget` / `daily_token_budget` / `daily_cost_budget`) and there was no way to see how close you were, no warning before the wall, and the hard stop printed a bare `[Quota exceeded]`. This adds the UX layer on top of the unchanged engine: a **`/budget`** command — no args shows usage vs every budget as colored bars + percentages; **`/budget $5`** sets a session **cost** cap (the `$` means USD), **`/budget 200k`** a session **token** cap (parses `200k` / `1.5m` / `200000`), **`/budget daily $20`** / **`/budget daily 2m`** the daily caps, and **`/budget clear`** removes all. A **`--budget $5`** / **`--budget 200k`** startup flag sets the session cap at launch. **Proximity warnings** fire at the end of any turn that crosses **≥80%** (yellow) / **≥95%** (red) of a cap, so the wall never arrives by surprise. **On hit** the agent now yields a `QuotaPause` event (instead of a plain text line): the REPL **auto-saves the session** (`session_latest.json` + daily backup, the same path `/resume` reads) and prints a friendly next-steps block — raise the **same** cap or remove it (`/budget clear`) then resend, or restart later and `/resume`. So a long task that runs out of budget is never lost: you analyze, adjust, and continue. **Tight enforcement (no surprise overshoot):** the check projects the next request's *input* (`compaction.estimate_tokens`) and stops *before* the call if it would cross the cap, and clamps that call's `max_tokens` to the remaining headroom (`quota.output_room`) — so a single tool-heavy turn can't blow 40k→49k past the budget the way a pure "already-spent ≥ limit" check let it. **One budget per scope:** setting a cap *replaces* the other unit for that scope (`/budget $5` after `/budget 200k` switches the session cap to cost rather than stacking), so a leftover token cap can't silently keep blocking after you switch to a `$` cap. **Unit-matched hint:** `QuotaExceeded` / `QuotaPause` carry which cap broke (`key`/`scope`/`unit`/`limit`), so the "raise it" suggestion is in the *right* unit — a token cap shows `/budget 40k`, a daily cost cap shows `/budget daily $40` — instead of a generic `$` amount that wouldn't lift a token cap. New helpers `quota.parse_budget` / `fmt_amount` / `usage_vs_limits` / `warnings` / `output_room`; command in `commands/core.py:cmd_budget`; `QuotaPause` in `agent.py`; REPL handling + `--budget` in `cheetahclaws.py`; 42-case `tests/test_budget.py` (isolated quota dir, incl. a regression that the hint matches the breached unit and that switching units clears the stale cap). The daemon's conservative `serve`-mode defaults (200k tok / $2 per session, 2M / $20 per day) are unchanged — interactive stays unlimited by default, the server stays guard-railed. See [docs/guides/features.md](guides/features.md) · [docs/guides/reference.md](guides/reference.md). - June 5, 2026 (**v3.5.82**): **Adaptive Markdown streaming — live output that stays correct on every device.** In-place Rich Live redraw is great on capable terminals but breaks elsewhere: it was disabled wholesale over SSH (so SSH users got raw tokens with no formatting), and where it *did* run it could leave **duplicate or stale frames** — on macOS Terminal (which can't erase above the scroll boundary), over laggy network PTYs, or with **wide CJK / emoji text** whose display width a naive line-count gets wrong. The renderer now selects a **streaming tier per device** in `ui.render.auto_stream_mode(config)`: **`live`** — full in-place redraw, only on terminals known to handle cursor-up (local TTYs, and modern emulators *even over SSH*: iTerm2, WezTerm, Windows Terminal, VSCode, kitty, Alacritty, Ghostty, detected via `TERM_PROGRAM` / `TERM` / `WT_SESSION` / `KITTY_WINDOW_ID` / `ALACRITTY_WINDOW_ID` / `WEZTERM_PANE`); **`commit`** — **append-only progressive Markdown**, the safe default for unknown-SSH / Apple Terminal / pipes / non-TTY, where each completed block (split on blank lines, respecting open code fences so a fenced block renders atomically) is rendered and printed **permanently** and the cursor is **never moved**, making a duplicate frame structurally impossible regardless of terminal, latency, or character width; **`plain`** — raw tokens, only when `rich` is unavailable. The append-only floor is provably duplication-free; `live` is progressive enhancement on top. Override with **`/config stream_mode=live|commit|plain`** (legacy boolean **`/config rich_live=true|false`** still works → `live`/`commit`). Implemented in `ui/render.py` (`set_stream_mode` / `auto_stream_mode` / `_safe_commit_point` / `_commit_stream` / `_commit_flush`), wired in at REPL start in `cheetahclaws.py`, with a 26-case test suite in `tests/test_stream_modes.py` (device routing, code-fence-aware block boundaries, append-only commit, and a regression asserting commit mode emits **zero** cursor sequences even on a TTY with CJK text). Two related UX items shipped alongside: **`/context` is now a visual grid** — a Claude-Code-style 20×10 cell grid of context-window usage, colored and broken down by category (system prompt / system tools / memory files / skills / messages / free space) with per-category token counts and percentages, adapting to the model's real context window and falling back to `#`/`.` on non-UTF-8 terminals (`commands/core.py:cmd_context`); and **`deepseek-v4-flash` is registered at its 1M context window** in `providers._MODEL_CONTEXT_LIMITS` (overriding the 128K deepseek provider default, which still applies to `deepseek-chat` / `deepseek-v4-pro`), so the prompt `%`, `/context`, and the compaction trigger all reflect the true 1M window. See [docs/guides/features.md](guides/features.md) · [docs/guides/reference.md](guides/reference.md). diff --git a/examples/example-plugin/cmd.py b/examples/example-plugin/cmd.py index 7b70596d..7ffa8fd4 100644 --- a/examples/example-plugin/cmd.py +++ b/examples/example-plugin/cmd.py @@ -4,7 +4,7 @@ This file demonstrates how to define slash commands that users type in the REPL. Export your commands as a COMMAND_DEFS dict. """ -from ui.render import info, ok, warn, err +from cheetahclaws.ui.render import info, ok, warn, err def _cmd_example(args: str, state, config) -> bool: diff --git a/examples/example-plugin/tools.py b/examples/example-plugin/tools.py index cc9ce366..e0a1da08 100644 --- a/examples/example-plugin/tools.py +++ b/examples/example-plugin/tools.py @@ -4,7 +4,7 @@ This file demonstrates how to define tools that the AI can call. Export your tools as a TOOL_DEFS list — do NOT call register_tool() directly. """ -from tool_registry import ToolDef +from cheetahclaws.tool_registry import ToolDef def _example_search(params: dict, config: dict) -> str: diff --git a/examples/kernel_e2e_smoke.py b/examples/kernel_e2e_smoke.py index 80c7b443..11da5d72 100644 --- a/examples/kernel_e2e_smoke.py +++ b/examples/kernel_e2e_smoke.py @@ -29,10 +29,10 @@ import time from pathlib import Path -import kernel +from cheetahclaws import kernel # Module-level classes referenced inside _run_demo, where the `kernel` # param holds a Kernel *instance* and would otherwise shadow the module. -from kernel import ScheduleSpec, SandboxPolicy +from cheetahclaws.kernel import ScheduleSpec, SandboxPolicy def main() -> int: @@ -117,7 +117,7 @@ def _run_demo(kernel: kernel.Kernel) -> int: # ── Step 6: build worker loop (spawns echo runner) ────────────── worker = kernel.make_worker( argv_factory=lambda entry: [ - sys.executable, "-m", "kernel.runner.runner_main", + sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main", ], policy_factory=lambda entry: SandboxPolicy( wall_seconds=10, diff --git a/pyproject.toml b/pyproject.toml index 81ca723b..3c04496b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,66 +61,15 @@ Issues = "https://github.com/SafeRL-Lab/cheetahclaws/issues" Documentation = "https://github.com/SafeRL-Lab/cheetahclaws/tree/main/docs" [project.scripts] -cheetahclaws = "cheetahclaws:main" +cheetahclaws = "cheetahclaws.cli:main" -[tool.setuptools] -# Top-level single-file modules. NOTE: never list a name here that also -# exists as a directory package (e.g. "memory") — setuptools will refuse -# to build or silently drop unrelated packages on some platforms (Windows -# + Python 3.13 + setuptools ≥ 75 reported in #97). -py-modules = [ - "cheetahclaws", - "agent", - "agent_runner", - "auxiliary", - "bootstrap", - "circuit_breaker", - "cloudsave", - "compaction", - "config", - "context", - "error_classifier", - "health", - "jobs", - "logging_utils", - "providers", - "quota", - "runtime", - "session_store", - "skills", - "subagent", - "tmux_tools", - "tool_registry", -] - -# Packages — use `find` so any new sub-package added under modular/, etc. -# gets shipped automatically. The include patterns are deliberately -# explicit (no bare wildcard) so we don't accidentally ship tests/, demos/, -# docs/ etc. Add a new top-level package by appending one entry to -# `include`; sub-packages auto-discover via the trailing `*`. +# Everything ships inside the single top-level ``cheetahclaws`` package. This +# is the whole point of the package layout: a generic submodule name (config, +# daemon, kernel …) can never be shadowed by something else on sys.path once +# installed, because it is only ever imported as ``cheetahclaws.``. +# ``cheetahclaws*`` auto-discovers the package and every sub-package. [tool.setuptools.packages.find] -include = [ - "tools*", - "daemon*", - "kernel*", - "mcp_client*", - "memory*", - "monitor*", - "multi_agent*", - "plugin*", - "skill*", - "task*", - "voice*", - "video*", - "checkpoint*", - "ui*", - "web*", - "bridges*", - "commands*", - "research*", - "prompts*", - "modular*", -] +include = ["cheetahclaws*"] exclude = [ "tests*", "demos*", @@ -130,11 +79,14 @@ exclude = [ ] [tool.setuptools.package-data] -"web" = ["*.js", "*.css", "*.html", "*.ico", "*.png", "static/**/*"] -"prompts" = ["base/*.md", "overlays/*.md", "fragments/*.md", "README.md"] -"modular.video" = ["PLUGIN.md"] -"modular.voice" = ["PLUGIN.md"] -"modular.trading" = ["PLUGIN.md", "skills/*.md", "agent_templates/*.md"] +# agent_templates/ is plain data (markdown) loaded by filesystem path from +# agent_runner.py and research/lab/roles.py — ship it with the package. +"cheetahclaws" = ["agent_templates/**/*.md", "agent_templates/**/*"] +"cheetahclaws.web" = ["*.js", "*.css", "*.html", "*.ico", "*.png", "static/**/*"] +"cheetahclaws.prompts" = ["base/*.md", "overlays/*.md", "fragments/*.md", "README.md"] +"cheetahclaws.modular.video" = ["PLUGIN.md"] +"cheetahclaws.modular.voice" = ["PLUGIN.md"] +"cheetahclaws.modular.trading" = ["PLUGIN.md", "skills/*.md", "agent_templates/*.md"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/tests/conftest.py b/tests/conftest.py index 1afe87f4..9371b25b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,7 @@ def _isolate_lab_output_dir(tmp_path, monkeypatch): research/lab/ isn't on the import path (e.g. minimal CI matrices). """ try: - from research.lab import storage as _storage + from cheetahclaws.research.lab import storage as _storage except Exception: return sandbox_root = tmp_path / "lab_test_papers" @@ -35,7 +35,7 @@ def _isolate_lab_output_dir(tmp_path, monkeypatch): # output.py + sandbox.py both import the constant by reference, # so re-bind those module-level names too. try: - from research.lab import output as _output + from cheetahclaws.research.lab import output as _output monkeypatch.setattr(_output, "DEFAULT_OUTPUT_DIR", sandbox_root) except Exception: pass diff --git a/tests/e2e_checkpoint.py b/tests/e2e_checkpoint.py index 868c0ba3..69a4732f 100644 --- a/tests/e2e_checkpoint.py +++ b/tests/e2e_checkpoint.py @@ -9,12 +9,12 @@ print(f"Workspace: {tmpdir}") # Patch checkpoints root to temp -import checkpoint.store as store +import cheetahclaws.checkpoint.store as store _orig_root = store._checkpoints_root store._checkpoints_root = lambda: tmpdir / ".nano_claude" / "checkpoints" -import checkpoint as ckpt -from checkpoint.hooks import set_session, get_tracked_edits, reset_tracked, _backup_before_write +from cheetahclaws import checkpoint as ckpt +from cheetahclaws.checkpoint.hooks import set_session, get_tracked_edits, reset_tracked, _backup_before_write # ── Simulate AgentState ── @dataclass diff --git a/tests/e2e_compact.py b/tests/e2e_compact.py index 97e51544..1b16623a 100644 --- a/tests/e2e_compact.py +++ b/tests/e2e_compact.py @@ -20,7 +20,7 @@ class FakeState: def test_compact(): - from compaction import ( + from cheetahclaws.compaction import ( estimate_tokens, snip_old_tool_results, find_split_point, _restore_plan_context, manual_compact, ) @@ -84,7 +84,7 @@ def test_compact(): tmpdir = Path(tempfile.mkdtemp()) plan_file = tmpdir / "plan.md" plan_file.write_text("# Plan\n\n1. Do stuff\n2. More stuff\n", encoding="utf-8") - import runtime + from cheetahclaws import runtime config = {"permission_mode": "plan", "_session_id": "test_compact"} sctx = runtime.get_session_ctx("test_compact") sctx.plan_file = str(plan_file) @@ -132,8 +132,8 @@ def test_compact(): config = {"model": "test", "permission_mode": "auto"} # Mock the LLM call in compact_messages - import compaction - import providers + from cheetahclaws import compaction + from cheetahclaws import providers class FakeTextChunk: def __init__(self, text): diff --git a/tests/e2e_daemon_skeleton.py b/tests/e2e_daemon_skeleton.py index 7563b0e6..741c8479 100644 --- a/tests/e2e_daemon_skeleton.py +++ b/tests/e2e_daemon_skeleton.py @@ -42,7 +42,7 @@ def daemon_proc(tmp_path): env["XDG_RUNTIME_DIR"] = str(tmp_path / "xdg") proc = subprocess.Popen( - [sys.executable, "cheetahclaws.py", "serve", + [sys.executable, "-m", "cheetahclaws", "serve", "--listen", "tcp://127.0.0.1:0"], cwd=str(REPO_ROOT), env=env, @@ -99,7 +99,7 @@ def daemon_proc(tmp_path): def _post_rpc(address: str, token: str, method: str, params=None, *, timeout=3.0): - from daemon import API_VERSION, API_VERSION_HEADER + from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER host, port_s = address.rsplit(":", 1) body_obj = {"jsonrpc": "2.0", "id": 1, "method": method} if params is not None: @@ -125,7 +125,7 @@ def _get(address: str, path: str, *, token: str | None = None, if token is not None: headers["Authorization"] = f"Bearer {token}" if api_version: - from daemon import API_VERSION, API_VERSION_HEADER + from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER headers[API_VERSION_HEADER] = API_VERSION conn = http.client.HTTPConnection(host, int(port_s), timeout=timeout) try: @@ -142,7 +142,7 @@ def _run_subcommand(args: list[str], home: Path, *, timeout=10.0): env["USERPROFILE"] = str(home) env["XDG_RUNTIME_DIR"] = str(home / "xdg") proc = subprocess.run( - [sys.executable, "cheetahclaws.py", *args], + [sys.executable, "-m", "cheetahclaws", *args], cwd=str(REPO_ROOT), env=env, timeout=timeout, capture_output=True, ) @@ -230,7 +230,7 @@ def test_metrics_with_token_returns_real_payload(daemon_proc): # ── SSE ──────────────────────────────────────────────────────────────────── def test_events_stream_emits_heartbeat(daemon_proc): - from daemon import API_VERSION, API_VERSION_HEADER + from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER _proc, _home, address, token = daemon_proc host, port_s = address.rsplit(":", 1) conn = http.client.HTTPConnection(host, int(port_s), timeout=25.0) @@ -313,7 +313,7 @@ def _start_daemon(home: Path, *, wait_s: float = 10.0) -> tuple[subprocess.Popen env["USERPROFILE"] = str(home) env["XDG_RUNTIME_DIR"] = str(home / "xdg") proc = subprocess.Popen( - [sys.executable, "cheetahclaws.py", "serve", + [sys.executable, "-m", "cheetahclaws", "serve", "--listen", "tcp://127.0.0.1:0"], cwd=str(REPO_ROOT), env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -421,7 +421,7 @@ def test_events_persist_in_sqlite_across_daemon_restart(tmp_path): This is the headline F-2 user-visible win for SSE clients (Web UI / future bridges) that survive daemon restarts. """ - from daemon import API_VERSION, API_VERSION_HEADER + from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER # Boot A, publish a few events via echo.ping. proc1, addr1, token1 = _start_daemon(tmp_path) diff --git a/tests/e2e_f4_runner.py b/tests/e2e_f4_runner.py index 70506fb9..cc736ac7 100644 --- a/tests/e2e_f4_runner.py +++ b/tests/e2e_f4_runner.py @@ -54,7 +54,7 @@ def _make_template(tmp_dir: Path, name: str = "e2e_stub") -> str: def _isolate_schema(tmp_path: Path) -> Path: """Point :mod:`daemon.schema` at a fresh DB under tmp_path so the test sees its own agent_runs / agent_iterations rows.""" - from daemon import schema + from cheetahclaws.daemon import schema db = tmp_path / "sessions.db" schema.set_db_path(db) @@ -66,7 +66,7 @@ def _isolate_schema(tmp_path: Path) -> Path: def _restore_schema_default(): - from daemon import schema + from cheetahclaws.daemon import schema if hasattr(schema._local, "conn") and schema._local.conn is not None: schema._local.conn.close() @@ -113,7 +113,7 @@ def setUp(self): def tearDown(self): # Stop any leftover runner from this test method. - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs for h in list(rs.list_all()): try: @@ -131,7 +131,7 @@ def tearDown(self): # ── #1: SQLite agent_runs row created on start ──────────────────────── def test_start_creates_agent_runs_row(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = rs.start( name="e2e-row", @@ -162,7 +162,7 @@ def test_start_creates_agent_runs_row(self): # ── #2: iteration_done lands in agent_iterations + updates last_iter ── def test_iteration_lands_in_sqlite_under_real_runner(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = rs.start( name="e2e-iter", @@ -216,7 +216,7 @@ def test_iteration_lands_in_sqlite_under_real_runner(self): # ── #3: agent_runs status is finalised on graceful stop ─────────────── def test_graceful_stop_finalises_agent_runs_status(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = rs.start( name="e2e-final", @@ -257,7 +257,7 @@ def test_real_runner_permission_routing_round_trip(self): PermissionRequest from the agent. Originator's answer flows back to the runner via real IPC; the runner advances past it.""" os.environ["CHEETAHCLAWS_E2E_FAKE_PERMISSION"] = "1" - from daemon import permission, runner_supervisor as rs + from cheetahclaws.daemon import permission, runner_supervisor as rs store = permission.PermissionStore() store.start_janitor() diff --git a/tests/e2e_litellm_provider.py b/tests/e2e_litellm_provider.py index 4f074e42..a168470e 100644 --- a/tests/e2e_litellm_provider.py +++ b/tests/e2e_litellm_provider.py @@ -60,7 +60,7 @@ def provider(): # import). Catch the ProviderUnavailable raised on first SDK use # so we skip — rather than fail — when CC_LITELLM_E2E is set on a # box that doesn't actually have litellm installed. - from kernel.runner.llm.litellm_provider import LiteLLMProvider + from cheetahclaws.kernel.runner.llm.litellm_provider import LiteLLMProvider p = LiteLLMProvider() try: p._ensure_litellm() @@ -70,7 +70,7 @@ def provider(): def test_basic_call(provider): - from kernel.runner.llm.provider import LlmRequest + from cheetahclaws.kernel.runner.llm.provider import LlmRequest req = LlmRequest(model=_MODEL, user="What is 2+2? Reply with just the number.", max_tokens=10) @@ -83,7 +83,7 @@ def test_basic_call(provider): def test_streaming_emits_deltas(provider): - from kernel.runner.llm.provider import LlmRequest + from cheetahclaws.kernel.runner.llm.provider import LlmRequest received: list[str] = [] req = LlmRequest( @@ -100,7 +100,7 @@ def test_streaming_emits_deltas(provider): def test_system_prompt_steers_reply(provider): - from kernel.runner.llm.provider import LlmRequest + from cheetahclaws.kernel.runner.llm.provider import LlmRequest req = LlmRequest( model=_MODEL, diff --git a/tests/e2e_plan_mode.py b/tests/e2e_plan_mode.py index abfbcc38..a0e97b9d 100644 --- a/tests/e2e_plan_mode.py +++ b/tests/e2e_plan_mode.py @@ -37,7 +37,7 @@ def test_plan_mode(): state = FakeState() # Import _check_permission from agent - from agent import _check_permission + from cheetahclaws.agent import _check_permission print(" PASS") @@ -67,7 +67,7 @@ def test_plan_mode(): plan_path = plans_dir / "plantest.md" plan_path.write_text("# Plan: Add WebSocket support\n\n", encoding="utf-8") - import runtime + from cheetahclaws import runtime sctx = runtime.get_session_ctx("test_plan") config["_session_id"] = "test_plan" sctx.prev_permission_mode = config["permission_mode"] @@ -160,7 +160,7 @@ def test_plan_mode(): config["permission_mode"] = "plan" sctx.plan_file = str(plan_path) - from context import build_system_prompt + from cheetahclaws.context import build_system_prompt prompt = build_system_prompt(config) assert "Plan Mode (ACTIVE)" in prompt, "System prompt should include plan mode section" assert str(plan_path) in prompt, "System prompt should reference plan file path" diff --git a/tests/e2e_plan_tools.py b/tests/e2e_plan_tools.py index 304e6a6a..f8ff7aba 100644 --- a/tests/e2e_plan_tools.py +++ b/tests/e2e_plan_tools.py @@ -25,8 +25,8 @@ def test_plan_tools(): def _run(tmpdir): - from tools import _enter_plan_mode, _exit_plan_mode - from agent import _check_permission + from cheetahclaws.tools import _enter_plan_mode, _exit_plan_mode + from cheetahclaws.agent import _check_permission config = { "permission_mode": "auto", @@ -38,7 +38,7 @@ def _run(tmpdir): print("STEP 1: EnterPlanMode") print(SEP) result = _enter_plan_mode({"task_description": "Add WebSocket support"}, config) - import runtime + from cheetahclaws import runtime sctx = runtime.get_session_ctx("tooltest") assert config["permission_mode"] == "plan" assert sctx.plan_file @@ -151,7 +151,7 @@ def _run(tmpdir): print(f"\n{SEP}") print("STEP 8: System prompt includes plan mode guidance") print(SEP) - from context import build_system_prompt + from cheetahclaws.context import build_system_prompt config["permission_mode"] = "auto" prompt = build_system_prompt(config) assert "EnterPlanMode" in prompt diff --git a/tests/e2e_prompt_regression.py b/tests/e2e_prompt_regression.py index 096f0a03..a13f93f1 100644 --- a/tests/e2e_prompt_regression.py +++ b/tests/e2e_prompt_regression.py @@ -21,7 +21,7 @@ import pytest -import context as _context +from cheetahclaws import context as _context _FIXTURE = Path(__file__).parent / "fixtures" / "golden_default_prompt.txt" diff --git a/tests/e2e_slash_prompt.py b/tests/e2e_slash_prompt.py index 88865580..4ee4f53c 100644 --- a/tests/e2e_slash_prompt.py +++ b/tests/e2e_slash_prompt.py @@ -19,7 +19,7 @@ import pty import select -from ui.input import HAS_PROMPT_TOOLKIT +from cheetahclaws.ui.input import HAS_PROMPT_TOOLKIT if not HAS_PROMPT_TOOLKIT: pytest.skip("prompt_toolkit not installed", allow_module_level=True) @@ -31,7 +31,7 @@ _CHILD_SCRIPT = r""" import sys sys.path.insert(0, {repo_root!r}) -import ui.input as _ui +import cheetahclaws.ui.input as _ui _COMMANDS = {{"help": True, "clear": True, "checkpoint": True, "cwd": True, "compact": True, "config": True, "cost": True, "copy": True, diff --git a/tests/test_agent_nudge.py b/tests/test_agent_nudge.py index 09f1e9c0..625c6d7a 100644 --- a/tests/test_agent_nudge.py +++ b/tests/test_agent_nudge.py @@ -17,9 +17,9 @@ import pytest -import agent -from agent import _looks_like_investigation, AgentState, run -from providers import AssistantTurn, TextChunk +from cheetahclaws import agent +from cheetahclaws.agent import _looks_like_investigation, AgentState, run +from cheetahclaws.providers import AssistantTurn, TextChunk # ── Heuristic ──────────────────────────────────────────────────────────── diff --git a/tests/test_agent_output_path.py b/tests/test_agent_output_path.py index 2983d6c4..677e420a 100644 --- a/tests/test_agent_output_path.py +++ b/tests/test_agent_output_path.py @@ -15,8 +15,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import agent_runner -from commands.agent_cmd import _resolve_output_path +from cheetahclaws import agent_runner +from cheetahclaws.commands.agent_cmd import _resolve_output_path # ── _resolve_output_path ───────────────────────────────────────────────── diff --git a/tests/test_agent_runner_dup_stop.py b/tests/test_agent_runner_dup_stop.py index 984cdb1d..f6f7e80c 100644 --- a/tests/test_agent_runner_dup_stop.py +++ b/tests/test_agent_runner_dup_stop.py @@ -17,8 +17,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import agent_runner -from agent_runner import AgentRunner, _normalize_summary +from cheetahclaws import agent_runner +from cheetahclaws.agent_runner import AgentRunner, _normalize_summary # ── _normalize_summary unit tests ──────────────────────────────────────── @@ -72,7 +72,7 @@ def _fake_agent_run_factory(text_per_iter: Iterable[str]): """Build an `agent.run` replacement that yields the next canned text on every invocation. Each canned text becomes one TextChunk. """ - from agent import TextChunk + from cheetahclaws.agent import TextChunk iter_texts = iter(text_per_iter) @@ -109,7 +109,7 @@ def _run_with_canned_outputs(self, monkeypatch, tmp_path, outputs, dup_limit=3, max_seconds=5.0): """Helper: instantiate runner, patch agent.run, run loop in thread, wait until it stops or timeout, return runner.""" - import agent + from cheetahclaws import agent runner = _build_runner(tmp_path, dup_limit=dup_limit, interval=0.0) monkeypatch.setattr(agent, "run", _fake_agent_run_factory(outputs)) runner.start() diff --git a/tests/test_brainstorm_grounding.py b/tests/test_brainstorm_grounding.py index 97f6cd59..5b624572 100644 --- a/tests/test_brainstorm_grounding.py +++ b/tests/test_brainstorm_grounding.py @@ -18,8 +18,8 @@ import pytest -import commands.advanced as adv -from commands.advanced import ( +import cheetahclaws.commands.advanced as adv +from cheetahclaws.commands.advanced import ( _parse_ground_flag, _format_grounding_brief, _fetch_grounding, @@ -144,7 +144,7 @@ def test_format_grounding_brief_empty_results(): def test_fetch_grounding_returns_empty_on_research_exception(monkeypatch): """A flaky network or missing API keys must not break the brainstorm — _fetch_grounding swallows the exception and returns "".""" - import research.aggregator as _agg + import cheetahclaws.research.aggregator as _agg def raising_research(**kw): raise RuntimeError("network unreachable") @@ -158,7 +158,7 @@ def test_fetch_grounding_returns_empty_on_empty_brief(monkeypatch): """A brief with zero results (every source 429'd, etc.) is also a 'no grounding' case — return empty so the brainstorm continues un-grounded with a logged warning, not a crash.""" - import research.aggregator as _agg + import cheetahclaws.research.aggregator as _agg monkeypatch.setattr(_agg, "research", lambda **kw: _FakeBrief(results=[])) out = _fetch_grounding("topic", 15, {}) @@ -168,7 +168,7 @@ def test_fetch_grounding_returns_empty_on_empty_brief(monkeypatch): def test_fetch_grounding_returns_formatted_block_on_success(monkeypatch): """Happy path: research returns a brief, _fetch_grounding returns the formatted markdown ready to inline.""" - import research.aggregator as _agg + import cheetahclaws.research.aggregator as _agg fake_brief = _FakeBrief(results=[ _FakeResult("arxiv", "Real Paper", "https://arxiv.org/x", "real snippet", "academic", 0.9), diff --git a/tests/test_brainstorm_lead.py b/tests/test_brainstorm_lead.py index f44f44fc..5eb28db5 100644 --- a/tests/test_brainstorm_lead.py +++ b/tests/test_brainstorm_lead.py @@ -20,8 +20,8 @@ import pytest -import commands.advanced as adv -from commands.advanced import ( +import cheetahclaws.commands.advanced as adv +from cheetahclaws.commands.advanced import ( _parse_lead_flag, _parse_rounds_flag, _lead_opening, diff --git a/tests/test_brainstorm_models_flag.py b/tests/test_brainstorm_models_flag.py index d2db32d0..dfcbd6f9 100644 --- a/tests/test_brainstorm_models_flag.py +++ b/tests/test_brainstorm_models_flag.py @@ -16,7 +16,7 @@ import pytest -from commands.advanced import _parse_models_flag +from cheetahclaws.commands.advanced import _parse_models_flag @pytest.mark.parametrize("args,expected_models,expected_remaining", [ diff --git a/tests/test_brainstorm_quality_guards.py b/tests/test_brainstorm_quality_guards.py index e96e84de..96040547 100644 --- a/tests/test_brainstorm_quality_guards.py +++ b/tests/test_brainstorm_quality_guards.py @@ -17,7 +17,7 @@ import pytest -from commands.advanced import ( +from cheetahclaws.commands.advanced import ( _extract_challenge_blocks, _jaccard_similarity, _is_redundant_challenge, @@ -173,7 +173,7 @@ def test_is_weak_lead_model(model_id, expected): def test_lead_synthesis_accepts_opening(monkeypatch): """Backward compat: opening is optional. Existing callers passing only the original 4 args must still work.""" - import commands.advanced as adv + import cheetahclaws.commands.advanced as adv monkeypatch.setattr(adv, "_llm_oneshot", lambda *a, **kw: "## Consensus\n- ok") # Without opening @@ -189,7 +189,7 @@ def test_lead_synthesis_passes_opening_to_prompt(monkeypatch): """When opening is provided, it MUST appear in the user message so the model can self-check its action plan against the ban list.""" captured = {} - import commands.advanced as adv + import cheetahclaws.commands.advanced as adv def fake(model, sys, user, config, **kw): captured["user"] = user diff --git a/tests/test_brainstorm_v2_advanced.py b/tests/test_brainstorm_v2_advanced.py index 70c0e815..90c22aba 100644 --- a/tests/test_brainstorm_v2_advanced.py +++ b/tests/test_brainstorm_v2_advanced.py @@ -22,8 +22,8 @@ import pytest -import commands.advanced as adv -from commands.advanced import ( +import cheetahclaws.commands.advanced as adv +from cheetahclaws.commands.advanced import ( _DEFAULT_BAN_KEYWORDS, _extract_ban_keywords, _filter_action_plan, diff --git a/tests/test_bridge_slash_handler.py b/tests/test_bridge_slash_handler.py index b2e020f5..00c9a5f9 100644 --- a/tests/test_bridge_slash_handler.py +++ b/tests/test_bridge_slash_handler.py @@ -13,7 +13,7 @@ from unittest.mock import patch -import cheetahclaws +from cheetahclaws import cli as cheetahclaws def _make_handler(handle_slash_return, run_query_calls): @@ -104,7 +104,7 @@ def test_handler_is_assigned_in_headless_bridges_bootstrap(monkeypatch): """End-to-end pin: when _start_headless_bridges runs with bridge config present, session_ctx.handle_slash must be set to a callable. Pre-fix this attribute stayed None, which is the actual user bug.""" - import runtime + from cheetahclaws import runtime sid = "test-headless-slash-wire" config = { "_session_id": sid, @@ -112,7 +112,7 @@ def test_handler_is_assigned_in_headless_bridges_bootstrap(monkeypatch): "telegram_chat_id": 12345, } # Stub the actual bridge thread spawn so we don't make HTTP calls. - import cheetahclaws as cc + from cheetahclaws import cli as cc monkeypatch.setattr(cc._btg, "_telegram_thread", None) class _NoopThread: @@ -134,7 +134,7 @@ def test_headless_bootstrap_wires_tg_send(monkeypatch): session_ctx.tg_send is non-None. Headless bootstrap previously left it unset, so inline-keyboard approval prompts never reached the user — the bridge silently fell through to terminal input().""" - import runtime, cheetahclaws as cc + from cheetahclaws import runtime; from cheetahclaws import cli as cc sid = "test-headless-tgsend-wire" config = { "_session_id": sid, @@ -161,8 +161,8 @@ def test_headless_run_query_handles_permission_request(monkeypatch): PermissionRequest event for sensitive tools. Pre-fix that event was dropped, leaving event.granted=False, so every approval-required tool silently denied without ever asking the user.""" - import runtime, cheetahclaws as cc - from agent import PermissionRequest + from cheetahclaws import runtime; from cheetahclaws import cli as cc + from cheetahclaws.agent import PermissionRequest sid = "test-headless-permission-event" config = { @@ -184,10 +184,10 @@ def _fake_run(prompt, state, cfg, system_prompt): captured.append(req) yield req - monkeypatch.setattr("agent.run", _fake_run) + monkeypatch.setattr("cheetahclaws.agent.run", _fake_run) monkeypatch.setattr(cc, "ask_permission_interactive", lambda desc, cfg: True) - monkeypatch.setattr("context.build_system_prompt", lambda c: "sys") + monkeypatch.setattr("cheetahclaws.context.build_system_prompt", lambda c: "sys") cc._start_headless_bridges(config) ctx = runtime.get_session_ctx(sid) @@ -206,7 +206,7 @@ def test_headless_run_query_promotes_telegram_incoming(monkeypatch): must promote that to in_telegram_turn so _is_in_tg_turn() returns True while ask_input_interactive routes the prompt — otherwise prompts fall through to terminal input().""" - import runtime, cheetahclaws as cc + from cheetahclaws import runtime; from cheetahclaws import cli as cc sid = "test-headless-turn-promotion" config = { @@ -226,8 +226,8 @@ def _fake_run(prompt, state, cfg, system_prompt): seen_in_turn.append(runtime.get_session_ctx(sid).in_telegram_turn) return iter(()) # generator yielding nothing - monkeypatch.setattr("agent.run", _fake_run) - monkeypatch.setattr("context.build_system_prompt", lambda c: "sys") + monkeypatch.setattr("cheetahclaws.agent.run", _fake_run) + monkeypatch.setattr("cheetahclaws.context.build_system_prompt", lambda c: "sys") cc._start_headless_bridges(config) ctx = runtime.get_session_ctx(sid) diff --git a/tests/test_budget.py b/tests/test_budget.py index e9358f06..265a8ee7 100644 --- a/tests/test_budget.py +++ b/tests/test_budget.py @@ -6,7 +6,7 @@ """ import pytest -import quota +from cheetahclaws import quota @pytest.fixture(autouse=True) @@ -99,9 +99,9 @@ def test_warnings_thresholds(cost, level): @pytest.fixture def cmd(monkeypatch): - import config + from cheetahclaws import config monkeypatch.setattr(config, "save_config", lambda cfg: None) - from commands.core import cmd_budget + from cheetahclaws.commands.core import cmd_budget return cmd_budget @@ -158,7 +158,7 @@ def test_cmd_budget_view_runs_with_no_budgets(cmd, capsys): # ── QuotaPause event ──────────────────────────────────────────────────────── def test_quota_pause_event_shape(): - from agent import QuotaPause + from cheetahclaws.agent import QuotaPause ev = QuotaPause("Session cost budget reached", {"session_cost": 5.0}) assert ev.reason == "Session cost budget reached" assert ev.usage["session_cost"] == 5.0 @@ -245,7 +245,7 @@ def test_output_room_takes_tightest_of_multiple(): def test_output_room_cost_budget_uses_output_price(monkeypatch): # Model with a known $/Mtok output price → cost cap converts to token room. - import providers + from cheetahclaws import providers monkeypatch.setitem(providers.COSTS, "budgetmodel", (1.0, 2.0)) # $2 /Mtok out with quota._lock: quota._sess_cost["t"] = 0.0 diff --git a/tests/test_cache_tokens.py b/tests/test_cache_tokens.py index 570d6f59..75d6d62c 100644 --- a/tests/test_cache_tokens.py +++ b/tests/test_cache_tokens.py @@ -19,7 +19,7 @@ # ---------- 1 & 2: AssistantTurn + AgentState ---------- def test_assistant_turn_has_cache_fields(): - from providers import AssistantTurn + from cheetahclaws.providers import AssistantTurn turn = AssistantTurn( text="hello", tool_calls=[], in_tokens=100, out_tokens=50, cache_read_tokens=80, cache_write_tokens=20, @@ -30,14 +30,14 @@ def test_assistant_turn_has_cache_fields(): def test_assistant_turn_cache_defaults_zero(): """Older providers and ad-hoc callers construct AssistantTurn without cache fields.""" - from providers import AssistantTurn + from cheetahclaws.providers import AssistantTurn turn = AssistantTurn(text="hi", tool_calls=[], in_tokens=10, out_tokens=5) assert turn.cache_read_tokens == 0 assert turn.cache_write_tokens == 0 def test_agent_state_accumulates_cache_tokens(): - from agent import AgentState + from cheetahclaws.agent import AgentState state = AgentState() assert (state.total_cache_read_tokens, state.total_cache_write_tokens) == (0, 0) @@ -53,8 +53,8 @@ def test_agent_state_accumulates_cache_tokens(): # ---------- 3: Checkpoint persistence ---------- def test_checkpoint_snapshot_includes_cache(tmp_path, monkeypatch): - from checkpoint import store - from agent import AgentState + from cheetahclaws.checkpoint import store + from cheetahclaws.agent import AgentState monkeypatch.setattr(store, "_checkpoints_root", lambda: tmp_path / ".checkpoints") store.reset_file_versions() @@ -77,8 +77,8 @@ def test_rewind_restores_cache_tokens_from_snapshot(tmp_path, monkeypatch): """Rewinding to an older snapshot must restore cache totals in lock-step with input/output totals — otherwise the running counters drift away from what make_snapshot will persist on the next turn.""" - from checkpoint import store - from agent import AgentState + from cheetahclaws.checkpoint import store + from cheetahclaws.agent import AgentState monkeypatch.setattr(store, "_checkpoints_root", lambda: tmp_path / ".checkpoints") store.reset_file_versions() @@ -114,7 +114,7 @@ class TestAnthropicCacheExtraction: """_anthropic_cache_tokens must read cache_read_input_tokens / cache_creation_input_tokens.""" def test_returns_both_when_populated(self): - from providers import _anthropic_cache_tokens + from cheetahclaws.providers import _anthropic_cache_tokens usage = SimpleNamespace( input_tokens=120, output_tokens=40, cache_read_input_tokens=77, cache_creation_input_tokens=33, @@ -123,13 +123,13 @@ def test_returns_both_when_populated(self): def test_missing_fields_default_to_zero(self): """Older Anthropic SDKs and Bedrock-over-litellm wrappers omit the cache fields.""" - from providers import _anthropic_cache_tokens + from cheetahclaws.providers import _anthropic_cache_tokens usage = SimpleNamespace(input_tokens=10, output_tokens=5) assert _anthropic_cache_tokens(usage) == (0, 0) def test_none_fields_coerced_to_zero(self): """Anthropic occasionally returns None (JSON null) rather than omitting the field.""" - from providers import _anthropic_cache_tokens + from cheetahclaws.providers import _anthropic_cache_tokens usage = SimpleNamespace( input_tokens=10, output_tokens=5, cache_read_input_tokens=None, cache_creation_input_tokens=None, @@ -141,7 +141,7 @@ class TestOpenAICacheExtraction: """_openai_cached_read_tokens must walk prompt_tokens_details.cached_tokens.""" def test_reads_cached_tokens_from_details(self): - from providers import _openai_cached_read_tokens + from cheetahclaws.providers import _openai_cached_read_tokens usage = SimpleNamespace( prompt_tokens=100, completion_tokens=50, prompt_tokens_details=SimpleNamespace(cached_tokens=42), @@ -149,12 +149,12 @@ def test_reads_cached_tokens_from_details(self): assert _openai_cached_read_tokens(usage) == 42 def test_missing_details_returns_zero(self): - from providers import _openai_cached_read_tokens + from cheetahclaws.providers import _openai_cached_read_tokens usage = SimpleNamespace(prompt_tokens=100, completion_tokens=50) assert _openai_cached_read_tokens(usage) == 0 def test_none_cached_tokens_returns_zero(self): - from providers import _openai_cached_read_tokens + from cheetahclaws.providers import _openai_cached_read_tokens usage = SimpleNamespace( prompt_tokens=100, completion_tokens=50, prompt_tokens_details=SimpleNamespace(cached_tokens=None), @@ -164,7 +164,7 @@ def test_none_cached_tokens_returns_zero(self): def test_ollama_stream_never_reports_cache_tokens(): """Ollama has no prompt-caching; the path must yield 0/0 without raising.""" - from providers import AssistantTurn + from cheetahclaws.providers import AssistantTurn # stream_ollama yields AssistantTurn(text, tool_calls, 0, 0, 0, 0) -- we can't # reach the full HTTP call in a unit test, but we can assert the shape of the # yielded object the callers rely on. @@ -177,10 +177,10 @@ def test_ollama_stream_never_reports_cache_tokens(): def test_agent_run_propagates_cache_tokens_from_mocked_stream(monkeypatch, tmp_path): """Drive agent.run once with a scripted stream and assert totals + snapshot.""" - import tools as _tools_init # noqa: F401 - register tools - from agent import AgentState, run - from providers import AssistantTurn - from checkpoint import store as ck_store + from cheetahclaws import tools as _tools_init # noqa: F401 - register tools + from cheetahclaws.agent import AgentState, run + from cheetahclaws.providers import AssistantTurn + from cheetahclaws.checkpoint import store as ck_store monkeypatch.setattr(ck_store, "_checkpoints_root", lambda: tmp_path / ".checkpoints") ck_store.reset_file_versions() @@ -192,7 +192,7 @@ def fake_stream(**_kwargs): cache_read_tokens=700, cache_write_tokens=50, ) - monkeypatch.setattr("agent.stream", fake_stream) + monkeypatch.setattr("cheetahclaws.agent.stream", fake_stream) state = AgentState() list(run("hello", state, { @@ -210,9 +210,9 @@ def fake_stream(**_kwargs): def test_agent_run_accumulates_cache_across_multi_turn(monkeypatch): """Two consecutive agent.run calls must sum their cache counters in state.""" - import tools as _tools_init # noqa: F401 - from agent import AgentState, run - from providers import AssistantTurn + from cheetahclaws import tools as _tools_init # noqa: F401 + from cheetahclaws.agent import AgentState, run + from cheetahclaws.providers import AssistantTurn emitted = iter([ AssistantTurn("one", [], 100, 50, cache_read_tokens=40, cache_write_tokens=10), @@ -222,7 +222,7 @@ def test_agent_run_accumulates_cache_across_multi_turn(monkeypatch): def fake_stream(**_kwargs): yield next(emitted) - monkeypatch.setattr("agent.stream", fake_stream) + monkeypatch.setattr("cheetahclaws.agent.stream", fake_stream) state = AgentState() cfg = {"model": "test", "permission_mode": "accept-all", diff --git a/tests/test_checkpoint.py b/tests/test_checkpoint.py index e0c1f827..4ce80a8d 100644 --- a/tests/test_checkpoint.py +++ b/tests/test_checkpoint.py @@ -26,14 +26,14 @@ def tmp_home(tmp_path): """Redirect ~/.nano_claude/checkpoints to a temp directory.""" ckpt_root = tmp_path / ".nano_claude" / "checkpoints" ckpt_root.mkdir(parents=True) - with patch("checkpoint.store._checkpoints_root", return_value=ckpt_root): + with patch("cheetahclaws.checkpoint.store._checkpoints_root", return_value=ckpt_root): yield tmp_path, ckpt_root @pytest.fixture(autouse=True) def reset_versions(): """Reset file version counters between tests.""" - from checkpoint.store import reset_file_versions + from cheetahclaws.checkpoint.store import reset_file_versions reset_file_versions() yield reset_file_versions() @@ -43,7 +43,7 @@ def reset_versions(): class TestTypes: def test_file_backup_roundtrip(self): - from checkpoint.types import FileBackup + from cheetahclaws.checkpoint.types import FileBackup fb = FileBackup(backup_filename="abc123@v1", version=1, backup_time="2024-01-01T00:00:00") d = fb.to_dict() fb2 = FileBackup.from_dict(d) @@ -52,14 +52,14 @@ def test_file_backup_roundtrip(self): assert fb2.backup_time == fb.backup_time def test_file_backup_none_filename(self): - from checkpoint.types import FileBackup + from cheetahclaws.checkpoint.types import FileBackup fb = FileBackup(backup_filename=None, version=0, backup_time="2024-01-01T00:00:00") d = fb.to_dict() fb2 = FileBackup.from_dict(d) assert fb2.backup_filename is None def test_snapshot_roundtrip(self): - from checkpoint.types import Snapshot, FileBackup + from cheetahclaws.checkpoint.types import Snapshot, FileBackup fb = FileBackup(backup_filename="abc@v1", version=1, backup_time="2024-01-01") snap = Snapshot( id=1, session_id="test123", created_at="2024-01-01", @@ -81,7 +81,7 @@ def test_snapshot_roundtrip(self): class TestStore: def test_track_file_edit_existing_file(self, tmp_home, tmp_path): - from checkpoint import store + from cheetahclaws.checkpoint import store # Create a file to back up test_file = tmp_path / "hello.py" test_file.write_text("print('hello')", encoding="utf-8") @@ -97,19 +97,19 @@ def test_track_file_edit_existing_file(self, tmp_home, tmp_path): assert backup_file.read_text(encoding="utf-8") == "print('hello')" def test_track_file_edit_nonexistent(self, tmp_home, tmp_path): - from checkpoint import store + from cheetahclaws.checkpoint import store result = store.track_file_edit("sess1", str(tmp_path / "nope.py")) assert result is None def test_track_file_edit_large_file_skipped(self, tmp_home, tmp_path): - from checkpoint import store + from cheetahclaws.checkpoint import store big_file = tmp_path / "big.bin" big_file.write_bytes(b"x" * (2 * 1024 * 1024)) # 2MB result = store.track_file_edit("sess1", str(big_file)) assert result is None def test_make_snapshot_basic(self, tmp_home, tmp_path): - from checkpoint import store + from cheetahclaws.checkpoint import store state = FakeState( messages=[{"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello"}], turn_count=1, @@ -123,7 +123,7 @@ def test_make_snapshot_basic(self, tmp_home, tmp_path): assert snap.message_index == 2 def test_make_snapshot_incremental(self, tmp_home, tmp_path): - from checkpoint import store + from cheetahclaws.checkpoint import store test_file = tmp_path / "code.py" test_file.write_text("v1", encoding="utf-8") @@ -147,7 +147,7 @@ def test_make_snapshot_incremental(self, tmp_home, tmp_path): assert snap2.file_backups[str(test_file)].backup_filename == snap1.file_backups[str(test_file)].backup_filename def test_list_snapshots(self, tmp_home): - from checkpoint import store + from cheetahclaws.checkpoint import store state = FakeState(messages=[], turn_count=0) store.make_snapshot("sess1", state, {}, "one") store.make_snapshot("sess1", state, {}, "two") @@ -157,7 +157,7 @@ def test_list_snapshots(self, tmp_home): assert snaps[1]["id"] == 2 def test_get_snapshot(self, tmp_home): - from checkpoint import store + from cheetahclaws.checkpoint import store state = FakeState(messages=[], turn_count=0) store.make_snapshot("sess1", state, {}, "test") snap = store.get_snapshot("sess1", 1) @@ -166,7 +166,7 @@ def test_get_snapshot(self, tmp_home): assert store.get_snapshot("sess1", 99) is None def test_rewind_files(self, tmp_home, tmp_path): - from checkpoint import store + from cheetahclaws.checkpoint import store test_file = tmp_path / "code.py" test_file.write_text("original", encoding="utf-8") @@ -188,7 +188,7 @@ def test_rewind_files(self, tmp_home, tmp_path): assert test_file.read_text(encoding="utf-8") == "original" def test_rewind_deletes_new_file(self, tmp_home, tmp_path): - from checkpoint import store + from cheetahclaws.checkpoint import store new_file = tmp_path / "new.py" state = FakeState(messages=[], turn_count=0) @@ -208,8 +208,8 @@ def test_rewind_deletes_new_file(self, tmp_home, tmp_path): assert not new_file.exists() def test_max_snapshots_sliding_window(self, tmp_home): - from checkpoint import store - from checkpoint.types import MAX_SNAPSHOTS + from cheetahclaws.checkpoint import store + from cheetahclaws.checkpoint.types import MAX_SNAPSHOTS state = FakeState(messages=[], turn_count=0) for i in range(MAX_SNAPSHOTS + 10): store.make_snapshot("sess1", state, {}, f"snap {i}") @@ -217,7 +217,7 @@ def test_max_snapshots_sliding_window(self, tmp_home): assert len(snaps) == MAX_SNAPSHOTS def test_files_changed_since(self, tmp_home, tmp_path): - from checkpoint import store + from cheetahclaws.checkpoint import store f1 = tmp_path / "a.py" f1.write_text("a", encoding="utf-8") f2 = tmp_path / "b.py" @@ -235,14 +235,14 @@ def test_files_changed_since(self, tmp_home, tmp_path): # f1 was not changed after snapshot 1 (it was already in snap 1) def test_delete_session_checkpoints(self, tmp_home): - from checkpoint import store + from cheetahclaws.checkpoint import store state = FakeState(messages=[], turn_count=0) store.make_snapshot("sess1", state, {}, "test") assert store.delete_session_checkpoints("sess1") assert store.list_snapshots("sess1") == [] def test_cleanup_old_sessions(self, tmp_home): - from checkpoint import store + from cheetahclaws.checkpoint import store # Create a session dir and make it old old_dir = store._session_dir("old_sess") old_dir.mkdir(parents=True, exist_ok=True) @@ -258,7 +258,7 @@ def test_cleanup_old_sessions(self, tmp_home): class TestHooks: def test_set_session_and_tracking(self, tmp_home, tmp_path): - from checkpoint import hooks, store + from cheetahclaws.checkpoint import hooks, store hooks.set_session("sess_test") hooks.reset_tracked() @@ -276,7 +276,7 @@ def test_set_session_and_tracking(self, tmp_home, tmp_path): assert edits2 == edits def test_reset_tracked(self, tmp_home, tmp_path): - from checkpoint import hooks + from cheetahclaws.checkpoint import hooks hooks.set_session("sess_test2") hooks.reset_tracked() # clear state from previous test @@ -292,7 +292,7 @@ def test_reset_tracked(self, tmp_home, tmp_path): def test_install_hooks_wraps_tools(self): """Verify install_hooks wraps Write/Edit/NotebookEdit without error.""" - from checkpoint import hooks + from cheetahclaws.checkpoint import hooks # Hooks are already installed by tools.py import, just verify no crash # and that the function is idempotent hooks._hooks_installed = False @@ -305,7 +305,7 @@ def test_install_hooks_wraps_tools(self): class TestIntegration: def test_write_snapshot_rewind_cycle(self, tmp_home, tmp_path): """Simulate: write file → snapshot → modify → rewind → verify restored.""" - from checkpoint import store, hooks + from cheetahclaws.checkpoint import store, hooks session_id = "integ_test" hooks.set_session(session_id) @@ -364,7 +364,7 @@ def test_write_snapshot_rewind_cycle(self, tmp_home, tmp_path): def test_initial_snapshot(self, tmp_home): """Initial snapshot should be id=1 with empty messages and prompt '(initial state)'.""" - from checkpoint import store + from cheetahclaws.checkpoint import store state = FakeState(messages=[], turn_count=0) snap = store.make_snapshot("init_test", state, {}, "(initial state)", tracked_edits=None) @@ -376,7 +376,7 @@ def test_initial_snapshot(self, tmp_home): def test_throttle_skips_when_no_changes(self, tmp_home): """Snapshot should be skipped when no files changed and message_index is same.""" - from checkpoint import store + from cheetahclaws.checkpoint import store state = FakeState(messages=[], turn_count=0) # Initial snapshot @@ -391,7 +391,7 @@ def test_throttle_skips_when_no_changes(self, tmp_home): def test_throttle_creates_when_messages_grew(self, tmp_home): """Snapshot should be created when messages grew even without file changes.""" - from checkpoint import store + from cheetahclaws.checkpoint import store state = FakeState(messages=[], turn_count=0) store.make_snapshot("throttle2", state, {}, "(initial state)") @@ -414,7 +414,7 @@ def test_throttle_creates_when_messages_grew(self, tmp_home): def test_throttle_conversation_rewind_works(self, tmp_home): """After throttled snapshots, conversation rewind via message_index still works.""" - from checkpoint import store + from cheetahclaws.checkpoint import store state = FakeState(messages=[], turn_count=0) # Snap 1: initial diff --git a/tests/test_circuit_breaker.py b/tests/test_circuit_breaker.py index 4c2b4789..3f7e0c30 100644 --- a/tests/test_circuit_breaker.py +++ b/tests/test_circuit_breaker.py @@ -8,8 +8,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import circuit_breaker as cb -from circuit_breaker import CircuitBreaker, CircuitOpenError, State, get_breaker, reset_breaker +from cheetahclaws import circuit_breaker as cb +from cheetahclaws.circuit_breaker import CircuitBreaker, CircuitOpenError, State, get_breaker, reset_breaker _PROV = "test_provider" _CFG = {"circuit_failure_threshold": 3, diff --git a/tests/test_compaction.py b/tests/test_compaction.py index 548755e6..e7cd879c 100644 --- a/tests/test_compaction.py +++ b/tests/test_compaction.py @@ -7,7 +7,7 @@ # Ensure project root is on sys.path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from compaction import ( +from cheetahclaws.compaction import ( estimate_tokens, get_context_limit, snip_old_tool_results, diff --git a/tests/test_context_overflow_recovery.py b/tests/test_context_overflow_recovery.py index 7f494171..b6df5ae9 100644 --- a/tests/test_context_overflow_recovery.py +++ b/tests/test_context_overflow_recovery.py @@ -24,7 +24,7 @@ import pytest -from agent import _try_reduce_output_cap_from_error +from cheetahclaws.agent import _try_reduce_output_cap_from_error # ── _try_reduce_output_cap_from_error ─────────────────────────────────── diff --git a/tests/test_context_window_cap.py b/tests/test_context_window_cap.py index b0cab1da..b3ae157f 100644 --- a/tests/test_context_window_cap.py +++ b/tests/test_context_window_cap.py @@ -20,8 +20,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import providers -import compaction +from cheetahclaws import providers +from cheetahclaws import compaction # ── Known model context registry ───────────────────────────────────────── diff --git a/tests/test_daemon_agent_methods.py b/tests/test_daemon_agent_methods.py index 781a5d93..975cb2d8 100644 --- a/tests/test_daemon_agent_methods.py +++ b/tests/test_daemon_agent_methods.py @@ -33,15 +33,15 @@ def __init__(self, config=None): def _build_registry(state=None): """Fresh RpcRegistry with agent.* methods registered.""" - from daemon.rpc import RpcRegistry - from daemon import agent_methods + from cheetahclaws.daemon.rpc import RpcRegistry + from cheetahclaws.daemon import agent_methods reg = RpcRegistry() agent_methods.register(reg, state or _FakeDaemonState()) return reg def _ctx(): - from daemon.rpc import CallContext + from cheetahclaws.daemon.rpc import CallContext return CallContext(client_id="test", transport="unix", api_version="0") @@ -107,7 +107,7 @@ class TestListWhenEmpty(unittest.TestCase): def test_list_returns_empty(self): # Clear the registry first in case another test left something. - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor for h in list(runner_supervisor.list_all()): runner_supervisor._unregister(h.name) reg = _build_registry() @@ -145,8 +145,8 @@ class TestStopRunningEndToEnd(unittest.TestCase): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_list_and_stop_round_trip(self): import textwrap, subprocess, threading - from daemon import runner_supervisor as rs - from daemon.runner_ipc import JsonLineChannel + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon.runner_ipc import JsonLineChannel source = textwrap.dedent(""" import json, sys diff --git a/tests/test_daemon_bridge_methods.py b/tests/test_daemon_bridge_methods.py index 45bb25aa..a433599e 100644 --- a/tests/test_daemon_bridge_methods.py +++ b/tests/test_daemon_bridge_methods.py @@ -23,15 +23,15 @@ def __init__(self, config=None): def _build_registry(state=None): - from daemon.rpc import RpcRegistry - from daemon import bridge_methods + from cheetahclaws.daemon.rpc import RpcRegistry + from cheetahclaws.daemon import bridge_methods reg = RpcRegistry() bridge_methods.register(reg, state or _FakeDaemonState()) return reg def _ctx(): - from daemon.rpc import CallContext + from cheetahclaws.daemon.rpc import CallContext return CallContext(client_id="tester", transport="unix", api_version="0") @@ -45,8 +45,8 @@ def _call(reg, method, params=None): class _BridgeMethodsBase(unittest.TestCase): def setUp(self): - from daemon import schema - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import schema + from cheetahclaws.daemon import bridge_supervisor as bs self._tmpdir = tempfile.TemporaryDirectory() self._db_path = Path(self._tmpdir.name) / "test.db" @@ -68,8 +68,8 @@ def setUp(self): bs._handles.clear() def tearDown(self): - from daemon import schema - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import schema + from cheetahclaws.daemon import bridge_supervisor as bs with bs._handles_lock: for h in list(bs._handles.values()): @@ -156,7 +156,7 @@ def test_start_list_stop_with_flag_on(self): os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" ev = threading.Event() - with patch("bridges.telegram._tg_supervisor", + with patch("cheetahclaws.bridges.telegram._tg_supervisor", side_effect=lambda *a, **kw: ev.wait()): reg = _build_registry() try: @@ -204,11 +204,11 @@ def test_send_with_no_bridge_returns_delivered_false(self): self.assertEqual(result, {"kind": "telegram", "delivered": False}) def test_send_with_running_bridge_calls_sender(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" ev = threading.Event() - with patch("bridges.telegram._tg_supervisor", + with patch("cheetahclaws.bridges.telegram._tg_supervisor", side_effect=lambda *a, **kw: ev.wait()): handle = bs.start("telegram", {"telegram_token": "t", "telegram_chat_id": 1}) diff --git a/tests/test_daemon_bridge_phase2.py b/tests/test_daemon_bridge_phase2.py index 641af8cb..49438e40 100644 --- a/tests/test_daemon_bridge_phase2.py +++ b/tests/test_daemon_bridge_phase2.py @@ -34,7 +34,7 @@ def _setup_isolated(tmp_path: Path): - from daemon import schema, events, bridge_supervisor as bs + from cheetahclaws.daemon import schema, events, bridge_supervisor as bs schema.set_db_path(tmp_path / "test.db") schema._local.conn = None events.reset_bus_for_tests() @@ -48,7 +48,7 @@ def _setup_isolated(tmp_path: Path): def _teardown_isolated(): - from daemon import schema, events, bridge_supervisor as bs + from cheetahclaws.daemon import schema, events, bridge_supervisor as bs with bs._handles_lock: for h in list(bs._handles.values()): try: @@ -95,7 +95,7 @@ def tearDown(self): class TestSessionIdFormat(unittest.TestCase): def test_telegram_session_id(self): - from daemon.bridge_supervisor import BridgeHandle + from cheetahclaws.daemon.bridge_supervisor import BridgeHandle h = BridgeHandle( kind="telegram", config={"telegram_chat_id": 12345}, @@ -105,7 +105,7 @@ def test_telegram_session_id(self): self.assertEqual(h.session_id(), "tg:12345") def test_slack_session_id(self): - from daemon.bridge_supervisor import BridgeHandle + from cheetahclaws.daemon.bridge_supervisor import BridgeHandle h = BridgeHandle( kind="slack", config={"slack_channel": "C123ABC"}, @@ -115,7 +115,7 @@ def test_slack_session_id(self): self.assertEqual(h.session_id(), "sl:C123ABC") def test_wechat_session_id(self): - from daemon.bridge_supervisor import BridgeHandle + from cheetahclaws.daemon.bridge_supervisor import BridgeHandle h = BridgeHandle( kind="wechat", config={"wechat_user_id": "u_xyz"}, @@ -132,19 +132,19 @@ class TestPhase2OutboundDelivery(_Phase2Base): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_session_reply_forwards_to_sender(self): - from daemon import bridge_supervisor as bs - from daemon import events as _events + from cheetahclaws.daemon import bridge_supervisor as bs + from cheetahclaws.daemon import events as _events os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" sent: list[str] = [] # Stub the Telegram supervisor (legacy path) — we won't reach it # because we're enabling Phase 2, but the import path runs. - with patch("bridges.telegram._tg_supervisor", + with patch("cheetahclaws.bridges.telegram._tg_supervisor", side_effect=lambda *a, **kw: threading.Event().wait(60)): # Patch _tg_api to return no updates so the inbound poller # spins quietly while we exercise the outbound path. - with patch("bridges.telegram._tg_api", + with patch("cheetahclaws.bridges.telegram._tg_api", return_value={"ok": True, "result": []}): handle = bs.start("telegram", { "telegram_token": "fake", @@ -172,13 +172,13 @@ def test_session_reply_forwards_to_sender(self): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_outbound_ignores_other_sessions(self): - from daemon import bridge_supervisor as bs - from daemon import events as _events + from cheetahclaws.daemon import bridge_supervisor as bs + from cheetahclaws.daemon import events as _events os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" sent: list[str] = [] - with patch("bridges.telegram._tg_api", + with patch("cheetahclaws.bridges.telegram._tg_api", return_value={"ok": True, "result": []}): handle = bs.start("telegram", { "telegram_token": "fake", @@ -212,8 +212,8 @@ class TestPhase2InboundPublish(_Phase2Base): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_telegram_inbound_publishes_event(self): - from daemon import bridge_supervisor as bs - from daemon import events as _events + from cheetahclaws.daemon import bridge_supervisor as bs + from cheetahclaws.daemon import events as _events os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" # First call to _tg_api is flush (offset=-1, returns latest). @@ -238,7 +238,7 @@ def fake_tg_api(token, method, params=None): }]} return {"ok": True, "result": []} - with patch("bridges.telegram._tg_api", side_effect=fake_tg_api): + with patch("cheetahclaws.bridges.telegram._tg_api", side_effect=fake_tg_api): # Subscribe BEFORE start so we capture the inbound event. bus = _events.get_bus() q = bus.subscribe() @@ -277,8 +277,8 @@ class TestBridgeStartRpcPhase2(_Phase2Base): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_rpc_passes_daemon_phase2_through(self): os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" - from daemon.rpc import RpcRegistry, CallContext - from daemon import bridge_methods, bridge_supervisor as bs + from cheetahclaws.daemon.rpc import RpcRegistry, CallContext + from cheetahclaws.daemon import bridge_methods, bridge_supervisor as bs class _State: config = {} @@ -286,7 +286,7 @@ class _State: reg = RpcRegistry() bridge_methods.register(reg, _State()) - with patch("bridges.telegram._tg_api", + with patch("cheetahclaws.bridges.telegram._tg_api", return_value={"ok": True, "result": []}): envelope = {"jsonrpc": "2.0", "id": 1, "method": "bridge.start", "params": { diff --git a/tests/test_daemon_bridge_supervisor.py b/tests/test_daemon_bridge_supervisor.py index f08d8488..66ca5ee3 100644 --- a/tests/test_daemon_bridge_supervisor.py +++ b/tests/test_daemon_bridge_supervisor.py @@ -37,8 +37,8 @@ class _BridgeTestBase(unittest.TestCase): parallel test runs don't share state.""" def setUp(self): - from daemon import schema - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import schema + from cheetahclaws.daemon import bridge_supervisor as bs self._tmpdir = tempfile.TemporaryDirectory() self._db_path = Path(self._tmpdir.name) / "test.db" schema.set_db_path(self._db_path) @@ -62,8 +62,8 @@ def setUp(self): bs._handles.clear() def tearDown(self): - from daemon import schema - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import schema + from cheetahclaws.daemon import bridge_supervisor as bs with bs._handles_lock: for h in list(bs._handles.values()): @@ -98,18 +98,18 @@ def tearDown(self): class TestFeatureFlag(_BridgeTestBase): def test_enabled_default_off(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs self.assertFalse(bs.enabled("telegram")) self.assertFalse(bs.enabled("slack")) self.assertFalse(bs.enabled("wechat")) def test_enabled_unknown_kind_is_false(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs self.assertFalse(bs.enabled("discord")) self.assertFalse(bs.enabled("")) def test_enabled_via_env(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" self.assertTrue(bs.enabled("telegram")) # F-7 and F-8 stay off — flags are per-bridge. @@ -117,7 +117,7 @@ def test_enabled_via_env(self): self.assertFalse(bs.enabled("wechat")) def test_truthy_values(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs for v in ("1", "true", "TRUE", "yes", "on", " 1 "): os.environ["CHEETAHCLAWS_ENABLE_F6"] = v self.assertTrue(bs.enabled("telegram"), v) @@ -139,29 +139,29 @@ def _quiet_telegram_worker_stub(stop_event): class TestLifecycle(_BridgeTestBase): def test_start_without_flag_raises(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs with self.assertRaises(RuntimeError) as ctx: bs.start("telegram", {"telegram_token": "x", "telegram_chat_id": 1}) self.assertIn("CHEETAHCLAWS_ENABLE_F6", str(ctx.exception)) def test_start_unsupported_kind_raises(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs with self.assertRaises(ValueError): bs.start("discord", {}) def test_start_slack_without_telegram_flag_raises(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F7"] = "1" with self.assertRaises(RuntimeError) as ctx: bs.start("slack", {"slack_token": "x", "slack_channel": "c"}) self.assertIn("depends on F-6", str(ctx.exception)) def test_start_and_stop_telegram(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" # Patch the inner supervisor so we don't actually hit Telegram. - with patch("bridges.telegram._tg_supervisor", + with patch("cheetahclaws.bridges.telegram._tg_supervisor", side_effect=lambda *a, **kw: _quiet_telegram_worker_stub( a[2].get("_test_stop", threading.Event()) if len(a) > 2 and isinstance(a[2], dict) @@ -187,11 +187,11 @@ def test_start_and_stop_telegram(self): self.assertEqual(rows, [("telegram", 0)]) def test_double_start_raises(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" ev = threading.Event() - with patch("bridges.telegram._tg_supervisor", + with patch("cheetahclaws.bridges.telegram._tg_supervisor", side_effect=lambda *a, **kw: ev.wait()): try: bs.start("telegram", {"telegram_token": "fake", @@ -204,7 +204,7 @@ def test_double_start_raises(self): bs.stop("telegram", timeout_s=3.0) def test_stop_unknown_returns_false(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs self.assertFalse(bs.stop("telegram")) @@ -214,11 +214,11 @@ def test_stop_unknown_returns_false(self): class TestNotify(_BridgeTestBase): def test_notify_no_bridge_returns_false(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs self.assertFalse(bs.notify("telegram", "hello")) def test_notify_calls_sender(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" sent: list[tuple[dict, str]] = [] @@ -226,7 +226,7 @@ def fake_sender(cfg, text): sent.append((cfg, text)) return True - with patch("bridges.telegram._tg_supervisor", + with patch("cheetahclaws.bridges.telegram._tg_supervisor", side_effect=lambda *a, **kw: threading.Event().wait(0.5)): handle = bs.start("telegram", {"telegram_token": "x", "telegram_chat_id": 5}) @@ -239,11 +239,11 @@ def fake_sender(cfg, text): bs.stop("telegram", timeout_s=3.0) def test_notify_empty_text_is_dropped(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs self.assertFalse(bs.notify("telegram", "")) def test_notify_broadcast_delivers_to_every_live_bridge(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" os.environ["CHEETAHCLAWS_ENABLE_F7"] = "1" @@ -254,9 +254,9 @@ def tg_sender(cfg, text): def sl_sender(cfg, text): seen["slack"].append(text); return True - with patch("bridges.telegram._tg_supervisor", + with patch("cheetahclaws.bridges.telegram._tg_supervisor", side_effect=lambda *a, **kw: threading.Event().wait(0.5)), \ - patch("bridges.slack._slack_supervisor", + patch("cheetahclaws.bridges.slack._slack_supervisor", side_effect=lambda *a, **kw: threading.Event().wait(0.5)): tg = bs.start("telegram", {"telegram_token": "t", "telegram_chat_id": 1}) @@ -278,11 +278,11 @@ def sl_sender(cfg, text): class TestSqlitePersistence(_BridgeTestBase): def test_list_persisted_after_stop(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" ev = threading.Event() - with patch("bridges.telegram._tg_supervisor", + with patch("cheetahclaws.bridges.telegram._tg_supervisor", side_effect=lambda *a, **kw: ev.wait()): bs.start("telegram", {"telegram_token": "abcdef", "telegram_chat_id": 7}) @@ -301,14 +301,14 @@ def test_list_persisted_after_stop(self): def test_db_failure_does_not_raise(self): """A broken SQLite handle must not prevent start/stop from running — bridges work degraded but don't take down the daemon.""" - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs class _Handle: kind = "telegram" config = {"telegram_token": "t", "telegram_chat_id": 1} last_error = "" # Pass a partial handle so we exercise just the helper. - with patch("daemon.schema.get_conn", + with patch("cheetahclaws.daemon.schema.get_conn", side_effect=sqlite3.OperationalError("forced")): self.assertFalse(bs._db_upsert_bridge(_Handle(), enabled_flag=True)) self.assertFalse(bs._db_finalize_bridge(_Handle())) @@ -321,7 +321,7 @@ class _Handle: class TestConfigRedaction(unittest.TestCase): def test_token_redacted_chat_id_kept(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs safe = bs._safe_cfg({ "telegram_token": "1234567890:abcdefghijklmnop", "telegram_chat_id": 99, @@ -339,7 +339,7 @@ def test_provider_api_keys_also_redacted(self): ``openai_api_key`` / ``password`` / ``*_secret`` / ``auth_*``) must also be redacted before they hit the bus or the bridges SQLite row.""" - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs safe = bs._safe_cfg({ "anthropic_api_key": "sk-ant-aaaabbbbccccdddd", "openai_api_key": "sk-proj-eeeeffffgggghhhh", @@ -366,7 +366,7 @@ class TestSlackWorker(_BridgeTestBase): """F-7: same supervisor scaffolding, slack-specific imports + sender.""" def test_slack_requires_f6_flag_too(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F7"] = "1" # No F-6 flag → bridge_supervisor.start refuses. with self.assertRaises(RuntimeError) as ctx: @@ -374,7 +374,7 @@ def test_slack_requires_f6_flag_too(self): self.assertIn("F-6", str(ctx.exception)) def test_slack_worker_calls_slack_supervisor(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" os.environ["CHEETAHCLAWS_ENABLE_F7"] = "1" @@ -384,7 +384,7 @@ def fake_supervisor(token, channel, config): called.append((token, channel)) ev.wait() - with patch("bridges.slack._slack_supervisor", + with patch("cheetahclaws.bridges.slack._slack_supervisor", side_effect=fake_supervisor): handle = bs.start("slack", {"slack_token": "sl-tok", "slack_channel": "general"}) @@ -395,15 +395,15 @@ def fake_supervisor(token, channel, config): bs.stop("slack", timeout_s=3.0) def test_slack_sender_dispatches_outbound(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" os.environ["CHEETAHCLAWS_ENABLE_F7"] = "1" sent: list[tuple] = [] ev = threading.Event() - with patch("bridges.slack._slack_supervisor", + with patch("cheetahclaws.bridges.slack._slack_supervisor", side_effect=lambda *a, **kw: ev.wait()), \ - patch("bridges.slack._slack_send", + patch("cheetahclaws.bridges.slack._slack_send", side_effect=lambda tok, chan, text: sent.append( (tok, chan, text))): handle = bs.start("slack", {"slack_token": "tok", @@ -425,14 +425,14 @@ class TestWechatWorker(_BridgeTestBase): QR-login path; the worker surfaces a clear error if either is missing.""" def test_wechat_requires_f6_flag_too(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F8"] = "1" with self.assertRaises(RuntimeError) as ctx: bs.start("wechat", {"wechat_token": "x", "wechat_base_url": "u"}) self.assertIn("F-6", str(ctx.exception)) def test_wechat_worker_calls_wx_supervisor(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" os.environ["CHEETAHCLAWS_ENABLE_F8"] = "1" @@ -442,7 +442,7 @@ def fake_supervisor(token, base_url, config): called.append((token, base_url)) ev.wait() - with patch("bridges.wechat._wx_supervisor", side_effect=fake_supervisor): + with patch("cheetahclaws.bridges.wechat._wx_supervisor", side_effect=fake_supervisor): handle = bs.start("wechat", { "wechat_token": "wc-tok", "wechat_base_url": "http://localhost:1234", @@ -457,7 +457,7 @@ def test_wechat_worker_reports_missing_config(self): base_url (typical when /wechat login hasn't run), the worker exits cleanly with a clear last_error rather than blowing up deep inside _wx_supervisor's first HTTP call.""" - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" os.environ["CHEETAHCLAWS_ENABLE_F8"] = "1" @@ -473,15 +473,15 @@ def test_wechat_worker_reports_missing_config(self): self.assertIn("wechat config missing", handle.last_error) def test_wechat_sender_dispatches_outbound(self): - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import bridge_supervisor as bs os.environ["CHEETAHCLAWS_ENABLE_F6"] = "1" os.environ["CHEETAHCLAWS_ENABLE_F8"] = "1" sent: list = [] ev = threading.Event() - with patch("bridges.wechat._wx_supervisor", + with patch("cheetahclaws.bridges.wechat._wx_supervisor", side_effect=lambda *a, **kw: ev.wait()), \ - patch("bridges.wechat._wx_send", + patch("cheetahclaws.bridges.wechat._wx_send", side_effect=lambda user_id, text, cfg: sent.append( (user_id, text))): handle = bs.start("wechat", { diff --git a/tests/test_daemon_cli.py b/tests/test_daemon_cli.py index 9158c421..2542ba99 100644 --- a/tests/test_daemon_cli.py +++ b/tests/test_daemon_cli.py @@ -10,7 +10,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from daemon import cli +from cheetahclaws.daemon import cli # ── --help / -h ───────────────────────────────────────────────────────────── diff --git a/tests/test_daemon_cmd.py b/tests/test_daemon_cmd.py index 3f704a41..42cef22a 100644 --- a/tests/test_daemon_cmd.py +++ b/tests/test_daemon_cmd.py @@ -14,7 +14,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from commands import daemon_cmd +from cheetahclaws.commands import daemon_cmd # ── dispatch ─────────────────────────────────────────────────────────────── diff --git a/tests/test_daemon_discovery.py b/tests/test_daemon_discovery.py index 1da20d82..021d2a52 100644 --- a/tests/test_daemon_discovery.py +++ b/tests/test_daemon_discovery.py @@ -11,7 +11,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from daemon import discovery +from cheetahclaws.daemon import discovery # ── make_info ────────────────────────────────────────────────────────────── @@ -170,6 +170,6 @@ def test_locate_clears_stale_file_when_pid_dead(tmp_path: Path, monkeypatch): # ── default path ─────────────────────────────────────────────────────────── def test_get_default_path_lives_under_config_dir(): - from config import CONFIG_DIR + from cheetahclaws.config import CONFIG_DIR p = discovery.get_default_path() assert p == CONFIG_DIR / "daemon.json" diff --git a/tests/test_daemon_events_sqlite.py b/tests/test_daemon_events_sqlite.py index 90a8f5b6..e6a822df 100644 --- a/tests/test_daemon_events_sqlite.py +++ b/tests/test_daemon_events_sqlite.py @@ -14,7 +14,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from daemon import events, schema +from cheetahclaws.daemon import events, schema @pytest.fixture(autouse=True) diff --git a/tests/test_daemon_f9_budgets.py b/tests/test_daemon_f9_budgets.py index 4559cbf9..a0a33117 100644 --- a/tests/test_daemon_f9_budgets.py +++ b/tests/test_daemon_f9_budgets.py @@ -24,7 +24,7 @@ class TestApplyServeDefaults(unittest.TestCase): def test_all_none_flipped_to_conservative_defaults(self): - from daemon.cli import _apply_serve_defaults, F9_SERVE_BUDGET_DEFAULTS + from cheetahclaws.daemon.cli import _apply_serve_defaults, F9_SERVE_BUDGET_DEFAULTS cfg = { "session_token_budget": None, "session_cost_budget": None, @@ -40,7 +40,7 @@ def test_all_none_flipped_to_conservative_defaults(self): def test_existing_values_are_preserved(self): """An operator who already configured a budget keeps it. F-9 defaults only fill in the gaps.""" - from daemon.cli import _apply_serve_defaults + from cheetahclaws.daemon.cli import _apply_serve_defaults cfg = { "session_token_budget": 50, # user-chosen "session_cost_budget": None, # default applies @@ -55,7 +55,7 @@ def test_existing_values_are_preserved(self): self.assertEqual(cfg["session_cost_budget"], 2.0) def test_unrelated_keys_untouched(self): - from daemon.cli import _apply_serve_defaults + from cheetahclaws.daemon.cli import _apply_serve_defaults cfg = {"log_level": "info", "model": "claude-opus-4-7"} _apply_serve_defaults(cfg) self.assertEqual(cfg["log_level"], "info") @@ -73,15 +73,15 @@ def __init__(self, config=None): def _build_system_registry(state): - from daemon.rpc import RpcRegistry - from daemon import system_methods + from cheetahclaws.daemon.rpc import RpcRegistry + from cheetahclaws.daemon import system_methods reg = RpcRegistry() system_methods.register(reg, state) return reg def _ctx(): - from daemon.rpc import CallContext + from cheetahclaws.daemon.rpc import CallContext return CallContext(client_id="t", transport="unix", api_version="0") @@ -131,8 +131,8 @@ def test_status_includes_runner_and_bridge_counts(self): def _build_agent_registry(state): - from daemon.rpc import RpcRegistry - from daemon import agent_methods + from cheetahclaws.daemon.rpc import RpcRegistry + from cheetahclaws.daemon import agent_methods reg = RpcRegistry() agent_methods.register(reg, state) return reg @@ -208,7 +208,7 @@ def test_resume_with_name_calls_supervisor(self): from unittest.mock import patch state = _FakeDaemonState({}) reg = _build_agent_registry(state) - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs with patch.object(rs, "resume", return_value=True) as mock_resume: result, err = _call(reg, "agent.resume", { "name": "agent-x", diff --git a/tests/test_daemon_monitor_methods.py b/tests/test_daemon_monitor_methods.py index 90b63ea7..02c0c815 100644 --- a/tests/test_daemon_monitor_methods.py +++ b/tests/test_daemon_monitor_methods.py @@ -10,10 +10,10 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import monitor.store as store -import monitor.scheduler as scheduler -from daemon import events, monitor_methods, schema -from daemon.rpc import CallContext, RpcRegistry +import cheetahclaws.monitor.store as store +import cheetahclaws.monitor.scheduler as scheduler +from cheetahclaws.daemon import events, monitor_methods, schema +from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # ── Fakes / fixtures ────────────────────────────────────────────────────── @@ -170,7 +170,7 @@ def test_run_publishes_monitor_report_event(): # ── Coexistence with other registered methods ─────────────────────────── def test_register_does_not_clash_with_system_methods(): - from daemon import system_methods + from cheetahclaws.daemon import system_methods registry = RpcRegistry() system_methods.register(registry, _FakeState()) monitor_methods.register(registry, _FakeState()) diff --git a/tests/test_daemon_proactive.py b/tests/test_daemon_proactive.py index 75fe843d..22888a8b 100644 --- a/tests/test_daemon_proactive.py +++ b/tests/test_daemon_proactive.py @@ -23,7 +23,7 @@ def _isolate_schema(): """Point the schema at a fresh tmp DB. Returns (tmpdir, db_path).""" - from daemon import schema + from cheetahclaws.daemon import schema tmp = tempfile.TemporaryDirectory() db = Path(tmp.name) / "test.db" schema.set_db_path(db) @@ -35,7 +35,7 @@ def _isolate_schema(): def _restore_schema(): - from daemon import schema + from cheetahclaws.daemon import schema if hasattr(schema._local, "conn") and schema._local.conn is not None: schema._local.conn.close() schema._local.conn = None @@ -54,14 +54,14 @@ def tearDown(self): self._tmp.cleanup() def test_get_returns_defaults_on_empty_table(self): - from daemon import proactive_state + from cheetahclaws.daemon import proactive_state enabled, iv, last = proactive_state.get_state() self.assertFalse(enabled) self.assertEqual(iv, proactive_state.DEFAULT_INTERVAL_S) self.assertEqual(last, 0.0) def test_set_state_persists_and_round_trips(self): - from daemon import proactive_state + from cheetahclaws.daemon import proactive_state proactive_state.set_state(enabled=True, interval_s=120) enabled, iv, last = proactive_state.get_state() self.assertTrue(enabled) @@ -71,14 +71,14 @@ def test_set_state_persists_and_round_trips(self): self.assertGreater(last, time.time() - 5) def test_set_state_rejects_zero_or_negative_interval(self): - from daemon import proactive_state + from cheetahclaws.daemon import proactive_state with self.assertRaises(ValueError): proactive_state.set_state(enabled=True, interval_s=0) with self.assertRaises(ValueError): proactive_state.set_state(enabled=True, interval_s=-5) def test_disable_keeps_interval(self): - from daemon import proactive_state + from cheetahclaws.daemon import proactive_state proactive_state.set_state(enabled=True, interval_s=999) proactive_state.disable() enabled, iv, _ = proactive_state.get_state() @@ -86,7 +86,7 @@ def test_disable_keeps_interval(self): self.assertEqual(iv, 999) def test_tickle_bumps_last_tick_at(self): - from daemon import proactive_state + from cheetahclaws.daemon import proactive_state proactive_state.set_state(enabled=True, interval_s=60) time.sleep(0.05) old = proactive_state.get_state()[2] @@ -97,7 +97,7 @@ def test_tickle_bumps_last_tick_at(self): def test_corrupt_value_falls_back_to_defaults(self): """A malformed row from a prior buggy writer must not crash the scheduler — get_state() heals with defaults instead.""" - from daemon import proactive_state, schema + from cheetahclaws.daemon import proactive_state, schema conn = schema.get_conn() # Manually plant garbage in the interval field. conn.execute( @@ -117,20 +117,20 @@ def test_corrupt_value_falls_back_to_defaults(self): class TestProactiveScheduler(unittest.TestCase): def setUp(self): self._tmp, self._db = _isolate_schema() - from daemon import events + from cheetahclaws.daemon import events events.reset_bus_for_tests() # Sanity: make sure no scheduler from a prior test is still alive. - from daemon import proactive_scheduler as ps + from cheetahclaws.daemon import proactive_scheduler as ps ps.stop() def tearDown(self): - from daemon import proactive_scheduler as ps + from cheetahclaws.daemon import proactive_scheduler as ps ps.stop() _restore_schema() self._tmp.cleanup() def test_disabled_state_does_not_publish(self): - from daemon import ( + from cheetahclaws.daemon import ( events, proactive_state, proactive_scheduler as ps, ) proactive_state.set_state(enabled=False, interval_s=1) @@ -150,7 +150,7 @@ def test_disabled_state_does_not_publish(self): self.assertEqual(ticks, []) def test_enabled_publishes_after_idle_interval(self): - from daemon import ( + from cheetahclaws.daemon import ( events, proactive_state, proactive_scheduler as ps, ) # Subscribe BEFORE start so we don't miss the first tick. @@ -189,7 +189,7 @@ def test_owned_by_daemon_disables_foreign_check(self): 2. monkey-patching discovery.locate to return a fake foreign pid 3. confirming a tick still fires """ - from daemon import ( + from cheetahclaws.daemon import ( events, proactive_state, proactive_scheduler as ps, discovery, ) proactive_state.set_state(enabled=True, interval_s=1) @@ -218,7 +218,7 @@ def test_owned_by_daemon_disables_foreign_check(self): events.get_bus().unsubscribe(q) def test_stop_joins_within_5s(self): - from daemon import proactive_scheduler as ps + from cheetahclaws.daemon import proactive_scheduler as ps ps.start(owned_by_daemon=True) t0 = time.monotonic() self.assertTrue(ps.stop()) @@ -227,7 +227,7 @@ def test_stop_joins_within_5s(self): self.assertFalse(ps.is_running()) def test_double_start_returns_false(self): - from daemon import proactive_scheduler as ps + from cheetahclaws.daemon import proactive_scheduler as ps try: self.assertTrue(ps.start(owned_by_daemon=True)) self.assertFalse(ps.start(owned_by_daemon=True)) @@ -243,18 +243,18 @@ class TestProactiveRpc(unittest.TestCase): def setUp(self): self._tmp, self._db = _isolate_schema() - from daemon import events + from cheetahclaws.daemon import events events.reset_bus_for_tests() def tearDown(self): - from daemon import proactive_scheduler as ps + from cheetahclaws.daemon import proactive_scheduler as ps ps.stop() _restore_schema() self._tmp.cleanup() def _registry(self): - from daemon import proactive_methods - from daemon.rpc import RpcRegistry + from cheetahclaws.daemon import proactive_methods + from cheetahclaws.daemon.rpc import RpcRegistry class _FakeState: config = {} @@ -264,7 +264,7 @@ class _FakeState: return reg def _call(self, reg, method, params=None): - from daemon.rpc import CallContext + from cheetahclaws.daemon.rpc import CallContext envelope = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}} ctx = CallContext(client_id="t", transport="unix", api_version="0") @@ -310,7 +310,7 @@ def test_set_rejects_zero_interval(self): self.assertEqual(err["code"], -32602) def test_tickle_bumps_last_tick_at(self): - from daemon import proactive_state + from cheetahclaws.daemon import proactive_state reg = self._registry() proactive_state.set_state(enabled=True, interval_s=300) time.sleep(0.05) @@ -320,7 +320,7 @@ def test_tickle_bumps_last_tick_at(self): self.assertGreater(result["last_tick_at"], time.time() - 5) def test_get_reports_scheduler_state(self): - from daemon import proactive_scheduler as ps + from cheetahclaws.daemon import proactive_scheduler as ps reg = self._registry() try: ps.start(owned_by_daemon=True) @@ -342,7 +342,7 @@ class TestReplStepAside(unittest.TestCase): full loop is timing-sensitive and covered by integration testing.""" def test_foreign_daemon_helper_returns_false_when_none(self): - from daemon import discovery + from cheetahclaws.daemon import discovery orig = discovery.locate discovery.locate = lambda: None try: @@ -354,7 +354,7 @@ def test_foreign_daemon_helper_returns_false_when_none(self): discovery.locate = orig def test_foreign_daemon_helper_returns_true_for_other_pid(self): - from daemon import discovery + from cheetahclaws.daemon import discovery orig = discovery.locate discovery.locate = lambda: {"pid": 1, "address": "x:0"} try: @@ -364,7 +364,7 @@ def test_foreign_daemon_helper_returns_true_for_other_pid(self): discovery.locate = orig def test_foreign_daemon_helper_returns_false_for_own_pid(self): - from daemon import discovery + from cheetahclaws.daemon import discovery orig = discovery.locate discovery.locate = lambda: {"pid": os.getpid(), "address": "x:0"} try: diff --git a/tests/test_daemon_quota_pause.py b/tests/test_daemon_quota_pause.py index 7d84d5db..1fb6c7ca 100644 --- a/tests/test_daemon_quota_pause.py +++ b/tests/test_daemon_quota_pause.py @@ -54,8 +54,8 @@ def _send(o): class TestQuotaPauseIPC(unittest.TestCase): def setUp(self): - from daemon import schema - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import schema + from cheetahclaws.daemon import runner_supervisor as rs self._tmpdir = tempfile.TemporaryDirectory() self._db_path = Path(self._tmpdir.name) / "test.db" schema.set_db_path(self._db_path) @@ -64,8 +64,8 @@ def setUp(self): rs._handles.clear() def tearDown(self): - from daemon import schema - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import schema + from cheetahclaws.daemon import runner_supervisor as rs for h in list(rs._handles.values()): try: h.proc.kill() @@ -83,8 +83,8 @@ def tearDown(self): self._tmpdir.cleanup() def _spawn(self, name, source): - from daemon import runner_supervisor as rs - from daemon.runner_ipc import JsonLineChannel + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon.runner_ipc import JsonLineChannel proc = subprocess.Popen( [sys.executable, "-u", "-c", source], @@ -110,7 +110,7 @@ def _spawn(self, name, source): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_paused_budget_then_resume_roundtrip(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs # Capture bus events so we can assert quota_warn fired. events: list[tuple[str, dict]] = [] @@ -176,7 +176,7 @@ def _read_row(): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_resume_unknown_runner_returns_false(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs self.assertFalse(rs.resume("no-such-runner")) diff --git a/tests/test_daemon_runner_notify_routing.py b/tests/test_daemon_runner_notify_routing.py index 2c9ab373..4f012c06 100644 --- a/tests/test_daemon_runner_notify_routing.py +++ b/tests/test_daemon_runner_notify_routing.py @@ -41,8 +41,8 @@ def _send(o): def _spawn_inline_notify_runner(name="notifier"): - from daemon import runner_supervisor as rs - from daemon.runner_ipc import JsonLineChannel + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon.runner_ipc import JsonLineChannel proc = subprocess.Popen( [sys.executable, "-u", "-c", _NOTIFY_RUNNER], @@ -69,8 +69,8 @@ def _spawn_inline_notify_runner(name="notifier"): class TestNotifyRouting(unittest.TestCase): def setUp(self): - from daemon import runner_supervisor as rs - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon import bridge_supervisor as bs with rs._handles_lock: rs._handles.clear() with bs._handles_lock: @@ -82,8 +82,8 @@ def setUp(self): bs._handles.clear() def tearDown(self): - from daemon import runner_supervisor as rs - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon import bridge_supervisor as bs for h in list(rs._handles.values()): try: h.proc.kill() @@ -96,8 +96,8 @@ def tearDown(self): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_notify_forwards_to_bridge_supervisor(self): - from daemon import runner_supervisor as rs - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon import bridge_supervisor as bs calls: list[tuple[str, str]] = [] def fake_notify(kind, text): @@ -122,8 +122,8 @@ def test_notify_broadcast_when_bridge_unspecified(self): """A runner that omits the ``bridge`` field defaults to ``*`` broadcast, so the originator's bridge doesn't have to be threaded all the way down to the agent template.""" - from daemon import runner_supervisor as rs - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon import bridge_supervisor as bs # Runner variant without "bridge" key. source = textwrap.dedent(""" @@ -138,7 +138,7 @@ def _send(o): sys.exit(0) """).strip() - from daemon.runner_ipc import JsonLineChannel + from cheetahclaws.daemon.runner_ipc import JsonLineChannel proc = subprocess.Popen( [sys.executable, "-u", "-c", source], stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -173,8 +173,8 @@ def _send(o): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_notify_with_empty_text_is_skipped(self): - from daemon import runner_supervisor as rs - from daemon import bridge_supervisor as bs + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon import bridge_supervisor as bs source = textwrap.dedent(""" import json, sys @@ -189,7 +189,7 @@ def _send(o): sys.exit(0) """).strip() - from daemon.runner_ipc import JsonLineChannel + from cheetahclaws.daemon.runner_ipc import JsonLineChannel proc = subprocess.Popen( [sys.executable, "-u", "-c", source], stdin=subprocess.PIPE, stdout=subprocess.PIPE, diff --git a/tests/test_daemon_runner_permission_routing.py b/tests/test_daemon_runner_permission_routing.py index 74533190..9788a9fb 100644 --- a/tests/test_daemon_runner_permission_routing.py +++ b/tests/test_daemon_runner_permission_routing.py @@ -91,8 +91,8 @@ def _spawn_inline_runner(name, source, *, init_payload=None, originator=""): """Bypass start() and build a RunnerHandle on top of a -c subprocess, mirroring the helper used in test_daemon_runner_supervisor.py.""" - from daemon import runner_supervisor - from daemon.runner_ipc import JsonLineChannel + from cheetahclaws.daemon import runner_supervisor + from cheetahclaws.daemon.runner_ipc import JsonLineChannel proc = subprocess.Popen( [sys.executable, "-u", "-c", source], @@ -121,7 +121,7 @@ def _spawn_inline_runner(name, source, *, init_payload=None, def _stop_and_cleanup(name): - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor runner_supervisor.stop(name, timeout_s=3.0) @@ -132,7 +132,7 @@ class TestStoreOnAnswerCallback(unittest.TestCase): """The store's new on_answer hook is what makes routing possible.""" def test_answer_invokes_callback_with_request(self): - from daemon import events, permission + from cheetahclaws.daemon import events, permission events.reset_bus_for_tests() store = permission.PermissionStore() seen: list = [] @@ -146,7 +146,7 @@ def test_answer_invokes_callback_with_request(self): self.assertEqual(seen[0].answer, {"approve": True}) def test_callback_exception_does_not_propagate(self): - from daemon import events, permission + from cheetahclaws.daemon import events, permission events.reset_bus_for_tests() store = permission.PermissionStore() def _boom(_r): @@ -162,7 +162,7 @@ def _boom(_r): store.answer(req.request_id, "alice", {"approve": False}) def test_janitor_timeout_fires_callback_with_deny_answer(self): - from daemon import events, permission + from cheetahclaws.daemon import events, permission events.reset_bus_for_tests() store = permission.PermissionStore() store.start_janitor() @@ -196,7 +196,7 @@ def test_auto_approve_runner_gets_granted_true_without_store(self): """Back-compat: a runner started with auto_approve=True keeps seeing instant grants. PermissionStore is bypassed even when one is wired in.""" - from daemon import events, permission + from cheetahclaws.daemon import events, permission events.reset_bus_for_tests() store = permission.PermissionStore() store.start_janitor() @@ -229,7 +229,7 @@ def test_originator_approves_routes_grant_back_to_runner(self): """Slow path: with auto_approve=False + a store, the supervisor opens a pending request, the originator calls store.answer(), and the runner sees granted=True via permission_response.""" - from daemon import events, permission + from cheetahclaws.daemon import events, permission events.reset_bus_for_tests() store = permission.PermissionStore() store.start_janitor() @@ -287,7 +287,7 @@ def _spy_send(obj): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_originator_denies_routes_deny_back_to_runner(self): """Same flow, but the originator returns ``{"approve": False}``.""" - from daemon import events, permission + from cheetahclaws.daemon import events, permission events.reset_bus_for_tests() store = permission.PermissionStore() store.start_janitor() @@ -330,7 +330,7 @@ def test_non_originator_cannot_answer(self): """The store's existing NotOriginator guard still applies — a stranger cannot deliver a permission_response on behalf of the runner.""" - from daemon import events, permission + from cheetahclaws.daemon import events, permission events.reset_bus_for_tests() store = permission.PermissionStore() store.start_janitor() @@ -368,7 +368,7 @@ def test_janitor_timeout_delivers_deny_to_runner(self): """If no originator answers, the janitor's timeout path must still fire on_answer with approve=False so the runner unblocks instead of waiting forever.""" - from daemon import events, permission + from cheetahclaws.daemon import events, permission events.reset_bus_for_tests() store = permission.PermissionStore() # The supervisor calls store.create() without an explicit @@ -444,7 +444,7 @@ def _spy_send(obj): # Give the reader thread a moment to drain. deadline = time.monotonic() + 2.0 - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs while time.monotonic() < deadline: # If the runner exited or status changed, that's our cue. if not handle.is_alive(): @@ -466,8 +466,8 @@ class TestAgentStartWiresPermissionStore(unittest.TestCase): runner_supervisor.start to capture the kwargs.""" def test_agent_start_forwards_originator_and_store(self): - from daemon import agent_methods, permission - from daemon.rpc import RpcRegistry, CallContext + from cheetahclaws.daemon import agent_methods, permission + from cheetahclaws.daemon.rpc import RpcRegistry, CallContext class _FakeState: def __init__(self): @@ -478,7 +478,7 @@ def __init__(self): # Imported lazily so this test file works even before F-4 #3 # landed (i.e. RestartPolicy didn't yet exist). - from daemon import runner_supervisor as _rs_for_handle + from cheetahclaws.daemon import runner_supervisor as _rs_for_handle class _FakeHandle: name = "x"; run_id = "r"; pid = 1; status = "running" iteration = 0; started_at = 0.0; template_name = "demo" @@ -498,7 +498,7 @@ def _fake_start(**kw): reg = RpcRegistry() agent_methods.register(reg, state) - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs with mock.patch.object(rs, "start", side_effect=_fake_start): envelope = { "jsonrpc": "2.0", "id": 1, "method": "agent.start", diff --git a/tests/test_daemon_runner_restart_policy.py b/tests/test_daemon_runner_restart_policy.py index f6ad82af..7b201196 100644 --- a/tests/test_daemon_runner_restart_policy.py +++ b/tests/test_daemon_runner_restart_policy.py @@ -44,7 +44,7 @@ class TestRestartPolicyPure(unittest.TestCase): """``next_delay`` and ``from_params`` — no I/O, no Timer.""" def test_disabled_returns_none_regardless_of_count(self): - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy p = RestartPolicy.disabled() self.assertIsNone(p.next_delay(0)) self.assertIsNone(p.next_delay(5)) @@ -53,13 +53,13 @@ def test_disabled_returns_none_regardless_of_count(self): def test_mode_none_with_max_restarts_still_disabled(self): """A misconfigured caller that sets max_restarts but forgets mode='on-crash' must not silently auto-restart.""" - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy p = RestartPolicy(mode="none", max_restarts=5) self.assertIsNone(p.next_delay(0)) def test_exponential_backoff_capped(self): """Without jitter: 1, 2, 4, 8, … capped at cap_s.""" - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy p = RestartPolicy(mode="on-crash", max_restarts=10, backoff_base_s=1.0, backoff_cap_s=5.0, backoff_jitter_s=0.0) @@ -73,7 +73,7 @@ def test_exponential_backoff_capped(self): def test_jitter_stays_within_bounds(self): """Jitter must never produce a negative or huge delay.""" import random - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy p = RestartPolicy(mode="on-crash", max_restarts=100, backoff_base_s=1.0, backoff_cap_s=2.0, backoff_jitter_s=0.5) @@ -87,7 +87,7 @@ def test_jitter_stays_within_bounds(self): self.assertLessEqual(d, 2.5) def test_exhausted_after_max_restarts(self): - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy p = RestartPolicy(mode="on-crash", max_restarts=3, backoff_base_s=1.0, backoff_cap_s=10.0, backoff_jitter_s=0.0) @@ -99,13 +99,13 @@ def test_exhausted_after_max_restarts(self): self.assertIsNone(p.next_delay(3)) def test_from_params_defaults(self): - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy p = RestartPolicy.from_params({}) self.assertEqual(p.mode, "none") self.assertEqual(p.max_restarts, 0) def test_from_params_round_trip(self): - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy p = RestartPolicy.from_params({ "restart_policy": "on-crash", "max_restarts": 3, @@ -120,12 +120,12 @@ def test_from_params_round_trip(self): self.assertAlmostEqual(p.backoff_jitter_s, 0.1) def test_from_params_rejects_bad_mode(self): - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy with self.assertRaises(TypeError): RestartPolicy.from_params({"restart_policy": "always"}) def test_from_params_rejects_negative_max_restarts(self): - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy with self.assertRaises(TypeError): RestartPolicy.from_params({"restart_policy": "on-crash", "max_restarts": -1}) @@ -134,7 +134,7 @@ def test_from_params_rejects_cap_below_base(self): """The footgun: cap=0.1 < base=1.0 would mean every backoff gets clamped down and the policy "feels" disabled. Catch it at config time instead.""" - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy with self.assertRaises(TypeError): RestartPolicy.from_params({ "restart_policy": "on-crash", "max_restarts": 3, @@ -142,7 +142,7 @@ def test_from_params_rejects_cap_below_base(self): }) def test_from_params_rejects_non_numeric_backoff(self): - from daemon.runner_supervisor import RestartPolicy + from cheetahclaws.daemon.runner_supervisor import RestartPolicy with self.assertRaises(TypeError): RestartPolicy.from_params({"restart_policy": "on-crash", "max_restarts": 1, @@ -171,8 +171,8 @@ def _spawn_crashing_runner_with_policy(name, policy): handshake: stores ``_start_kwargs``, calls ``_register``, and spawns the reader thread. """ - from daemon import runner_supervisor as rs - from daemon.runner_ipc import JsonLineChannel + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon.runner_ipc import JsonLineChannel proc = subprocess.Popen( [sys.executable, "-u", "-c", _MOCK_CRASH_SOURCE], @@ -220,7 +220,7 @@ class TestRestartHookIntegration(unittest.TestCase): not actually fork another subprocess — just records the call.""" def setUp(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs # Wipe any handles left over from earlier tests (modules are # process-global; flakes here usually trace back to stragglers). with rs._handles_lock: @@ -228,7 +228,7 @@ def setUp(self): self._restore_spawner = rs._RESTART_SPAWNER def tearDown(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs rs._RESTART_SPAWNER = self._restore_spawner with rs._handles_lock: for h in list(rs._handles.values()): @@ -247,7 +247,7 @@ def tearDown(self): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_disabled_policy_does_not_schedule_restart(self): """Default policy: crash leaves status='crashed', no Timer.""" - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs spawn = MagicMock() rs._RESTART_SPAWNER = spawn @@ -267,7 +267,7 @@ def test_disabled_policy_does_not_schedule_restart(self): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_on_crash_schedules_restart(self): """mode='on-crash' with restarts left: Timer fires _RESTART_SPAWNER.""" - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs called: list[dict] = [] def fake_spawn(**kwargs): @@ -308,7 +308,7 @@ class _Stub: def test_max_restarts_exhausted_emits_event(self): """After max_restarts the lineage stops respawning and publishes agent_runner_restart_exhausted.""" - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs events: list[tuple[str, dict]] = [] @@ -354,13 +354,13 @@ class _FakeBus: class TestStopCancelsRestart(unittest.TestCase): def setUp(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs with rs._handles_lock: rs._handles.clear() self._restore_spawner = rs._RESTART_SPAWNER def tearDown(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs rs._RESTART_SPAWNER = self._restore_spawner with rs._handles_lock: for h in list(rs._handles.values()): @@ -380,7 +380,7 @@ def tearDown(self): def test_stop_cancels_pending_restart_timer(self): """A stop() arriving while a restart Timer is armed must cancel the Timer and avoid a respawn.""" - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs spawned = threading.Event() def fake_spawn(**_kwargs): @@ -438,8 +438,8 @@ class TestRestartHandleSerialisation(unittest.TestCase): """agent_methods._handle_to_dict surfaces restart_count/policy.""" def test_handle_dict_includes_restart_fields(self): - from daemon import runner_supervisor as rs - from daemon.agent_methods import _handle_to_dict + from cheetahclaws.daemon import runner_supervisor as rs + from cheetahclaws.daemon.agent_methods import _handle_to_dict class _FakeProc: def poll(self): return 0 @@ -468,17 +468,17 @@ class TestUnregisterIdentityGuard(unittest.TestCase): """ def setUp(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs with rs._handles_lock: rs._handles.clear() def tearDown(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs with rs._handles_lock: rs._handles.clear() def _fake_handle(self, name, run_id): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs class _FakeProc: def poll(self): return 0 return rs.RunnerHandle( @@ -488,7 +488,7 @@ def poll(self): return 0 ) def test_unregister_with_expected_only_pops_matching_handle(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs old = self._fake_handle("foo", "run_old") new = self._fake_handle("foo", "run_new") diff --git a/tests/test_daemon_runner_supervisor.py b/tests/test_daemon_runner_supervisor.py index 69eccaeb..d77835af 100644 --- a/tests/test_daemon_runner_supervisor.py +++ b/tests/test_daemon_runner_supervisor.py @@ -74,8 +74,8 @@ def _spawn_with_inline_runner(name, source): what we want to exercise; we just need any subprocess on the other end of the JsonLineChannel that speaks the protocol.""" import subprocess - from daemon import runner_supervisor - from daemon.runner_ipc import JsonLineChannel + from cheetahclaws.daemon import runner_supervisor + from cheetahclaws.daemon.runner_ipc import JsonLineChannel proc = subprocess.Popen( [sys.executable, "-u", "-c", source], @@ -109,7 +109,7 @@ class TestSupervisorBasics(unittest.TestCase): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_enabled_default_off(self): - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor # Clear the env var first so a stray export doesn't fool the test. old = os.environ.pop("CHEETAHCLAWS_ENABLE_F4", None) try: @@ -120,7 +120,7 @@ def test_enabled_default_off(self): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_enabled_via_env(self): - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor os.environ["CHEETAHCLAWS_ENABLE_F4"] = "1" try: self.assertTrue(runner_supervisor.enabled()) @@ -129,12 +129,12 @@ def test_enabled_via_env(self): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_get_returns_none_for_unknown(self): - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor self.assertIsNone(runner_supervisor.get("does-not-exist")) @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_stop_unknown_returns_false(self): - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor self.assertFalse(runner_supervisor.stop("does-not-exist")) @@ -146,7 +146,7 @@ def test_graceful_stop_within_5s(self): handle = _spawn_with_inline_runner("graceful", _MOCK_RUNNER_SOURCE) self.assertTrue(handle.is_alive()) - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor t0 = time.monotonic() self.assertTrue(runner_supervisor.stop("graceful", timeout_s=5.0)) elapsed = time.monotonic() - t0 @@ -163,7 +163,7 @@ def test_hanging_runner_escalates_to_sigkill(self): handle = _spawn_with_inline_runner("hang", _MOCK_HANG_SOURCE) self.assertTrue(handle.is_alive()) - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor t0 = time.monotonic() ok = runner_supervisor.stop("hang", timeout_s=5.0) elapsed = time.monotonic() - t0 @@ -217,7 +217,7 @@ def _send(o): "good iteration_done after a bad one wasn't applied") # And a graceful stop must still work. - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor self.assertTrue(runner_supervisor.stop("malformed", timeout_s=5.0)) self.assertFalse(handle.is_alive()) @@ -230,7 +230,7 @@ def test_uncaught_reader_exception_kills_proc_in_finally(self): proc.poll to return None forever (simulating a hung process), injecting an exception via the reader's IPC parse path. Realistically rare, but the safety net should still fire.""" - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor # Use the hanging-runner stand-in. handle = _spawn_with_inline_runner("safety-net", _MOCK_HANG_SOURCE) @@ -257,7 +257,7 @@ class TestSupervisorCrashDetection(unittest.TestCase): @unittest.skipIf(pytestmark_skipif_windows, "POSIX only") def test_kill_9_marks_handle_crashed(self): handle = _spawn_with_inline_runner("crashy", _MOCK_RUNNER_SOURCE) - from daemon import runner_supervisor + from cheetahclaws.daemon import runner_supervisor # SIGKILL from the outside — supervisor never asked for stop. os.killpg(os.getpgid(handle.pid), signal.SIGKILL) @@ -283,8 +283,8 @@ class TestIpcShim(unittest.TestCase): """Confirm daemon/runner_ipc.py re-exports the kernel implementation.""" def test_reexports_match_kernel(self): - from daemon import runner_ipc - from kernel.runner import ipc as kernel_ipc + from cheetahclaws.daemon import runner_ipc + from cheetahclaws.kernel.runner import ipc as kernel_ipc self.assertIs(runner_ipc.JsonLineChannel, kernel_ipc.JsonLineChannel) self.assertIs(runner_ipc.IpcReadTimeout, kernel_ipc.IpcReadTimeout) @@ -298,7 +298,7 @@ class TestSqlitePersistence(unittest.TestCase): def setUp(self): import tempfile - from daemon import schema + from cheetahclaws.daemon import schema self._tmpdir = tempfile.TemporaryDirectory() self._db_path = Path(self._tmpdir.name) / "test.db" schema.set_db_path(self._db_path) @@ -306,7 +306,7 @@ def setUp(self): # Lazy: schema is auto-inited on first get_conn() in the helpers. def tearDown(self): - from daemon import schema + from cheetahclaws.daemon import schema if hasattr(schema._local, "conn") and schema._local.conn is not None: schema._local.conn.close() schema._local.conn = None @@ -318,7 +318,7 @@ def _make_fake_handle(self, *, name="t", run_id="run_abcdef", template="demo", args="--foo bar"): """Build a RunnerHandle that has just enough state for the DB helpers — no subprocess, no IPC channel.""" - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs import subprocess as sp # A dummy popen object whose poll() returns 0 (so is_alive=False # is consistent). The DB helpers never touch proc/chan; we only @@ -344,7 +344,7 @@ def _query(self, sql, *params): # ── agent_runs insert ───────────────────────────────────────────────── def test_insert_agent_run_creates_row(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_one", template="t1", args="a1") self.assertTrue(rs._db_insert_agent_run(handle)) @@ -361,7 +361,7 @@ def test_insert_agent_run_creates_row(self): self.assertEqual(last_iter, 0) def test_insert_agent_run_idempotent_on_same_id(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_dup") self.assertTrue(rs._db_insert_agent_run(handle)) # Second call must not raise and must not duplicate. @@ -373,7 +373,7 @@ def test_insert_agent_run_idempotent_on_same_id(self): # ── agent_iterations insert + last_iteration update ────────────────── def test_insert_iteration_accumulates_rows(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_iter") rs._db_insert_agent_run(handle) for i in range(1, 4): @@ -395,7 +395,7 @@ def test_insert_iteration_accumulates_rows(self): self.assertEqual(last, 3) def test_insert_iteration_rejects_invalid_iteration_number(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_neg") rs._db_insert_agent_run(handle) # iteration 0 and negative are rejected (must be ≥ 1). @@ -409,7 +409,7 @@ def test_insert_iteration_rejects_invalid_iteration_number(self): def test_insert_iteration_idempotent_on_replay(self): """A delayed re-delivery of the same iteration_done must not double-count or downgrade last_iteration.""" - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_replay") rs._db_insert_agent_run(handle) rs._db_insert_iteration(handle, {"iteration": 5, "status": "ok", @@ -434,7 +434,7 @@ def test_insert_iteration_idempotent_on_replay(self): # ── finalize ───────────────────────────────────────────────────────── def test_finalize_run_marks_stopped(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_stopped") rs._db_insert_agent_run(handle) self.assertTrue(rs._db_finalize_run(handle, status="stopped")) @@ -447,7 +447,7 @@ def test_finalize_run_marks_stopped(self): self.assertIsNone(err) def test_finalize_run_marks_crashed_with_error(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_crashed") rs._db_insert_agent_run(handle) self.assertTrue(rs._db_finalize_run( @@ -462,7 +462,7 @@ def test_finalize_run_marks_crashed_with_error(self): self.assertIn("killed", err) def test_finalize_rejects_unknown_status(self): - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_bad") rs._db_insert_agent_run(handle) self.assertFalse(rs._db_finalize_run(handle, status="weird")) @@ -476,13 +476,13 @@ def test_finalize_rejects_unknown_status(self): def test_db_failure_does_not_raise(self): """All three helpers must swallow exceptions — the supervisor thread cannot die from a transient DB error.""" - from daemon import runner_supervisor as rs + from cheetahclaws.daemon import runner_supervisor as rs handle = self._make_fake_handle(run_id="run_dberr") def _raising_get_conn(): raise sqlite3.OperationalError("forced for test") - with patch("daemon.schema.get_conn", side_effect=_raising_get_conn): + with patch("cheetahclaws.daemon.schema.get_conn", side_effect=_raising_get_conn): self.assertFalse(rs._db_insert_agent_run(handle)) self.assertFalse(rs._db_insert_iteration( handle, {"iteration": 1, "status": "ok", diff --git a/tests/test_daemon_schema.py b/tests/test_daemon_schema.py index 4f525cf7..d7deb31b 100644 --- a/tests/test_daemon_schema.py +++ b/tests/test_daemon_schema.py @@ -10,7 +10,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from daemon import schema +from cheetahclaws.daemon import schema # ── Idempotent init ──────────────────────────────────────────────────────── diff --git a/tests/test_daemon_session_methods.py b/tests/test_daemon_session_methods.py index b954488e..112dcf2b 100644 --- a/tests/test_daemon_session_methods.py +++ b/tests/test_daemon_session_methods.py @@ -26,14 +26,14 @@ def _setup_bus(tmp_path: Path): """Reinit the event bus on a tmpdir-backed SQLite so the in-memory LRU and the SSE feed don't bleed between tests.""" - from daemon import schema, events + from cheetahclaws.daemon import schema, events schema.set_db_path(tmp_path / "test.db") schema._local.conn = None events.reset_bus_for_tests() def _teardown_bus(): - from daemon import schema, events + from cheetahclaws.daemon import schema, events events.reset_bus_for_tests() if hasattr(schema._local, "conn") and schema._local.conn is not None: try: @@ -50,15 +50,15 @@ def __init__(self, config=None): def _build_registry(): - from daemon.rpc import RpcRegistry - from daemon import session_methods + from cheetahclaws.daemon.rpc import RpcRegistry + from cheetahclaws.daemon import session_methods reg = RpcRegistry() session_methods.register(reg, _FakeState()) return reg def _ctx(client_id="bridge:tg:99"): - from daemon.rpc import CallContext + from cheetahclaws.daemon.rpc import CallContext return CallContext(client_id=client_id, transport="unix", api_version="0") @@ -74,7 +74,7 @@ def setUp(self): self._tmpdir = tempfile.TemporaryDirectory() _setup_bus(Path(self._tmpdir.name)) # Wipe the LRU so test ordering doesn't leak. - from daemon import session_methods + from cheetahclaws.daemon import session_methods session_methods._RECENT_LRU.clear() def tearDown(self): @@ -85,7 +85,7 @@ def tearDown(self): class TestSessionSend(_SessionTestBase): def test_send_publishes_session_inbound(self): - from daemon import events + from cheetahclaws.daemon import events bus = events.get_bus() q = bus.subscribe() try: @@ -108,7 +108,7 @@ def test_send_publishes_session_inbound(self): bus.unsubscribe(q) def test_send_explicit_origin_overrides_client_id(self): - from daemon import events + from cheetahclaws.daemon import events bus = events.get_bus() q = bus.subscribe() try: @@ -155,7 +155,7 @@ def test_send_returns_custom_message_id(self): class TestSessionReply(_SessionTestBase): def test_reply_publishes_session_outbound(self): - from daemon import events + from cheetahclaws.daemon import events bus = events.get_bus() q = bus.subscribe() try: diff --git a/tests/test_daemon_spike.py b/tests/test_daemon_spike.py index 365adbc0..d5c9df16 100644 --- a/tests/test_daemon_spike.py +++ b/tests/test_daemon_spike.py @@ -15,16 +15,16 @@ import pytest -from daemon import API_VERSION, API_VERSION_HEADER, events -from daemon.events import EventBus -from daemon.originator import ( +from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER, events +from cheetahclaws.daemon.events import EventBus +from cheetahclaws.daemon.originator import ( CLIENT_ID_HEADER, CLIENT_KIND_HEADER, OriginatorStore, ) -from daemon.permission import ( +from cheetahclaws.daemon.permission import ( PermissionStore, NotOriginator, UnknownRequest, DEFAULT_TIMEOUT_INTERACTIVE_S, ) -from daemon.server import make_tcp_server +from cheetahclaws.daemon.server import make_tcp_server # ── Fixtures ───────────────────────────────────────────────────────────────── @@ -268,7 +268,7 @@ def test_sse_heartbeat_arrives(daemon_tcp): """ host, port, token, _ = daemon_tcp # Speed up heartbeats for the test so it doesn't take 15s. - from daemon import server as _srv + from cheetahclaws.daemon import server as _srv original = _srv.HEARTBEAT_INTERVAL_S _srv.HEARTBEAT_INTERVAL_S = 0.5 try: diff --git a/tests/test_daemon_system_methods.py b/tests/test_daemon_system_methods.py index 01717fd6..fc02ea2f 100644 --- a/tests/test_daemon_system_methods.py +++ b/tests/test_daemon_system_methods.py @@ -7,8 +7,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from daemon import system_methods -from daemon.rpc import CallContext, RpcRegistry +from cheetahclaws.daemon import system_methods +from cheetahclaws.daemon.rpc import CallContext, RpcRegistry # ── Fake DaemonState that exposes just the surface system_methods touches ── @@ -87,8 +87,8 @@ def test_system_shutdown_returned_from_dispatch_with_correct_envelope(): def test_register_does_not_clash_with_spike_methods(): """Spike's `register_methods` is invoked first by DaemonState.__init__; system_methods.register must not collide with any spike-defined names.""" - from daemon.methods import register as register_spike - from daemon.permission import PermissionStore + from cheetahclaws.daemon.methods import register as register_spike + from cheetahclaws.daemon.permission import PermissionStore registry = RpcRegistry() store = PermissionStore() diff --git a/tests/test_deepseek_thinking.py b/tests/test_deepseek_thinking.py index 873305a0..1ea7545c 100644 --- a/tests/test_deepseek_thinking.py +++ b/tests/test_deepseek_thinking.py @@ -13,7 +13,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from config import DEFAULTS +from cheetahclaws.config import DEFAULTS class TestThinkingDefault: diff --git a/tests/test_diff_view.py b/tests/test_diff_view.py index 0505c7d5..c2d6b6c4 100644 --- a/tests/test_diff_view.py +++ b/tests/test_diff_view.py @@ -3,7 +3,7 @@ import pytest def test_generate_unified_diff(): - from tools import generate_unified_diff + from cheetahclaws.tools import generate_unified_diff old = "line1\nline2\nline3\n" new = "line1\nline2_modified\nline3\n" diff = generate_unified_diff(old, new, "test.py") @@ -13,12 +13,12 @@ def test_generate_unified_diff(): assert "+line2_modified" in diff def test_generate_unified_diff_empty_old(): - from tools import generate_unified_diff + from cheetahclaws.tools import generate_unified_diff diff = generate_unified_diff("", "new content\n", "test.py") assert "+new content" in diff def test_edit_returns_diff(tmp_path): - from tools import _edit + from cheetahclaws.tools import _edit f = tmp_path / "test.txt" f.write_text("hello world\n") result = _edit(str(f), "hello", "goodbye") @@ -26,7 +26,7 @@ def test_edit_returns_diff(tmp_path): assert "+goodbye world" in result def test_write_existing_returns_diff(tmp_path): - from tools import _write + from cheetahclaws.tools import _write f = tmp_path / "test.txt" f.write_text("old content\n") result = _write(str(f), "new content\n") @@ -34,14 +34,14 @@ def test_write_existing_returns_diff(tmp_path): assert "+new content" in result def test_write_new_file_no_diff(tmp_path): - from tools import _write + from cheetahclaws.tools import _write f = tmp_path / "new.txt" result = _write(str(f), "content\n") assert "Created" in result assert "---" not in result def test_diff_truncation(): - from tools import generate_unified_diff, maybe_truncate_diff + from cheetahclaws.tools import generate_unified_diff, maybe_truncate_diff old = "\n".join(f"line{i}" for i in range(200)) new = "\n".join(f"CHANGED{i}" for i in range(200)) diff = generate_unified_diff(old, new, "big.py") diff --git a/tests/test_fanout.py b/tests/test_fanout.py index ef4c2ce2..5ced98af 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -15,7 +15,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from multi_agent.fanout import ( +from cheetahclaws.multi_agent.fanout import ( DEFAULT_FANOUT_TOOLS, chunk_text, coalesce_chunks, diff --git a/tests/test_health_payloads.py b/tests/test_health_payloads.py index efb7937f..5a622084 100644 --- a/tests/test_health_payloads.py +++ b/tests/test_health_payloads.py @@ -12,7 +12,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import health +from cheetahclaws import health def test_healthz_payload_basic_shape(): diff --git a/tests/test_input_completer.py b/tests/test_input_completer.py index 9ecbe33e..78679f4e 100644 --- a/tests/test_input_completer.py +++ b/tests/test_input_completer.py @@ -8,7 +8,7 @@ import pytest -from ui.input import HAS_PROMPT_TOOLKIT, SlashCompleter +from cheetahclaws.ui.input import HAS_PROMPT_TOOLKIT, SlashCompleter if not HAS_PROMPT_TOOLKIT: pytest.skip("prompt_toolkit not installed", allow_module_level=True) @@ -136,7 +136,7 @@ def test_symmetry_commands_only_also_visible(): def test_setup_registers_module_level_providers(): """Verify ui.input.setup() injects providers without requiring ctor args.""" - import ui.input as ui_input + import cheetahclaws.ui.input as ui_input cmds = {"alpha": True, "beta": True} meta = {"alpha": ("A", []), "beta": ("B", [])} @@ -153,7 +153,7 @@ def test_setup_registers_module_level_providers(): def test_module_does_not_import_cheetahclaws(): """Regression guard for the circular-import concern from review.""" import sys - import ui.input as ui_input + import cheetahclaws.ui.input as ui_input # Reload ui.input in a clean state and confirm cheetahclaws is not pulled in. # (Running this in the test session where cheetahclaws may already be loaded # is acceptable — the assertion is about ui.input's own import graph.) diff --git a/tests/test_jobs_sqlite.py b/tests/test_jobs_sqlite.py index 0c43cbf1..076c8236 100644 --- a/tests/test_jobs_sqlite.py +++ b/tests/test_jobs_sqlite.py @@ -11,8 +11,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import jobs -from daemon import schema +from cheetahclaws import jobs +from cheetahclaws.daemon import schema @pytest.fixture(autouse=True) diff --git a/tests/test_kernel_agentfs.py b/tests/test_kernel_agentfs.py index 28dde44a..e7511560 100644 --- a/tests/test_kernel_agentfs.py +++ b/tests/test_kernel_agentfs.py @@ -5,7 +5,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentFSStore, DEFAULT_MAX_OBJECT_BYTES, FsAlreadyExists, diff --git a/tests/test_kernel_agentfs_daemon.py b/tests/test_kernel_agentfs_daemon.py index 87ebe392..2cea9545 100644 --- a/tests/test_kernel_agentfs_daemon.py +++ b/tests/test_kernel_agentfs_daemon.py @@ -14,12 +14,12 @@ import pytest -from daemon import API_VERSION, API_VERSION_HEADER, events -from daemon.originator import CLIENT_KIND_HEADER -from daemon.server import make_tcp_server +from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER, events +from cheetahclaws.daemon.originator import CLIENT_KIND_HEADER +from cheetahclaws.daemon.server import make_tcp_server -from kernel import register_with_daemon -from kernel.integration import detach +from cheetahclaws.kernel import register_with_daemon +from cheetahclaws.kernel.integration import detach def _free_port() -> int: @@ -145,7 +145,7 @@ def test_quota_exceeded_via_rpc(daemon): def test_phase4_does_not_break_earlier(daemon): h, p, t = daemon s, r = _rpc(h, p, t, "kernel.info", {}) - from kernel import SCHEMA_VERSION + from cheetahclaws.kernel import SCHEMA_VERSION assert r["result"]["schema_version"] == SCHEMA_VERSION # Existing kernel.* surfaces still work. pid = _rpc(h, p, t, "kernel.agent.create", diff --git a/tests/test_kernel_api_contract.py b/tests/test_kernel_api_contract.py index 29d09213..c297078b 100644 --- a/tests/test_kernel_api_contract.py +++ b/tests/test_kernel_api_contract.py @@ -17,10 +17,10 @@ import pytest -from daemon import events -from daemon.server import make_tcp_server +from cheetahclaws.daemon import events +from cheetahclaws.daemon.server import make_tcp_server -from kernel import ( +from cheetahclaws.kernel import ( ALL_KNOWN_METHODS, DEPRECATED_METHODS, EXPERIMENTAL_METHODS, @@ -31,7 +31,7 @@ register_with_daemon, verify_contract, ) -from kernel.integration import detach +from cheetahclaws.kernel.integration import detach def _free_port() -> int: diff --git a/tests/test_kernel_ast_tool.py b/tests/test_kernel_ast_tool.py index a5ab5d22..02b8528e 100644 --- a/tests/test_kernel_ast_tool.py +++ b/tests/test_kernel_ast_tool.py @@ -3,12 +3,12 @@ import pytest -from kernel.tools.ast_tool import ( +from cheetahclaws.kernel.tools.ast_tool import ( ALLOWED_KINDS, AST_TOOL, ast_handler, ) -from kernel.tools.registry import ( +from cheetahclaws.kernel.tools.registry import ( ToolContext, ToolFailed, ToolFsDenied, @@ -219,7 +219,7 @@ def test_path_too_large(tmp_path, monkeypatch): f = tmp_path / "big.py" f.write_text("x = 1\n") monkeypatch.setattr( - "kernel.tools.ast_tool.DEFAULT_AST_MAX_FILE_BYTES", 1, + "cheetahclaws.kernel.tools.ast_tool.DEFAULT_AST_MAX_FILE_BYTES", 1, ) ctx = ToolContext(pid=1, kernel=_AllowAll()) with pytest.raises(ToolFailed): @@ -230,7 +230,7 @@ def test_path_too_large(tmp_path, monkeypatch): def test_ast_tool_in_register_builtin_tools(): - from kernel.tools.builtin import register_builtin_tools + from cheetahclaws.kernel.tools.builtin import register_builtin_tools reg = ToolRegistry() names = register_builtin_tools(reg) assert "AST" in names diff --git a/tests/test_kernel_bridge_mirror.py b/tests/test_kernel_bridge_mirror.py index 9d31e34f..cfbfe390 100644 --- a/tests/test_kernel_bridge_mirror.py +++ b/tests/test_kernel_bridge_mirror.py @@ -6,7 +6,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( BridgeKind, BridgeMessage, BridgeMirror, @@ -15,7 +15,7 @@ inbound_topic, outbound_topic, ) -from kernel.bridge_mirror import MESSAGE_KIND +from cheetahclaws.kernel.bridge_mirror import MESSAGE_KIND @pytest.fixture diff --git a/tests/test_kernel_capability.py b/tests/test_kernel_capability.py index ec0b9400..4bc5a63c 100644 --- a/tests/test_kernel_capability.py +++ b/tests/test_kernel_capability.py @@ -3,7 +3,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentState, Capability, CapabilityDerivationError, @@ -119,7 +119,7 @@ def test_create_round_trip(stores): def test_create_unknown_pid(stores): _, cs = stores - from kernel import UnknownPid + from cheetahclaws.kernel import UnknownPid with pytest.raises(UnknownPid): cs.create(pid=9999, tool_grants=["Read"]) diff --git a/tests/test_kernel_chaos_smoke.py b/tests/test_kernel_chaos_smoke.py index b233944f..b2ad4e26 100644 --- a/tests/test_kernel_chaos_smoke.py +++ b/tests/test_kernel_chaos_smoke.py @@ -5,7 +5,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentState, AgentFSStore, CapabilityStore, @@ -58,7 +58,7 @@ def test_chaos_kill_random_agent_is_deterministic(stores): # For a stricter assertion: two ChaosMonkeys with the same seed # picking from the SAME population pick the same victim. # We'll use a fresh DB to make populations identical. - from kernel import KernelStore as _KS + from cheetahclaws.kernel import KernelStore as _KS import tempfile, pathlib with tempfile.TemporaryDirectory() as d: ks2 = _KS.open(pathlib.Path(d) / "k.db") diff --git a/tests/test_kernel_cli.py b/tests/test_kernel_cli.py index 57138c34..b33b8b26 100644 --- a/tests/test_kernel_cli.py +++ b/tests/test_kernel_cli.py @@ -13,13 +13,13 @@ import pytest -from daemon import discovery as _discovery -from daemon import events as _events -from daemon.server import make_tcp_server +from cheetahclaws.daemon import discovery as _discovery +from cheetahclaws.daemon import events as _events +from cheetahclaws.daemon.server import make_tcp_server -from kernel import register_with_daemon -from kernel.cli import dispatch as kernel_dispatch -from kernel.integration import detach +from cheetahclaws.kernel import register_with_daemon +from cheetahclaws.kernel.cli import dispatch as kernel_dispatch +from cheetahclaws.kernel.integration import detach def _free_port() -> int: @@ -227,7 +227,7 @@ def test_cli_queue_empty(running_kernel): def test_cli_queue_with_entries(running_kernel): ds = running_kernel["server"].daemon_state a = ds.kernel_store.create(name="x", template="t") - from kernel import ScheduleSpec + from cheetahclaws.kernel import ScheduleSpec sid = ds.scheduler_store.enqueue(ScheduleSpec(pid=a.pid, priority=5)) rc, out, err = _run_cli("queue") assert rc == 0 diff --git a/tests/test_kernel_daemon.py b/tests/test_kernel_daemon.py index 843182ab..402329e2 100644 --- a/tests/test_kernel_daemon.py +++ b/tests/test_kernel_daemon.py @@ -18,14 +18,14 @@ import pytest -from daemon import API_VERSION, API_VERSION_HEADER, events -from daemon.originator import CLIENT_ID_HEADER, CLIENT_KIND_HEADER -from daemon.server import make_tcp_server +from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER, events +from cheetahclaws.daemon.originator import CLIENT_ID_HEADER, CLIENT_KIND_HEADER +from cheetahclaws.daemon.server import make_tcp_server -from kernel import register_with_daemon -from kernel.integration import detach -from kernel.process import AgentState -from kernel.store import EV_PROCESS_CREATED, EV_PROCESS_RECOVERED +from cheetahclaws.kernel import register_with_daemon +from cheetahclaws.kernel.integration import detach +from cheetahclaws.kernel.process import AgentState +from cheetahclaws.kernel.store import EV_PROCESS_CREATED, EV_PROCESS_RECOVERED def _free_port() -> int: @@ -104,7 +104,7 @@ def test_kernel_info_reports_zero_state(daemon_with_kernel): info = resp["result"] # Track the live schema version rather than pinning a literal — # avoids a churn point on every additive schema bump. - from kernel import SCHEMA_VERSION + from cheetahclaws.kernel import SCHEMA_VERSION assert info["schema_version"] == SCHEMA_VERSION assert info["agent_count"] == 0 assert info["event_count"] == 0 @@ -238,7 +238,7 @@ def test_recovery_runs_at_register_time(tmp_path): db = tmp_path / "kernel.db" # Seed: one RUNNING agent left over from a prior daemon. - from kernel import KernelStore + from cheetahclaws.kernel import KernelStore seeded = KernelStore.open(db) try: a = seeded.create(name="x", template="t") diff --git a/tests/test_kernel_dialogue.py b/tests/test_kernel_dialogue.py index acd837e2..19fd2329 100644 --- a/tests/test_kernel_dialogue.py +++ b/tests/test_kernel_dialogue.py @@ -8,7 +8,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentState, DialogueOrchestrator, DialogueQuotaBreached, @@ -18,7 +18,7 @@ SandboxPolicy, UnknownPid, ) -from kernel.runner.llm import ( +from cheetahclaws.kernel.runner.llm import ( LlmRequest, LlmResponse, MockProvider, @@ -137,7 +137,7 @@ def _spawn_llm(kernel: Kernel, response: dict, *, sup = kernel.make_supervisor() sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={"model": "m", "user": user_msg}, env={**os.environ, "CC_LLM_PROVIDER": "mock", @@ -179,7 +179,7 @@ def test_existing_runner_text_defaults_empty(tmp_path): sup = k.make_supervisor() sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.runner_main"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"], policy=SandboxPolicy(wall_seconds=10), ) info = sup.wait(a.pid, timeout=15) diff --git a/tests/test_kernel_diff_tool.py b/tests/test_kernel_diff_tool.py index 2c120abd..09c8f81b 100644 --- a/tests/test_kernel_diff_tool.py +++ b/tests/test_kernel_diff_tool.py @@ -3,12 +3,12 @@ import pytest -from kernel.tools.diff_tool import ( +from cheetahclaws.kernel.tools.diff_tool import ( DEFAULT_DIFF_CAP_BYTES, DIFF_TOOL, diff_handler, ) -from kernel.tools.registry import ( +from cheetahclaws.kernel.tools.registry import ( ToolContext, ToolFsDenied, ToolInvalidArgs, @@ -177,7 +177,7 @@ def test_truncation(monkeypatch): """Force a tiny cap to verify truncation marker.""" ctx = ToolContext(pid=1, kernel=None) monkeypatch.setattr( - "kernel.tools.diff_tool.DEFAULT_DIFF_CAP_BYTES", + "cheetahclaws.kernel.tools.diff_tool.DEFAULT_DIFF_CAP_BYTES", 200, ) big_a = "\n".join(f"line{i}" for i in range(500)) + "\n" @@ -191,7 +191,7 @@ def test_truncation(monkeypatch): def test_diff_tool_registered_by_register_builtin_tools(): - from kernel.tools.builtin import register_builtin_tools + from cheetahclaws.kernel.tools.builtin import register_builtin_tools reg = ToolRegistry() names = register_builtin_tools(reg) assert "Diff" in names diff --git a/tests/test_kernel_exec_streaming.py b/tests/test_kernel_exec_streaming.py index 5f525d24..50cd8dd9 100644 --- a/tests/test_kernel_exec_streaming.py +++ b/tests/test_kernel_exec_streaming.py @@ -7,14 +7,14 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( Kernel, SandboxPolicy, ToolRegistry, register_builtin_tools, ) -from kernel.tools.exec_tool import exec_handler, register_exec_tool -from kernel.tools.registry import ( +from cheetahclaws.kernel.tools.exec_tool import exec_handler, register_exec_tool +from cheetahclaws.kernel.tools.registry import ( ToolContext, ToolInvalidArgs, dispatch_tool_call, @@ -50,7 +50,7 @@ def _spy(args, ctx): captured["on_chunk"] = ctx.on_chunk return {"ok": True} - from kernel.tools.registry import Tool, ToolRegistry + from cheetahclaws.kernel.tools.registry import Tool, ToolRegistry reg = ToolRegistry() reg.register(Tool( @@ -252,7 +252,7 @@ def test_exec_streaming_end_to_end(kernel): # via -c so we don't need a fixture file. driver = ( "import sys\n" - "from kernel.runner.ipc import JsonLineChannel\n" + "from cheetahclaws.kernel.runner.ipc import JsonLineChannel\n" "ch = JsonLineChannel(sys.stdin.buffer, sys.stdout.buffer)\n" "init = ch.recv(timeout=10)\n" "ch.send({'op':'ready','pid': init['pid']})\n" diff --git a/tests/test_kernel_exec_tool.py b/tests/test_kernel_exec_tool.py index 1cf81f89..fbb14556 100644 --- a/tests/test_kernel_exec_tool.py +++ b/tests/test_kernel_exec_tool.py @@ -17,7 +17,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( EXEC_TOOL, Kernel, SandboxPolicy, @@ -25,7 +25,7 @@ register_builtin_tools, register_exec_tool, ) -from kernel.tools.exec_tool import ( +from cheetahclaws.kernel.tools.exec_tool import ( DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_S, MAX_TIMEOUT_S, @@ -35,7 +35,7 @@ _validate_timeout, exec_handler, ) -from kernel.tools.registry import ( +from cheetahclaws.kernel.tools.registry import ( ToolContext, ToolFailed, ToolFsDenied, diff --git a/tests/test_kernel_facade.py b/tests/test_kernel_facade.py index a56aaa89..883474b4 100644 --- a/tests/test_kernel_facade.py +++ b/tests/test_kernel_facade.py @@ -8,7 +8,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentState, Kernel, KernelStore, @@ -140,7 +140,7 @@ def test_make_worker_after_make_supervisor(tmp_path): sup = k.make_supervisor() worker = k.make_worker( argv_factory=lambda e: [ - sys.executable, "-m", "kernel.runner.runner_main", + sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main", ], max_concurrent=1, ) @@ -156,7 +156,7 @@ def test_close_stops_worker(tmp_path): k = Kernel.open(tmp_path / "kernel.db") worker = k.make_worker( argv_factory=lambda e: [ - sys.executable, "-m", "kernel.runner.runner_main", + sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main", ], ) worker.start() @@ -172,9 +172,9 @@ def test_attach_to_daemon_registers_methods(tmp_path): daemon's RPC registry.""" import socket import uuid - from daemon import events as _events - from daemon.server import make_tcp_server - from kernel import verify_contract + from cheetahclaws.daemon import events as _events + from cheetahclaws.daemon.server import make_tcp_server + from cheetahclaws.kernel import verify_contract _events.reset_bus_for_tests() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/tests/test_kernel_fetch_streaming.py b/tests/test_kernel_fetch_streaming.py index 90178d42..4d39a262 100644 --- a/tests/test_kernel_fetch_streaming.py +++ b/tests/test_kernel_fetch_streaming.py @@ -8,8 +8,8 @@ import pytest -from kernel.tools.fetch_tool import fetch_handler -from kernel.tools.registry import ToolContext, ToolInvalidArgs +from cheetahclaws.kernel.tools.fetch_tool import fetch_handler +from cheetahclaws.kernel.tools.registry import ToolContext, ToolInvalidArgs # ── Local HTTP server fixture ─────────────────────────────────────── @@ -82,7 +82,7 @@ def check_fs(self, pid, path, mode): return True @pytest.fixture def kernel_for_local(monkeypatch): monkeypatch.setattr( - "kernel.tools.fetch_tool._is_private_ip", + "cheetahclaws.kernel.tools.fetch_tool._is_private_ip", lambda ip: False, ) return _AllowAllKernel() diff --git a/tests/test_kernel_fetch_tool.py b/tests/test_kernel_fetch_tool.py index 0e20d8af..abcdeed2 100644 --- a/tests/test_kernel_fetch_tool.py +++ b/tests/test_kernel_fetch_tool.py @@ -16,7 +16,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( FETCH_TOOL, Kernel, ToolNetDenied, @@ -24,7 +24,7 @@ register_builtin_tools, register_fetch_tool, ) -from kernel.tools.fetch_tool import ( +from cheetahclaws.kernel.tools.fetch_tool import ( DEFAULT_MAX_BYTES, DEFAULT_TIMEOUT_S, _is_private_ip, @@ -35,7 +35,7 @@ _validate_url, fetch_handler, ) -from kernel.tools.registry import ( +from cheetahclaws.kernel.tools.registry import ( ToolContext, ToolFailed, ToolInvalidArgs, @@ -190,7 +190,7 @@ def test_timeout_too_high(): def test_max_redirects_default(): - from kernel.tools.fetch_tool import DEFAULT_MAX_REDIRECTS + from cheetahclaws.kernel.tools.fetch_tool import DEFAULT_MAX_REDIRECTS assert _validate_max_redirects(None) == DEFAULT_MAX_REDIRECTS @@ -390,7 +390,7 @@ def kernel_for_local(monkeypatch): the private-IP check (return False so 127.0.0.1 is treated as public). Restored automatically.""" monkeypatch.setattr( - "kernel.tools.fetch_tool._is_private_ip", + "cheetahclaws.kernel.tools.fetch_tool._is_private_ip", lambda ip: False, ) return _OverrideDnsKernel() @@ -481,7 +481,7 @@ def test_e2e_redirect_to_private_blocked(http_server, monkeypatch): # Allow only the first hop's check; real check on the second. call_count = {"n": 0} real_is_private = None - from kernel.tools.fetch_tool import _is_private_ip as _real + from cheetahclaws.kernel.tools.fetch_tool import _is_private_ip as _real real_is_private = _real def faked(ip): @@ -490,7 +490,7 @@ def faked(ip): return False # let the first hop through return real_is_private(ip) monkeypatch.setattr( - "kernel.tools.fetch_tool._is_private_ip", faked, + "cheetahclaws.kernel.tools.fetch_tool._is_private_ip", faked, ) ctx = ToolContext(pid=1, kernel=_OverrideDnsKernel()) @@ -551,7 +551,7 @@ def do_GET(self): t = threading.Thread(target=server.serve_forever, daemon=True) t.start() monkeypatch.setattr( - "kernel.tools.fetch_tool._is_private_ip", lambda ip: False, + "cheetahclaws.kernel.tools.fetch_tool._is_private_ip", lambda ip: False, ) try: ctx = ToolContext(pid=1, kernel=_OverrideDnsKernel()) @@ -660,7 +660,7 @@ def test_dispatch_via_supervisor(tmp_path, http_server, monkeypatch): """The full kernel + dispatch path with a granted Fetch.""" port, base = http_server monkeypatch.setattr( - "kernel.tools.fetch_tool._is_private_ip", lambda ip: False, + "cheetahclaws.kernel.tools.fetch_tool._is_private_ip", lambda ip: False, ) k = Kernel.open(tmp_path / "kernel.db") try: diff --git a/tests/test_kernel_git_tool.py b/tests/test_kernel_git_tool.py index f37b40f1..949a6675 100644 --- a/tests/test_kernel_git_tool.py +++ b/tests/test_kernel_git_tool.py @@ -7,12 +7,12 @@ import pytest -from kernel.tools.git_tool import ( +from cheetahclaws.kernel.tools.git_tool import ( GIT_TOOL, git_handler, register_git_tool, ) -from kernel.tools.registry import ( +from cheetahclaws.kernel.tools.registry import ( ToolContext, ToolFsDenied, ToolInvalidArgs, @@ -261,7 +261,7 @@ def test_fs_denied_on_repo(repo): def test_git_not_in_register_builtin_tools(): """Git is opt-in — must not be auto-registered.""" - from kernel.tools.builtin import register_builtin_tools + from cheetahclaws.kernel.tools.builtin import register_builtin_tools reg = ToolRegistry() register_builtin_tools(reg) assert not reg.has("Git") diff --git a/tests/test_kernel_glob_list_tools.py b/tests/test_kernel_glob_list_tools.py index 0a930b89..201d00e9 100644 --- a/tests/test_kernel_glob_list_tools.py +++ b/tests/test_kernel_glob_list_tools.py @@ -6,12 +6,12 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( Kernel, ToolRegistry, register_builtin_tools, ) -from kernel.tools.registry import dispatch_tool_call +from cheetahclaws.kernel.tools.registry import dispatch_tool_call # ── Helpers ────────────────────────────────────────────────────────────── diff --git a/tests/test_kernel_ledger.py b/tests/test_kernel_ledger.py index 8f772962..bfc5ce49 100644 --- a/tests/test_kernel_ledger.py +++ b/tests/test_kernel_ledger.py @@ -5,7 +5,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( KernelStore, LedgerEntry, LedgerExists, diff --git a/tests/test_kernel_llm_runner.py b/tests/test_kernel_llm_runner.py index 7527660d..fe9e1ee6 100644 --- a/tests/test_kernel_llm_runner.py +++ b/tests/test_kernel_llm_runner.py @@ -9,7 +9,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( Kernel, LedgerStore, RunnerSupervisor, @@ -17,7 +17,7 @@ ScheduleSpec, WorkerLoop, ) -from kernel.runner.llm import ( +from cheetahclaws.kernel.runner.llm import ( LlmRequest, LlmResponse, MockProvider, @@ -32,7 +32,7 @@ ) -LLM_RUNNER_ARGV = [sys.executable, "-m", "kernel.runner.llm"] +LLM_RUNNER_ARGV = [sys.executable, "-m", "cheetahclaws.kernel.runner.llm"] # ── Dataclass round-trip ──────────────────────────────────────────────── @@ -418,21 +418,21 @@ def test_worker_loop_runs_llm_job(stack): def test_anthropic_provider_lazy_imports(): """Importing the module must NOT trigger the SDK import.""" - from kernel.runner.llm import anthropic_provider as ap_mod + from cheetahclaws.kernel.runner.llm import anthropic_provider as ap_mod # Class is importable without the SDK. assert hasattr(ap_mod, "AnthropicProvider") def test_anthropic_provider_construction_doesnt_import_sdk(): """Instantiation alone doesn't import anthropic; the call does.""" - from kernel.runner.llm.anthropic_provider import AnthropicProvider + from cheetahclaws.kernel.runner.llm.anthropic_provider import AnthropicProvider p = AnthropicProvider(api_key="dummy") # Client still None — not constructed yet. assert p._client is None def test_anthropic_provider_no_api_key_raises(): - from kernel.runner.llm.anthropic_provider import AnthropicProvider + from cheetahclaws.kernel.runner.llm.anthropic_provider import AnthropicProvider p = AnthropicProvider(api_key=None) # Save and clear ANTHROPIC_API_KEY so the unavailable check fires. saved = os.environ.pop("ANTHROPIC_API_KEY", None) diff --git a/tests/test_kernel_llm_streaming.py b/tests/test_kernel_llm_streaming.py index 27f4b83e..15757477 100644 --- a/tests/test_kernel_llm_streaming.py +++ b/tests/test_kernel_llm_streaming.py @@ -7,13 +7,13 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( Kernel, SandboxPolicy, ToolRegistry, register_builtin_tools, ) -from kernel.runner.llm import ( +from cheetahclaws.kernel.runner.llm import ( LlmRequest, LlmResponse, MockProvider, @@ -129,7 +129,7 @@ def test_subprocess_streams_each_char_to_chunks(kernel): received: list = [] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={"model": "m", "user": "hello", "stream": True}, @@ -156,7 +156,7 @@ def test_subprocess_stream_false_no_chunks(kernel): received: list = [] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={"model": "m", "user": "x"}, # stream omitted env=_scripted_env(responses), @@ -177,7 +177,7 @@ def test_subprocess_stream_with_long_text(kernel): received: list = [] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={"model": "m", "user": "x", "stream": True}, env=_scripted_env(responses), @@ -197,7 +197,7 @@ def test_subprocess_stream_with_unicode(kernel): received: list = [] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={"model": "m", "user": "x", "stream": True}, env=_scripted_env(responses), @@ -237,7 +237,7 @@ def test_subprocess_streaming_multi_iteration_tool_call(kernel): received: list = [] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={ "model": "m", "user": "go", @@ -277,7 +277,7 @@ def test_subprocess_stream_with_mock_no_stream_method(kernel): "CC_LLM_MOCK_RESPONSE_JSON": response_json} sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={"model": "m", "user": "x", "stream": True}, env=env, @@ -296,7 +296,7 @@ def test_subprocess_stream_with_mock_no_stream_method(kernel): def test_anthropic_provider_has_stream_method(): """The class should expose `stream` as an attribute even without anthropic SDK installed.""" - from kernel.runner.llm.anthropic_provider import AnthropicProvider + from cheetahclaws.kernel.runner.llm.anthropic_provider import AnthropicProvider p = AnthropicProvider(api_key="dummy") assert hasattr(p, "stream") assert callable(p.stream) diff --git a/tests/test_kernel_llm_tools.py b/tests/test_kernel_llm_tools.py index a1276ba9..f1fd4ded 100644 --- a/tests/test_kernel_llm_tools.py +++ b/tests/test_kernel_llm_tools.py @@ -8,14 +8,14 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( Kernel, LedgerStore, SandboxPolicy, ToolRegistry, register_builtin_tools, ) -from kernel.runner.llm import ( +from cheetahclaws.kernel.runner.llm import ( LlmRequest, LlmResponse, MockProvider, @@ -238,7 +238,7 @@ def test_runner_tool_use_then_final(tmp_path): ] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={ "model": "m", @@ -270,7 +270,7 @@ def test_runner_tool_use_no_tools_field_falls_back_to_text(tmp_path): sup = k.make_supervisor() sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={"model": "m", "user": "hi"}, env=_scripted_env([ @@ -305,7 +305,7 @@ def test_runner_max_iterations_cap(tmp_path): ] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=20), init_payload={ "model": "m", "user": "x", @@ -352,7 +352,7 @@ def test_runner_tool_denied_continues_loop(tmp_path): ] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={ "model": "m", "user": "do echo", @@ -394,7 +394,7 @@ def test_runner_charges_accumulate_across_iterations(tmp_path): ] sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.llm"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.llm"], policy=SandboxPolicy(wall_seconds=15), init_payload={"model": "m", "user": "go", "tools": [{"name": "Echo", "description": "x", @@ -416,7 +416,7 @@ def test_runner_charges_accumulate_across_iterations(tmp_path): def test_anthropic_provider_messages_with_tools_doesnt_import_sdk(): """We can construct LlmRequest with tools without anthropic SDK installed — the import is lazy on __call__.""" - from kernel.runner.llm.anthropic_provider import AnthropicProvider + from cheetahclaws.kernel.runner.llm.anthropic_provider import AnthropicProvider p = AnthropicProvider(api_key="dummy") # Just construct a request — provider call would fail without SDK, # but we don't call it. diff --git a/tests/test_kernel_mailbox.py b/tests/test_kernel_mailbox.py index 19c04acc..2b7a12fe 100644 --- a/tests/test_kernel_mailbox.py +++ b/tests/test_kernel_mailbox.py @@ -6,7 +6,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( KernelStore, Mailbox, MailboxAlreadyExists, diff --git a/tests/test_kernel_observability.py b/tests/test_kernel_observability.py index 0016cf05..48f367c0 100644 --- a/tests/test_kernel_observability.py +++ b/tests/test_kernel_observability.py @@ -6,7 +6,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentFSStore, AgentState, CapabilityStore, @@ -18,7 +18,7 @@ SchedulerStore, ScheduleSpec, ) -from kernel.errors import InvalidPayload, UnknownPid +from cheetahclaws.kernel.errors import InvalidPayload, UnknownPid @pytest.fixture @@ -258,7 +258,7 @@ def test_prometheus_text_passes_basic_regex_contract(stores): def test_prometheus_text_label_escaping(): """Make sure label-value escaping handles backslashes and quotes.""" - from kernel.observability import _esc_label + from cheetahclaws.kernel.observability import _esc_label assert _esc_label('plain') == 'plain' assert _esc_label('with"quote') == 'with\\"quote' assert _esc_label('with\\back') == 'with\\\\back' diff --git a/tests/test_kernel_phase2_daemon.py b/tests/test_kernel_phase2_daemon.py index eab39bda..3f99b3b5 100644 --- a/tests/test_kernel_phase2_daemon.py +++ b/tests/test_kernel_phase2_daemon.py @@ -13,12 +13,12 @@ import pytest -from daemon import API_VERSION, API_VERSION_HEADER, events -from daemon.originator import CLIENT_KIND_HEADER -from daemon.server import make_tcp_server +from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER, events +from cheetahclaws.daemon.originator import CLIENT_KIND_HEADER +from cheetahclaws.daemon.server import make_tcp_server -from kernel import register_with_daemon -from kernel.integration import detach +from cheetahclaws.kernel import register_with_daemon +from cheetahclaws.kernel.integration import detach def _free_port() -> int: @@ -214,7 +214,7 @@ def test_phase2_does_not_break_phase1(daemon): h, p, t = daemon s, r = _rpc(h, p, t, "kernel.info", {}) assert s == 200 - from kernel import SCHEMA_VERSION + from cheetahclaws.kernel import SCHEMA_VERSION assert r["result"]["schema_version"] == SCHEMA_VERSION pid = _rpc(h, p, t, "kernel.agent.create", {"name": "x", "template": "t"})[1]["result"]["pid"] diff --git a/tests/test_kernel_phase3_complete_daemon.py b/tests/test_kernel_phase3_complete_daemon.py index 9c68bfed..eda1375d 100644 --- a/tests/test_kernel_phase3_complete_daemon.py +++ b/tests/test_kernel_phase3_complete_daemon.py @@ -14,12 +14,12 @@ import pytest -from daemon import API_VERSION, API_VERSION_HEADER, events -from daemon.originator import CLIENT_KIND_HEADER -from daemon.server import make_tcp_server +from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER, events +from cheetahclaws.daemon.originator import CLIENT_KIND_HEADER +from cheetahclaws.daemon.server import make_tcp_server -from kernel import register_with_daemon -from kernel.integration import detach +from cheetahclaws.kernel import register_with_daemon +from cheetahclaws.kernel.integration import detach def _free_port() -> int: @@ -184,7 +184,7 @@ def test_registry_duplicate_name_via_rpc(daemon): def test_phase3_does_not_break_earlier_phases(daemon): h, p, t = daemon s, r = _rpc(h, p, t, "kernel.info", {}) - from kernel import SCHEMA_VERSION + from cheetahclaws.kernel import SCHEMA_VERSION assert r["result"]["schema_version"] == SCHEMA_VERSION pid = _rpc(h, p, t, "kernel.agent.create", {"name": "z", "template": "t"})[1]["result"]["pid"] diff --git a/tests/test_kernel_recovery.py b/tests/test_kernel_recovery.py index 0e7cbb6f..7ef3acbe 100644 --- a/tests/test_kernel_recovery.py +++ b/tests/test_kernel_recovery.py @@ -9,12 +9,12 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentState, KernelStore, ) -from kernel.errors import InvalidPayload -from kernel.store import ( +from cheetahclaws.kernel.errors import InvalidPayload +from cheetahclaws.kernel.store import ( EV_PROCESS_RECOVERED, RECOVERY_MARK_DEAD, RECOVERY_SUSPEND, diff --git a/tests/test_kernel_registry.py b/tests/test_kernel_registry.py index 094969da..e567178d 100644 --- a/tests/test_kernel_registry.py +++ b/tests/test_kernel_registry.py @@ -3,7 +3,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( KernelStore, RegistryEntry, RegistryInvalidName, diff --git a/tests/test_kernel_runner.py b/tests/test_kernel_runner.py index 0975ae07..f04ee9b6 100644 --- a/tests/test_kernel_runner.py +++ b/tests/test_kernel_runner.py @@ -15,7 +15,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentState, JsonLineChannel, KernelStore, @@ -36,7 +36,7 @@ ) -RUNNER_ARGV = [sys.executable, "-m", "kernel.runner.runner_main"] +RUNNER_ARGV = [sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"] @pytest.fixture diff --git a/tests/test_kernel_sandbox.py b/tests/test_kernel_sandbox.py index 46a89ff3..0d7216b5 100644 --- a/tests/test_kernel_sandbox.py +++ b/tests/test_kernel_sandbox.py @@ -17,7 +17,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( SANDBOX_DEFAULT, SANDBOX_OFF, SANDBOX_STRICT, diff --git a/tests/test_kernel_scheduler.py b/tests/test_kernel_scheduler.py index 53605dc0..3e38ff8d 100644 --- a/tests/test_kernel_scheduler.py +++ b/tests/test_kernel_scheduler.py @@ -6,7 +6,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentState, KernelStore, LedgerStore, diff --git a/tests/test_kernel_scheduler_daemon.py b/tests/test_kernel_scheduler_daemon.py index fe70086d..551f3c9e 100644 --- a/tests/test_kernel_scheduler_daemon.py +++ b/tests/test_kernel_scheduler_daemon.py @@ -10,12 +10,12 @@ import pytest -from daemon import API_VERSION, API_VERSION_HEADER, events -from daemon.originator import CLIENT_KIND_HEADER -from daemon.server import make_tcp_server +from cheetahclaws.daemon import API_VERSION, API_VERSION_HEADER, events +from cheetahclaws.daemon.originator import CLIENT_KIND_HEADER +from cheetahclaws.daemon.server import make_tcp_server -from kernel import register_with_daemon -from kernel.integration import detach +from cheetahclaws.kernel import register_with_daemon +from cheetahclaws.kernel.integration import detach def _free_port() -> int: diff --git a/tests/test_kernel_state_machine.py b/tests/test_kernel_state_machine.py index 033fb0e7..6fa7492e 100644 --- a/tests/test_kernel_state_machine.py +++ b/tests/test_kernel_state_machine.py @@ -6,7 +6,7 @@ """ from __future__ import annotations -from kernel.process import ( +from cheetahclaws.kernel.process import ( ALLOWED_TRANSITIONS, AgentProcess, AgentState, diff --git a/tests/test_kernel_store.py b/tests/test_kernel_store.py index 4dd319a8..4903ac84 100644 --- a/tests/test_kernel_store.py +++ b/tests/test_kernel_store.py @@ -10,24 +10,24 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentProcess, AgentState, IllegalTransition, KernelStore, UnknownPid, ) -from kernel.errors import ( +from cheetahclaws.kernel.errors import ( KERNEL_ILLEGAL_TRANSITION, KERNEL_UNKNOWN_PID, InvalidPayload, ) -from kernel.schema import ( +from cheetahclaws.kernel.schema import ( EXPECTED_SCHEMA_VERSION, get_schema_version, open_connection, ) -from kernel.store import ( +from cheetahclaws.kernel.store import ( EV_PROCESS_CREATED, EV_PROCESS_RECOVERED, EV_PROCESS_TERMINATED, diff --git a/tests/test_kernel_streaming.py b/tests/test_kernel_streaming.py index 0730ce71..015db465 100644 --- a/tests/test_kernel_streaming.py +++ b/tests/test_kernel_streaming.py @@ -6,7 +6,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( Kernel, RunnerExitInfo, SandboxPolicy, @@ -19,7 +19,7 @@ ) -RUNNER_ARGV = [sys.executable, "-m", "kernel.runner.runner_main"] +RUNNER_ARGV = [sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"] @pytest.fixture diff --git a/tests/test_kernel_tools.py b/tests/test_kernel_tools.py index 76ce0749..64bde594 100644 --- a/tests/test_kernel_tools.py +++ b/tests/test_kernel_tools.py @@ -8,7 +8,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( Kernel, SandboxPolicy, Tool, @@ -408,7 +408,7 @@ def test_supervisor_dispatches_echo_via_runner(tmp_path): }) sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.runner_main"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"], policy=SandboxPolicy(wall_seconds=10), env={**os.environ, "CC_RUNNER_BEHAVIOR": f"tool_call={call_body}"}, ) @@ -438,7 +438,7 @@ def test_supervisor_dispatches_read_with_fs_grant(tmp_path): "args": {"path": str(target)}}) sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.runner_main"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"], policy=SandboxPolicy(wall_seconds=10), env={**os.environ, "CC_RUNNER_BEHAVIOR": f"tool_call={body}"}, ) @@ -461,7 +461,7 @@ def test_supervisor_capability_denied_via_runner(tmp_path): "args": {"text": "x"}}) sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.runner_main"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"], policy=SandboxPolicy(wall_seconds=10), env={**os.environ, "CC_RUNNER_BEHAVIOR": f"tool_call={body}"}, ) @@ -481,7 +481,7 @@ def test_supervisor_without_registry_returns_tool_not_found(tmp_path): "args": {"text": "x"}}) sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.runner_main"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"], policy=SandboxPolicy(wall_seconds=10), env={**os.environ, "CC_RUNNER_BEHAVIOR": f"tool_call={body}"}, ) @@ -506,7 +506,7 @@ def test_supervisor_records_tool_dispatched_event(tmp_path): "args": {"text": "hi"}}) sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.runner_main"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"], policy=SandboxPolicy(wall_seconds=10), env={**os.environ, "CC_RUNNER_BEHAVIOR": f"tool_call={body}"}, ) @@ -532,7 +532,7 @@ def test_supervisor_records_tool_denied_event(tmp_path): "args": {"text": "hi"}}) sup.spawn( pid=a.pid, - argv=[sys.executable, "-m", "kernel.runner.runner_main"], + argv=[sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"], policy=SandboxPolicy(wall_seconds=10), env={**os.environ, "CC_RUNNER_BEHAVIOR": f"tool_call={body}"}, ) diff --git a/tests/test_kernel_worker.py b/tests/test_kernel_worker.py index 9c9b4c8c..16d5ea0b 100644 --- a/tests/test_kernel_worker.py +++ b/tests/test_kernel_worker.py @@ -8,7 +8,7 @@ import pytest -from kernel import ( +from cheetahclaws.kernel import ( AgentState, KernelStore, LedgerStore, @@ -26,7 +26,7 @@ ) -RUNNER_ARGV = [sys.executable, "-m", "kernel.runner.runner_main"] +RUNNER_ARGV = [sys.executable, "-m", "cheetahclaws.kernel.runner.runner_main"] @pytest.fixture diff --git a/tests/test_lab_phase_a.py b/tests/test_lab_phase_a.py index aef21993..13b1b84c 100644 --- a/tests/test_lab_phase_a.py +++ b/tests/test_lab_phase_a.py @@ -12,8 +12,8 @@ import pytest -from research.lab import iterate, resume, backlog -from research.lab.iterate import ( +from cheetahclaws.research.lab import iterate, resume, backlog +from cheetahclaws.research.lab.iterate import ( DIMENSIONS, DIMENSION_TO_STAGE, IterationConfig, @@ -22,8 +22,8 @@ parse_review_scores, weakest_dimension, ) -from research.lab.orchestrator import LLMResponse, Stage -from research.lab.storage import LabStorage +from cheetahclaws.research.lab.orchestrator import LLMResponse, Stage +from cheetahclaws.research.lab.storage import LabStorage # ── Fake LLM ────────────────────────────────────────────────────────────── @@ -191,8 +191,8 @@ def test_score_report_aggregates_across_reviewers(storage): fake.set_response("reviewer_3", "novelty: 8\nrigor: 7\nclarity: 8\nevidence: 7") - from research.lab.orchestrator import LabRun, LabState - from research.lab.roles import build_default_assignment + from cheetahclaws.research.lab.orchestrator import LabRun, LabState + from cheetahclaws.research.lab.roles import build_default_assignment roles = build_default_assignment({}) state = LabState(run_id=rec.run_id, topic=rec.topic, stage=Stage.FINALIZATION) run = LabRun(state=state, storage=storage, roles=roles, config={}, diff --git a/tests/test_litellm_provider.py b/tests/test_litellm_provider.py index c1549053..a3093dcd 100644 --- a/tests/test_litellm_provider.py +++ b/tests/test_litellm_provider.py @@ -5,13 +5,13 @@ import pytest -from kernel.runner.llm.provider import ( +from cheetahclaws.kernel.runner.llm.provider import ( LlmRequest, LlmResponse, ProviderInvalidRequest, ProviderUnavailable, ) -from kernel.runner.llm.litellm_provider import LiteLLMProvider +from cheetahclaws.kernel.runner.llm.litellm_provider import LiteLLMProvider def _make_provider_with_fake_litellm( @@ -109,7 +109,7 @@ def test_module_imports_without_litellm(self, monkeypatch): # Force re-import: pop the cached module from sys.modules. import importlib - import kernel.runner.llm.litellm_provider as mod + import cheetahclaws.kernel.runner.llm.litellm_provider as mod importlib.reload(mod) # Construction must succeed too. @@ -487,7 +487,7 @@ def test_litellm_registered_in_runner(self): """CC_LLM_PROVIDER=litellm must be routable through the subprocess runner, otherwise the provider isn't actually usable end-to-end.""" - from kernel.runner.llm import __main__ as runner_main + from cheetahclaws.kernel.runner.llm import __main__ as runner_main import inspect @@ -499,7 +499,7 @@ def test_litellm_in_top_level_providers_registry(self): """The CLI / Web UI consult providers.PROVIDERS when resolving --model X. Without a litellm entry there, no end-to-end caller can reach the new adapter — only direct Python use works.""" - import providers + from cheetahclaws import providers assert "litellm" in providers.PROVIDERS entry = providers.PROVIDERS["litellm"] @@ -511,7 +511,7 @@ def test_model_string_routes_to_litellm(self): """`litellm/openai/gpt-4o` must resolve as provider=litellm with bare_model=openai/gpt-4o (the form litellm.completion wants), so the slash-prefix routing actually works.""" - import providers + from cheetahclaws import providers assert providers.detect_provider("litellm/openai/gpt-4o") == "litellm" assert providers.bare_model("litellm/openai/gpt-4o") == "openai/gpt-4o" diff --git a/tests/test_logging_utils.py b/tests/test_logging_utils.py index 4c90bfbc..6bced1b5 100644 --- a/tests/test_logging_utils.py +++ b/tests/test_logging_utils.py @@ -10,7 +10,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import logging_utils +from cheetahclaws import logging_utils def _reset(): diff --git a/tests/test_mcp.py b/tests/test_mcp.py index e28dd151..3341ba16 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -10,13 +10,13 @@ import pytest -from mcp_client.types import ( +from cheetahclaws.mcp_client.types import ( MCPServerConfig, MCPTool, MCPServerState, MCPTransport, make_request, make_notification, INIT_PARAMS, ) -from mcp_client.config import load_mcp_configs, add_server_to_user_config, remove_server_from_user_config -from mcp_client.client import MCPManager, MCPClient, StdioTransport, get_mcp_manager -import mcp_client.config as _mcp_config +from cheetahclaws.mcp_client.config import load_mcp_configs, add_server_to_user_config, remove_server_from_user_config +from cheetahclaws.mcp_client.client import MCPManager, MCPClient, StdioTransport, get_mcp_manager +import cheetahclaws.mcp_client.config as _mcp_config # ── Fixtures ────────────────────────────────────────────────────────────────── @@ -24,7 +24,7 @@ @pytest.fixture(autouse=True) def reset_manager(monkeypatch): """Each test gets a fresh MCPManager singleton.""" - import mcp_client.client as _client_mod + import cheetahclaws.mcp_client.client as _client_mod monkeypatch.setattr(_client_mod, "_manager", None) diff --git a/tests/test_memory.py b/tests/test_memory.py index 530e9ce4..1c98708d 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -2,8 +2,8 @@ import pytest from pathlib import Path -import memory.store as _store -from memory.store import ( +import cheetahclaws.memory.store as _store +from cheetahclaws.memory.store import ( MemoryEntry, save_memory, load_index, @@ -14,8 +14,8 @@ parse_frontmatter, get_index_content, ) -from memory.context import get_memory_context, truncate_index_content -from memory.scan import ( +from cheetahclaws.memory.context import get_memory_context, truncate_index_content +from cheetahclaws.memory.scan import ( scan_memory_dir, format_memory_manifest, memory_age_days, @@ -23,7 +23,7 @@ memory_freshness_text, MemoryHeader, ) -from memory.types import MEMORY_TYPES +from cheetahclaws.memory.types import MEMORY_TYPES # ── Fixtures ───────────────────────────────────────────────────────────── diff --git a/tests/test_monitor_scheduler_events.py b/tests/test_monitor_scheduler_events.py index 83330fce..7fa9a077 100644 --- a/tests/test_monitor_scheduler_events.py +++ b/tests/test_monitor_scheduler_events.py @@ -13,9 +13,9 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import monitor.store as store -import monitor.scheduler as scheduler -from daemon import events, schema +import cheetahclaws.monitor.store as store +import cheetahclaws.monitor.scheduler as scheduler +from cheetahclaws.daemon import events, schema @pytest.fixture(autouse=True) @@ -59,7 +59,7 @@ def test_run_one_persists_report_to_monitor_reports(): def test_run_one_records_failed_delivery_in_report(): store.add_subscription("arxiv", schedule="daily", channels=["telegram"]) # Force telegram delivery to fail - import monitor.scheduler as sched + import cheetahclaws.monitor.scheduler as sched def _failing_deliver(_report, channels, _config): return {ch: "no token" for ch in channels} sched.deliver = _failing_deliver @@ -116,7 +116,7 @@ def test_event_report_id_matches_persisted_row(): def test_event_carries_error_list_on_partial_failure(): store.add_subscription("arxiv", schedule="daily", channels=["telegram", "console"]) - import monitor.scheduler as sched + import cheetahclaws.monitor.scheduler as sched sched.deliver = lambda _r, channels, _c: { "telegram": "no token", "console": ""} bus = events.get_bus() diff --git a/tests/test_monitor_store_sqlite.py b/tests/test_monitor_store_sqlite.py index a3e986ca..f3291b76 100644 --- a/tests/test_monitor_store_sqlite.py +++ b/tests/test_monitor_store_sqlite.py @@ -10,8 +10,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import monitor.store as store -from daemon import schema +import cheetahclaws.monitor.store as store +from cheetahclaws.daemon import schema @pytest.fixture(autouse=True) diff --git a/tests/test_native_tool_intercept.py b/tests/test_native_tool_intercept.py index 4e35a166..f7673997 100644 --- a/tests/test_native_tool_intercept.py +++ b/tests/test_native_tool_intercept.py @@ -8,7 +8,7 @@ import pytest -from providers import ( +from cheetahclaws.providers import ( _find_native_tool_marker, _extract_native_tool_calls, TextChunk, AssistantTurn, @@ -170,7 +170,7 @@ def _set_chunks(self, chunks): def test_stream_buffers_gemma_output_and_emits_tool_call(monkeypatch): """End-to-end: streaming Gemma's native format → no garbage to user, proper tool_calls in AssistantTurn.""" - from providers import stream_openai_compat + from cheetahclaws.providers import stream_openai_compat chunks = [ _FakeChunk(_FakeDelta(content="Sure, let me check. ")), @@ -218,7 +218,7 @@ def test_stream_buffers_gemma_output_and_emits_tool_call(monkeypatch): def test_stream_falls_back_to_text_when_native_call_unparsable(monkeypatch): """If buffering started but the format is unrecognisable, emit the raw buffer as text so the user sees something.""" - from providers import stream_openai_compat + from cheetahclaws.providers import stream_openai_compat chunks = [ _FakeChunk(_FakeDelta(content="Looking up. ")), diff --git a/tests/test_nim_provider.py b/tests/test_nim_provider.py index c658a0f5..3665a264 100644 --- a/tests/test_nim_provider.py +++ b/tests/test_nim_provider.py @@ -14,9 +14,9 @@ import pytest -import agent -from agent import AgentState, run -from providers import ( +from cheetahclaws import agent +from cheetahclaws.agent import AgentState, run +from cheetahclaws.providers import ( PROVIDERS, COSTS, AssistantTurn, TextChunk, bare_model, detect_provider, nim_next_model, ) diff --git a/tests/test_options_menu.py b/tests/test_options_menu.py index 79fde2e4..78d95ecc 100644 --- a/tests/test_options_menu.py +++ b/tests/test_options_menu.py @@ -20,8 +20,8 @@ import pytest -import runtime -from tools import interaction as itx +from cheetahclaws import runtime +from cheetahclaws.tools import interaction as itx # ── Helpers ─────────────────────────────────────────────────────────────── diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 7b9a34e6..46e41b9a 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -145,31 +145,33 @@ def test_every_top_level_package_dir_reachable_by_find(): # These imports must always succeed in a healthy install. If any of them # raise ImportError, the user's `pip install .` produced a broken wheel. +# Everything now lives under the single ``cheetahclaws`` package — that is the +# whole point of the layout (no generic top-level name to be shadowed). _REQUIRED_IMPORTS = [ - "prompts", - "prompts.select", - "memory", - "memory.context", - "ui", - "web", - "bridges", - "commands", - "research", - "research.lab", - "modular", - "modular.trading", - "modular.trading.data", - "modular.trading.engines", - "modular.trading.agents", - "modular.trading.alt_data", - "modular.trading.broker", - "modular.trading.discover", - "modular.trading.ml", - "modular.video", - "modular.voice", - "context", # top-level py-module - "providers", "cheetahclaws", + "cheetahclaws.prompts", + "cheetahclaws.prompts.select", + "cheetahclaws.memory", + "cheetahclaws.memory.context", + "cheetahclaws.ui", + "cheetahclaws.web", + "cheetahclaws.bridges", + "cheetahclaws.commands", + "cheetahclaws.research", + "cheetahclaws.research.lab", + "cheetahclaws.modular", + "cheetahclaws.modular.trading", + "cheetahclaws.modular.trading.data", + "cheetahclaws.modular.trading.engines", + "cheetahclaws.modular.trading.agents", + "cheetahclaws.modular.trading.alt_data", + "cheetahclaws.modular.trading.broker", + "cheetahclaws.modular.trading.discover", + "cheetahclaws.modular.trading.ml", + "cheetahclaws.modular.video", + "cheetahclaws.modular.voice", + "cheetahclaws.context", + "cheetahclaws.providers", ] @@ -190,4 +192,4 @@ def test_required_module_imports(modname): def test_prompts_exports_pick_base_prompt(): """The exact symbol context.py needs (failing line in issue #97).""" - from prompts import pick_base_prompt, load_fragment # noqa: F401 + from cheetahclaws.prompts import pick_base_prompt, load_fragment # noqa: F401 diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 214631a6..e8ef1a12 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -9,15 +9,15 @@ import pytest -from plugin.types import ( +from cheetahclaws.plugin.types import ( PluginManifest, PluginEntry, PluginScope, parse_plugin_identifier, sanitize_plugin_name, ) -from plugin.recommend import ( +from cheetahclaws.plugin.recommend import ( recommend_plugins, recommend_from_files, format_recommendations, PluginRecommendation, ) -import plugin.store as _store +import cheetahclaws.plugin.store as _store # ── Fixtures ────────────────────────────────────────────────────────────────── @@ -286,7 +286,7 @@ def test_format_empty(self): class TestAskUserQuestion: def test_freetext_answer(self): """User typing free text returns that text verbatim.""" - import tools as _tools + from cheetahclaws import tools as _tools with patch("builtins.input", return_value="yes"): result = _tools._ask_user_question("Continue?", allow_freetext=True) @@ -294,7 +294,7 @@ def test_freetext_answer(self): def test_option_selection_by_number(self): """Selecting option 1 from a numbered list returns its label.""" - import tools as _tools + from cheetahclaws import tools as _tools with patch("builtins.input", return_value="1"): result = _tools._ask_user_question( @@ -306,7 +306,7 @@ def test_option_selection_by_number(self): def test_option_freetext_via_zero(self): """Typing 0 with allow_freetext prompts for a custom answer.""" - import tools as _tools + from cheetahclaws import tools as _tools with patch("builtins.input", side_effect=["0", "custom reply"]): result = _tools._ask_user_question( @@ -318,6 +318,6 @@ def test_option_freetext_via_zero(self): def test_tool_schema_registered(self): """AskUserQuestion must appear in TOOL_SCHEMAS.""" - from tools import TOOL_SCHEMAS + from cheetahclaws.tools import TOOL_SCHEMAS names = [s["name"] for s in TOOL_SCHEMAS] assert "AskUserQuestion" in names diff --git a/tests/test_plugin_external.py b/tests/test_plugin_external.py index a2f338fd..27445db2 100644 --- a/tests/test_plugin_external.py +++ b/tests/test_plugin_external.py @@ -6,8 +6,8 @@ import pytest -from plugin import store -from plugin.store import ( +from cheetahclaws.plugin import store +from cheetahclaws.plugin.store import ( PLUGIN_PATH_ENV, _dep_distribution_name, _external_plugin_dirs, @@ -18,7 +18,7 @@ get_plugin, list_plugins, ) -from plugin.types import PluginScope +from cheetahclaws.plugin.types import PluginScope @pytest.fixture(autouse=True) diff --git a/tests/test_prompt_assembly.py b/tests/test_prompt_assembly.py index 41111f62..ccc58335 100644 --- a/tests/test_prompt_assembly.py +++ b/tests/test_prompt_assembly.py @@ -14,7 +14,7 @@ import pytest -import context as _context +from cheetahclaws import context as _context @pytest.fixture(autouse=True) @@ -41,7 +41,7 @@ def test_assembled_prompt_contains_identity_and_env(): def test_plan_mode_appends_fragment_with_plan_file_filled(monkeypatch, tmp_path): plan_path = str(tmp_path / "plan.md") # Seed the runtime context so _render_plan_fragment can find the path. - import runtime + from cheetahclaws import runtime sctx = runtime.get_session_ctx("test-session") sctx.plan_file = plan_path try: @@ -94,7 +94,7 @@ def test_assembly_order_is_base_then_env_then_memory_then_plan(monkeypatch): monkeypatch.setattr(_context, "get_memory_context", lambda: "- a memory") monkeypatch.setattr(_context, "_tmux_available", lambda: True) - import runtime + from cheetahclaws import runtime runtime.get_session_ctx("test-session").plan_file = "/tmp/plan.md" try: prompt = _context.build_system_prompt(_base_config(permission_mode="plan")) @@ -117,7 +117,7 @@ def test_missing_config_falls_back_to_default(): anthropic.md. Falling back to a Claude-styled prompt would silently apply XML-tag structuring etc. to whatever model picked it up later. """ - from prompts import select as _select + from cheetahclaws.prompts import select as _select prompt = _context.build_system_prompt(None) assert "CheetahClaws" in prompt assert "# Environment" in prompt diff --git a/tests/test_prompt_selection.py b/tests/test_prompt_selection.py index c1c8adde..eb5245c2 100644 --- a/tests/test_prompt_selection.py +++ b/tests/test_prompt_selection.py @@ -16,8 +16,8 @@ import pytest -from prompts import pick_base_prompt, load_fragment -from prompts import select as _select +from cheetahclaws.prompts import pick_base_prompt, load_fragment +from cheetahclaws.prompts import select as _select def _default_text() -> str: diff --git a/tests/test_prompt_size.py b/tests/test_prompt_size.py index c2096f90..a992f5b9 100644 --- a/tests/test_prompt_size.py +++ b/tests/test_prompt_size.py @@ -24,8 +24,8 @@ import pytest -_BASE_DIR = Path(__file__).parent.parent / "prompts" / "base" -_OVERLAYS_DIR = Path(__file__).parent.parent / "prompts" / "overlays" +_BASE_DIR = Path(__file__).parent.parent / "cheetahclaws" / "prompts" / "base" +_OVERLAYS_DIR = Path(__file__).parent.parent / "cheetahclaws" / "prompts" / "overlays" # Keep these in sync with prompts/README.md. Bump deliberately, not by accident. MAX_BASE_PROMPT_LINES = 150 diff --git a/tests/test_qq_bridge.py b/tests/test_qq_bridge.py index f4e19050..4cb04bca 100644 --- a/tests/test_qq_bridge.py +++ b/tests/test_qq_bridge.py @@ -28,7 +28,7 @@ def test_config_defaults(monkeypatch, tmp_path): """QQ config keys exist in DEFAULTS.""" monkeypatch.setenv("HOME", str(tmp_path)) import importlib - import config + from cheetahclaws import config importlib.reload(config) cfg = config.load_config() assert "qq_appid" in cfg @@ -39,7 +39,7 @@ def test_config_defaults(monkeypatch, tmp_path): def test_runtime_context_fields(): """RuntimeContext has QQ fields with correct defaults.""" - from runtime import RuntimeContext + from cheetahclaws.runtime import RuntimeContext ctx = RuntimeContext() assert ctx.qq_send is None assert ctx.qq_input_event is None @@ -52,13 +52,13 @@ def test_runtime_context_fields(): def test_is_in_qq_turn_default(): """Turn detection returns False when no QQ turn is active.""" - from tools.interaction import _is_in_qq_turn + from cheetahclaws.tools.interaction import _is_in_qq_turn assert _is_in_qq_turn({}) is False def test_is_in_qq_turn_thread_local(): """Turn detection returns True when thread-local flag is set.""" - from tools.interaction import _qq_thread_local, _is_in_qq_turn + from cheetahclaws.tools.interaction import _qq_thread_local, _is_in_qq_turn _qq_thread_local.active = True try: assert _is_in_qq_turn({}) is True @@ -68,8 +68,8 @@ def test_is_in_qq_turn_thread_local(): def test_is_in_qq_turn_runtime_ctx(): """Turn detection returns True when RuntimeContext.in_qq_turn is True.""" - from tools.interaction import _is_in_qq_turn - import runtime + from cheetahclaws.tools.interaction import _is_in_qq_turn + from cheetahclaws import runtime ctx = runtime.get_session_ctx("_test_qq_turn") ctx.in_qq_turn = True try: @@ -81,7 +81,7 @@ def test_is_in_qq_turn_runtime_ctx(): def test_qq_cmd_missing_config(): """cmd_qq shows error when no args and no saved config.""" - from bridges.qq import cmd_qq + from cheetahclaws.bridges.qq import cmd_qq result = cmd_qq("", None, {"qq_appid": "", "qq_secret": ""}) assert result is True @@ -89,11 +89,11 @@ def test_qq_cmd_missing_config(): def test_qq_cmd_inline_config(tmp_path, monkeypatch): """cmd_qq saves appid/secret when provided inline.""" from unittest.mock import patch - from bridges.qq import cmd_qq + from cheetahclaws.bridges.qq import cmd_qq monkeypatch.setenv("HOME", str(tmp_path)) cfg = {"qq_appid": "", "qq_secret": ""} # Mock bridge start to avoid spawning a real daemon thread - with patch("bridges.qq._qq_start_bridge"): + with patch("cheetahclaws.bridges.qq._qq_start_bridge"): result = cmd_qq("myappid mysecret", None, cfg) assert result is True assert cfg["qq_appid"] == "myappid" @@ -102,21 +102,21 @@ def test_qq_cmd_inline_config(tmp_path, monkeypatch): def test_qq_cmd_status_not_running(): """cmd_qq status reports not configured when empty.""" - from bridges.qq import cmd_qq + from cheetahclaws.bridges.qq import cmd_qq result = cmd_qq("status", None, {"qq_appid": "", "qq_secret": ""}) assert result is True def test_qq_cmd_status_configured(): """cmd_qq status reports configured but not running.""" - from bridges.qq import cmd_qq + from cheetahclaws.bridges.qq import cmd_qq result = cmd_qq("status", None, {"qq_appid": "test123", "qq_secret": "sec"}) assert result is True def test_message_dedup_set_capped(): """_qq_seen_msgids stays under 2000 entries.""" - from bridges import qq + from cheetahclaws.bridges import qq for i in range(2100): qq._qq_seen_msgids.add(f"msg_{i}") assert len(qq._qq_seen_msgids) <= 2100 @@ -125,7 +125,7 @@ def test_message_dedup_set_capped(): def test_reply_ctx_tracking(): """Passive reply context stores msg_id, event_id, seq, timestamp, and msg_type.""" - from bridges.qq import _qq_reply_ctx, _qq_reply_lock + from cheetahclaws.bridges.qq import _qq_reply_ctx, _qq_reply_lock with _qq_reply_lock: _qq_reply_ctx["test_target"] = ("msg123", "event456", 1, time.time(), "group") assert "test_target" in _qq_reply_ctx @@ -148,14 +148,14 @@ def test_reply_ctx_tracking(): def test_qq_send_no_api(): """_qq_send is a no-op when no API is configured.""" - from bridges.qq import _qq_send + from cheetahclaws.bridges.qq import _qq_send _qq_send("some_target", "hello", {"qq_appid": "x"}) def test_qq_pending_input_only_accepts_prompt_target(): """A QQ permission reply from another target must not release the prompt.""" - from runtime import RuntimeContext - from bridges.qq import _qq_try_deliver_input + from cheetahclaws.runtime import RuntimeContext + from cheetahclaws.bridges.qq import _qq_try_deliver_input ctx = RuntimeContext() evt = threading.Event() @@ -173,7 +173,7 @@ def test_qq_pending_input_only_accepts_prompt_target(): def test_qq_send_with_chunks(): """_qq_send splits long text into chunks.""" - from bridges.qq import _qq_send, _QQ_MAX_MSG_LEN + from cheetahclaws.bridges.qq import _qq_send, _QQ_MAX_MSG_LEN long_text = "A" * (_QQ_MAX_MSG_LEN * 2 + 100) # Should not raise even without API _qq_send("target", long_text, {}) @@ -181,7 +181,7 @@ def test_qq_send_with_chunks(): def test_passive_window_constants(): """Passive reply window follows botpy's documented 5-minute validity.""" - from bridges.qq import _QQ_PASSIVE_WINDOW, _QQ_STREAM_INTERVAL, _QQ_MAX_MSG_LEN, _QQ_STREAM_MIN_LEN + from cheetahclaws.bridges.qq import _QQ_PASSIVE_WINDOW, _QQ_STREAM_INTERVAL, _QQ_MAX_MSG_LEN, _QQ_STREAM_MIN_LEN assert _QQ_PASSIVE_WINDOW == 300 assert _QQ_STREAM_INTERVAL == 2.0 assert _QQ_MAX_MSG_LEN == 2000 @@ -192,12 +192,12 @@ def test_send_future_exception_is_logged(): """Errors raised by scheduled QQ HTTP sends should be surfaced.""" from concurrent.futures import Future from unittest.mock import patch - from bridges.qq import _qq_log_send_future + from cheetahclaws.bridges.qq import _qq_log_send_future fut = Future() fut.set_exception(RuntimeError("api failed")) - with patch("bridges.qq._log.warn") as warn: + with patch("cheetahclaws.bridges.qq._log.warn") as warn: _qq_log_send_future(fut, "group", "target") warn.assert_called_once() @@ -207,7 +207,7 @@ def test_send_future_exception_is_logged(): def test_queue_or_dispatch_marks_busy_before_thread_dispatch(): """A second same-target job should queue before worker thread starts.""" from unittest.mock import MagicMock, patch - from bridges import qq + from cheetahclaws.bridges import qq job1 = MagicMock() job1.id = "job1" @@ -223,7 +223,7 @@ def test_queue_or_dispatch_marks_busy_before_thread_dispatch(): def fake_dispatch(job, prompt, target_id, msg_type, run_query_cb, session_ctx, config, image_b64=None): dispatched.append((job.id, prompt, target_id, image_b64)) - with patch("bridges.qq._dispatch_qq_job", side_effect=fake_dispatch): + with patch("cheetahclaws.bridges.qq._dispatch_qq_job", side_effect=fake_dispatch): pos1 = qq._queue_or_dispatch_qq_job( job1, "prompt1", "target", "group", None, None, {}, "img1" ) @@ -243,7 +243,7 @@ def fake_dispatch(job, prompt, target_id, msg_type, run_query_cb, session_ctx, c def test_qq_thread_not_running_initially(): """QQ bridge thread state is properly managed.""" - from bridges.qq import _qq_thread + from cheetahclaws.bridges.qq import _qq_thread # After import, thread may have been started by other tests; # just verify the module-level variable exists and is accessible assert _qq_thread is None or isinstance(_qq_thread, threading.Thread) @@ -251,7 +251,7 @@ def test_qq_thread_not_running_initially(): def test_qq_stop_event_cleared(): """QQ stop event should not be set initially.""" - from bridges.qq import _qq_stop + from cheetahclaws.bridges.qq import _qq_stop assert not _qq_stop.is_set() @@ -259,7 +259,7 @@ def test_post_group_clean_payload_no_msg_id(fake_botpy_route): """_qq_post_group builds clean payload without msg_id/event_id when empty.""" import asyncio from unittest.mock import AsyncMock, MagicMock - from bridges.qq import _qq_post_group + from cheetahclaws.bridges.qq import _qq_post_group api = MagicMock() api._http = MagicMock() @@ -290,7 +290,7 @@ def test_post_group_clean_payload_with_msg_id(fake_botpy_route): """_qq_post_group includes msg_id/msg_seq when msg_id is provided.""" import asyncio from unittest.mock import AsyncMock, MagicMock - from bridges.qq import _qq_post_group + from cheetahclaws.bridges.qq import _qq_post_group api = MagicMock() api._http = MagicMock() @@ -312,7 +312,7 @@ def test_post_group_clean_payload_with_event_id(fake_botpy_route): """_qq_post_group uses event_id when msg_id is None.""" import asyncio from unittest.mock import AsyncMock, MagicMock - from bridges.qq import _qq_post_group + from cheetahclaws.bridges.qq import _qq_post_group api = MagicMock() api._http = MagicMock() @@ -335,7 +335,7 @@ def test_post_c2c_clean_payload(fake_botpy_route): """_qq_post_c2c builds clean payload.""" import asyncio from unittest.mock import AsyncMock, MagicMock - from bridges.qq import _qq_post_c2c + from cheetahclaws.bridges.qq import _qq_post_c2c api = MagicMock() api._http = MagicMock() @@ -357,7 +357,7 @@ def test_post_c2c_clean_payload(fake_botpy_route): def test_msg_seq_starts_at_1_for_new_message(): """First send with reply context should use msg_seq=1.""" import time - from bridges.qq import _qq_reply_ctx, _qq_reply_lock + from cheetahclaws.bridges.qq import _qq_reply_ctx, _qq_reply_lock from unittest.mock import MagicMock, patch # Set up reply context with seq=0 (as stored by _handle_message) @@ -370,9 +370,9 @@ def test_msg_seq_starts_at_1_for_new_message(): def mock_send_group(api, group_openid, content, msg_id=None, event_id=None, msg_seq=1): captured_seqs.append((msg_id, event_id, msg_seq, content)) - with patch("bridges.qq._qq_send_group", side_effect=mock_send_group): - with patch("bridges.qq._qq_api_client", MagicMock()): - from bridges.qq import _qq_send + with patch("cheetahclaws.bridges.qq._qq_send_group", side_effect=mock_send_group): + with patch("cheetahclaws.bridges.qq._qq_api_client", MagicMock()): + from cheetahclaws.bridges.qq import _qq_send _qq_send("test_target", "hello", {}) # First chunk should have msg_seq=1 @@ -388,7 +388,7 @@ def mock_send_group(api, group_openid, content, msg_id=None, event_id=None, msg_ def test_msg_seq_increments_correctly_for_chunks(): """Multiple chunks should increment msg_seq properly.""" import time - from bridges.qq import _qq_reply_ctx, _qq_reply_lock, _QQ_MAX_MSG_LEN + from cheetahclaws.bridges.qq import _qq_reply_ctx, _qq_reply_lock, _QQ_MAX_MSG_LEN from unittest.mock import MagicMock, patch # Set up reply context with seq=0 @@ -400,9 +400,9 @@ def test_msg_seq_increments_correctly_for_chunks(): def mock_send_group(api, group_openid, content, msg_id=None, event_id=None, msg_seq=1): captured_seqs.append((msg_id, event_id, msg_seq, content)) - with patch("bridges.qq._qq_send_group", side_effect=mock_send_group): - with patch("bridges.qq._qq_api_client", MagicMock()): - from bridges.qq import _qq_send + with patch("cheetahclaws.bridges.qq._qq_send_group", side_effect=mock_send_group): + with patch("cheetahclaws.bridges.qq._qq_api_client", MagicMock()): + from cheetahclaws.bridges.qq import _qq_send # Send text that will be split into 2 chunks long_text = "A" * (_QQ_MAX_MSG_LEN + 100) _qq_send("test_target", long_text, {}) @@ -420,7 +420,7 @@ def mock_send_group(api, group_openid, content, msg_id=None, event_id=None, msg_ def test_no_duplicate_send_in_bg_runner(): """_qq_bg_runner should not send duplicate messages.""" from unittest.mock import MagicMock, patch - from bridges.qq import _qq_bg_runner + from cheetahclaws.bridges.qq import _qq_bg_runner session_ctx = MagicMock() @@ -433,11 +433,11 @@ def test_no_duplicate_send_in_bg_runner(): def mock_qq_send(target_id, text, _cfg=None, _msg_type=None): send_calls.append((target_id, text)) - with patch("bridges.qq._qq_api_client", MagicMock()): - with patch("bridges.qq._qq_send", side_effect=mock_qq_send): - with patch("bridges.qq._jobs.start"): - with patch("bridges.qq._jobs.stream_result"): - with patch("bridges.qq._jobs.complete"): + with patch("cheetahclaws.bridges.qq._qq_api_client", MagicMock()): + with patch("cheetahclaws.bridges.qq._qq_send", side_effect=mock_qq_send): + with patch("cheetahclaws.bridges.qq._jobs.start"): + with patch("cheetahclaws.bridges.qq._jobs.stream_result"): + with patch("cheetahclaws.bridges.qq._jobs.complete"): _qq_bg_runner(job, "test prompt", "target123", "group", run_query_cb, session_ctx, {}) @@ -452,8 +452,8 @@ def mock_qq_send(target_id, text, _cfg=None, _msg_type=None): def test_qq_bg_runner_sets_pending_image_for_matching_job(): """Downloaded QQ images should be attached to the job that owns them.""" from unittest.mock import MagicMock, patch - from bridges.qq import _qq_bg_runner - import runtime + from cheetahclaws.bridges.qq import _qq_bg_runner + from cheetahclaws import runtime session_id = "_test_qq_image_job" config = {"_session_id": session_id} @@ -467,9 +467,9 @@ def run_query_cb(_prompt): runtime.get_ctx(config).pending_image = None try: - with patch("bridges.qq._qq_send"): - with patch("bridges.qq._jobs.start"): - with patch("bridges.qq._jobs.complete"): + with patch("cheetahclaws.bridges.qq._qq_send"): + with patch("cheetahclaws.bridges.qq._jobs.start"): + with patch("cheetahclaws.bridges.qq._jobs.complete"): _qq_bg_runner( job, "describe this", @@ -489,8 +489,8 @@ def run_query_cb(_prompt): def test_qq_bg_runner_clears_pending_image_if_run_query_fails_before_consuming(): """A failed image job must not leak its image into the next turn.""" from unittest.mock import MagicMock, patch - from bridges.qq import _qq_bg_runner - import runtime + from cheetahclaws.bridges.qq import _qq_bg_runner + from cheetahclaws import runtime session_id = "_test_qq_image_fail_cleanup" config = {"_session_id": session_id} @@ -502,9 +502,9 @@ def run_query_cb(_prompt): raise RuntimeError("boom before agent consumes image") try: - with patch("bridges.qq._qq_send"): - with patch("bridges.qq._jobs.start"): - with patch("bridges.qq._jobs.fail"): + with patch("cheetahclaws.bridges.qq._qq_send"): + with patch("cheetahclaws.bridges.qq._jobs.start"): + with patch("cheetahclaws.bridges.qq._jobs.fail"): _qq_bg_runner( job, "describe this", @@ -523,7 +523,7 @@ def run_query_cb(_prompt): def test_streaming_hook_idempotency(): """Streaming hooks should handle duplicate calls gracefully.""" from unittest.mock import MagicMock, patch - from bridges.qq import _qq_bg_runner + from cheetahclaws.bridges.qq import _qq_bg_runner session_ctx = MagicMock() @@ -554,10 +554,10 @@ def mock_qq_send(target_id, text, _cfg=None, _msg_type=None): _ = _cfg, _msg_type # Mark as intentionally unused send_calls.append((target_id, text)) - with patch("bridges.qq._qq_api_client", MagicMock()): - with patch("bridges.qq._qq_send", side_effect=mock_qq_send): - with patch("bridges.qq._jobs.start"): - with patch("bridges.qq._jobs.complete"): + with patch("cheetahclaws.bridges.qq._qq_api_client", MagicMock()): + with patch("cheetahclaws.bridges.qq._qq_send", side_effect=mock_qq_send): + with patch("cheetahclaws.bridges.qq._jobs.start"): + with patch("cheetahclaws.bridges.qq._jobs.complete"): _qq_bg_runner(job, "test", "target", "group", mock_run_query, session_ctx, {}) @@ -573,8 +573,8 @@ def mock_qq_send(target_id, text, _cfg=None, _msg_type=None): def test_qq_bg_runner_serializes_global_streaming_hooks(): """Concurrent QQ jobs must not overwrite each other's session-level hooks.""" from unittest.mock import MagicMock, patch - from bridges.qq import _qq_bg_runner - from runtime import RuntimeContext + from cheetahclaws.bridges.qq import _qq_bg_runner + from cheetahclaws.runtime import RuntimeContext session_ctx = RuntimeContext() job_a = MagicMock() @@ -601,10 +601,10 @@ def mock_qq_send(target_id, text, _cfg=None, _msg_type=None): with send_lock: send_calls.append((target_id, text)) - with patch("bridges.qq._qq_send", side_effect=mock_qq_send): - with patch("bridges.qq._jobs.start"): - with patch("bridges.qq._jobs.stream_result"): - with patch("bridges.qq._jobs.complete"): + with patch("cheetahclaws.bridges.qq._qq_send", side_effect=mock_qq_send): + with patch("cheetahclaws.bridges.qq._jobs.start"): + with patch("cheetahclaws.bridges.qq._jobs.stream_result"): + with patch("cheetahclaws.bridges.qq._jobs.complete"): t1 = threading.Thread( target=_qq_bg_runner, args=( diff --git a/tests/test_quota.py b/tests/test_quota.py index 1eba49bd..6702ca80 100644 --- a/tests/test_quota.py +++ b/tests/test_quota.py @@ -10,8 +10,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import quota -from quota import QuotaExceeded, check_quota, record_usage, get_usage, reset_session +from cheetahclaws import quota +from cheetahclaws.quota import QuotaExceeded, check_quota, record_usage, get_usage, reset_session # ── Helpers ─────────────────────────────────────────────────────────────── @@ -41,7 +41,7 @@ def setup_method(self): self.tmpdir = tempfile.mkdtemp() self.sid = "test_sess" _reset_session(self.sid) - self._patcher = patch("quota._quota_dir", return_value=Path(self.tmpdir)) + self._patcher = patch("cheetahclaws.quota._quota_dir", return_value=Path(self.tmpdir)) self._patcher.start() def teardown_method(self): @@ -113,7 +113,7 @@ def setup_method(self): self.tmpdir = tempfile.mkdtemp() self.sid = "rec_sess" _reset_session(self.sid) - self._patcher = patch("quota._quota_dir", return_value=Path(self.tmpdir)) + self._patcher = patch("cheetahclaws.quota._quota_dir", return_value=Path(self.tmpdir)) self._patcher.start() def teardown_method(self): @@ -166,7 +166,7 @@ def setup_method(self): self.tmpdir = tempfile.mkdtemp() self.sid = "get_sess" _reset_session(self.sid) - self._patcher = patch("quota._quota_dir", return_value=Path(self.tmpdir)) + self._patcher = patch("cheetahclaws.quota._quota_dir", return_value=Path(self.tmpdir)) self._patcher.start() def teardown_method(self): @@ -194,7 +194,7 @@ def setup_method(self): self.tmpdir = tempfile.mkdtemp() self.sid = "rst_sess" _reset_session(self.sid) - self._patcher = patch("quota._quota_dir", return_value=Path(self.tmpdir)) + self._patcher = patch("cheetahclaws.quota._quota_dir", return_value=Path(self.tmpdir)) self._patcher.start() def teardown_method(self): @@ -224,7 +224,7 @@ def test_reset_nonexistent_session_is_noop(self): class TestThreadSafety: def setup_method(self): self.tmpdir = tempfile.mkdtemp() - self._patcher = patch("quota._quota_dir", return_value=Path(self.tmpdir)) + self._patcher = patch("cheetahclaws.quota._quota_dir", return_value=Path(self.tmpdir)) self._patcher.start() def teardown_method(self): diff --git a/tests/test_read_overflow_redirect.py b/tests/test_read_overflow_redirect.py index cbcdf463..9c482367 100644 --- a/tests/test_read_overflow_redirect.py +++ b/tests/test_read_overflow_redirect.py @@ -12,7 +12,7 @@ import pytest -from tools.files import ( +from cheetahclaws.tools.files import ( _is_cjk_heavy, _maybe_redirect_to_summarize, ) @@ -153,7 +153,7 @@ def test_read_tool_redirects_huge_text_file(tmp_path): big.write_text("Sample line with content. " * 4000, encoding="utf-8") # ~100KB # Call via the tool registry (simulates what agent.py does) - from tools import execute_tool + from cheetahclaws.tools import execute_tool out = execute_tool( "Read", {"file_path": str(big)}, @@ -171,7 +171,7 @@ def test_read_tool_passes_through_small_file(tmp_path): small = tmp_path / "small.txt" small.write_text("just a few lines\nof normal text\n", encoding="utf-8") - from tools import execute_tool + from cheetahclaws.tools import execute_tool out = execute_tool( "Read", {"file_path": str(small)}, diff --git a/tests/test_readonly_dedup.py b/tests/test_readonly_dedup.py index 5c4d6491..7e945e34 100644 --- a/tests/test_readonly_dedup.py +++ b/tests/test_readonly_dedup.py @@ -25,9 +25,9 @@ import pytest -import agent -from agent import AgentState, run, ToolStart, ToolEnd, TextChunk -from providers import AssistantTurn +from cheetahclaws import agent +from cheetahclaws.agent import AgentState, run, ToolStart, ToolEnd, TextChunk +from cheetahclaws.providers import AssistantTurn def _fake_turn(text="", tool_calls=None): diff --git a/tests/test_render_streaming.py b/tests/test_render_streaming.py index 4d4c72e7..68483718 100644 --- a/tests/test_render_streaming.py +++ b/tests/test_render_streaming.py @@ -1,5 +1,5 @@ import pytest -import ui.render as render +import cheetahclaws.ui.render as render class _FakeLive: diff --git a/tests/test_research.py b/tests/test_research.py index 6cb100bf..d33b2c27 100644 --- a/tests/test_research.py +++ b/tests/test_research.py @@ -27,7 +27,7 @@ # ─── 1. types ────────────────────────────────────────────────────────────── def test_result_defaults(): - from research.types import Result + from cheetahclaws.research.types import Result r = Result(source="x", title="t", url="https://x") assert r.engagement_raw == 0 assert r.engagement_score == 0.0 @@ -36,7 +36,7 @@ def test_result_defaults(): def test_brief_by_domain_groups_and_sorts(): - from research.types import Brief, Result + from cheetahclaws.research.types import Brief, Result rs = [ Result(source="a", title="t1", url="u1", domain="tech", engagement_score=0.2), Result(source="b", title="t2", url="u2", domain="academic", engagement_score=0.9), @@ -58,18 +58,18 @@ def test_brief_by_domain_groups_and_sorts(): ("breaking news today on AI regulation", "news"), ]) def test_classifier_routes_obvious_topics(topic, want_top): - from research.classifier import classify + from cheetahclaws.research.classifier import classify assert classify(topic)[0] == want_top def test_classifier_empty_topic_returns_web(): - from research.classifier import classify + from cheetahclaws.research.classifier import classify assert classify("") == ["web"] assert classify(" ") == ["web"] def test_classifier_never_empty(): - from research.classifier import classify + from cheetahclaws.research.classifier import classify # Gibberish should still yield a nonempty list assert classify("zxqvn mrtwk pfj") != [] @@ -77,8 +77,8 @@ def test_classifier_never_empty(): # ─── 3. ranker ───────────────────────────────────────────────────────────── def test_ranker_normalizes_engagement(): - from research.ranker import rank - from research.types import Result + from cheetahclaws.research.ranker import rank + from cheetahclaws.research.types import Result rs = [ Result(source="hackernews", title="a", url="u1", engagement_raw=100), Result(source="hackernews", title="b", url="u2", engagement_raw=5000), @@ -98,8 +98,8 @@ def test_ranker_normalizes_engagement(): def test_ranker_recency_bonus_for_fresh_results(): from datetime import datetime, timezone - from research.ranker import rank - from research.types import Result + from cheetahclaws.research.ranker import rank + from cheetahclaws.research.types import Result now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") old = "2020-01-01T00:00:00Z" rs = [ @@ -113,8 +113,8 @@ def test_ranker_recency_bonus_for_fresh_results(): def test_ranker_dedupe_keeps_highest_engagement(): - from research.ranker import dedupe - from research.types import Result + from cheetahclaws.research.ranker import dedupe + from cheetahclaws.research.types import Result rs = [ Result(source="a", title="x", url="https://same.com/p", engagement_raw=10), Result(source="b", title="x dup", url="https://same.com/p/", engagement_raw=500), @@ -129,8 +129,8 @@ def test_ranker_dedupe_keeps_highest_engagement(): # ─── 4. cache ────────────────────────────────────────────────────────────── def test_cache_roundtrip(tmp_path, monkeypatch): - from research import cache - from research.types import Result + from cheetahclaws.research import cache + from cheetahclaws.research.types import Result monkeypatch.setattr(cache, "_db_path", lambda: tmp_path / "c.db") rs = [Result(source="s", title="t", url="u", engagement_raw=7)] @@ -142,8 +142,8 @@ def test_cache_roundtrip(tmp_path, monkeypatch): def test_cache_expires(tmp_path, monkeypatch): - from research import cache - from research.types import Result + from cheetahclaws.research import cache + from cheetahclaws.research.types import Result monkeypatch.setattr(cache, "_db_path", lambda: tmp_path / "c.db") rs = [Result(source="s", title="t", url="u")] @@ -154,7 +154,7 @@ def test_cache_expires(tmp_path, monkeypatch): def test_cache_miss_returns_none(tmp_path, monkeypatch): - from research import cache + from cheetahclaws.research import cache monkeypatch.setattr(cache, "_db_path", lambda: tmp_path / "c.db") assert cache.get("s", "nope", 10) is None @@ -165,19 +165,19 @@ def _patch_http_get(monkeypatch, payload): """Replace research.http.get with a function returning `payload`.""" def fake_get(url, params=None, headers=None, **kw): return payload - monkeypatch.setattr("research.http.get", fake_get, raising=False) + monkeypatch.setattr("cheetahclaws.research.http.get", fake_get, raising=False) # Sources import get directly — patch the imported reference too return fake_get def test_hackernews_parses_algolia(): - from research.sources import hackernews + from cheetahclaws.research.sources import hackernews fixture = {"hits": [{ "title": "Test story", "url": "https://example.com/p", "points": 420, "num_comments": 33, "author": "alice", "created_at": "2026-04-01T12:00:00Z", "objectID": "9999", }]} - with mock.patch("research.sources.hackernews.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.hackernews.get", return_value=fixture): rs = hackernews.search("test", 5) assert len(rs) == 1 assert rs[0].engagement_raw == 420 + 16 # 33 // 2 @@ -185,7 +185,7 @@ def test_hackernews_parses_algolia(): def test_semantic_scholar_parses_tldr(): - from research.sources import semantic_scholar as ss + from cheetahclaws.research.sources import semantic_scholar as ss fixture = {"data": [{ "title": "A paper", "abstract": "Long abstract...", @@ -198,7 +198,7 @@ def test_semantic_scholar_parses_tldr(): "externalIds": {}, "openAccessPdf": {"url": "https://arxiv.org/pdf/x.pdf"}, }]} - with mock.patch("research.sources.semantic_scholar.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.semantic_scholar.get", return_value=fixture): rs = ss.search("test", 5) assert len(rs) == 1 assert rs[0].engagement_raw == 42 @@ -207,7 +207,7 @@ def test_semantic_scholar_parses_tldr(): def test_reddit_builds_permalink_url(): - from research.sources import reddit as rd + from cheetahclaws.research.sources import reddit as rd fixture = {"data": {"children": [{"data": { "title": "Reddit post", "subreddit": "programming", @@ -217,7 +217,7 @@ def test_reddit_builds_permalink_url(): "author": "redditor", "created_utc": 1714000000, }}]}} - with mock.patch("research.sources.reddit.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.reddit.get", return_value=fixture): rs = rd.search("test", 5) assert len(rs) == 1 assert rs[0].url.endswith("/r/programming/comments/abc/x/") @@ -225,7 +225,7 @@ def test_reddit_builds_permalink_url(): def test_github_splits_repos_and_issues(): - from research.sources import github as gh + from cheetahclaws.research.sources import github as gh repo_fixture = {"items": [{ "full_name": "foo/bar", "html_url": "https://github.com/foo/bar", @@ -257,14 +257,14 @@ def fake_get(url, params=None, headers=None, **kw): return repo_fixture return issue_fixture - with mock.patch("research.sources.github.get", side_effect=fake_get): + with mock.patch("cheetahclaws.research.sources.github.get", side_effect=fake_get): rs = gh.search("test", 10) assert any("foo/bar" in r.title for r in rs) assert any(r.title.startswith("[issue]") for r in rs) def test_arxiv_parses_atom_feed(monkeypatch): - from research.sources import arxiv + from cheetahclaws.research.sources import arxiv feed = b""" @@ -285,7 +285,7 @@ def __enter__(self): return self def __exit__(self, *a): return False monkeypatch.setattr( - "research.sources.arxiv.urllib.request.urlopen", + "cheetahclaws.research.sources.arxiv.urllib.request.urlopen", lambda req, timeout=None: FakeResp(feed), ) rs = arxiv.search("test", 3) @@ -295,7 +295,7 @@ def __exit__(self, *a): return False def test_google_news_parses_rss(monkeypatch): - from research.sources import google_news + from cheetahclaws.research.sources import google_news rss = b""" @@ -314,7 +314,7 @@ def __enter__(self): return self def __exit__(self, *a): return False monkeypatch.setattr( - "research.sources.google_news.urllib.request.urlopen", + "cheetahclaws.research.sources.google_news.urllib.request.urlopen", lambda req, timeout=None: FakeResp(rss), ) rs = google_news.search("test", 5) @@ -324,7 +324,7 @@ def __exit__(self, *a): return False def test_openalex_reconstructs_inverted_abstract(): - from research.sources import openalex + from cheetahclaws.research.sources import openalex # Abstract: "The quick brown fox" fixture = {"results": [{ "title": "X", @@ -336,13 +336,13 @@ def test_openalex_reconstructs_inverted_abstract(): "The": [0], "quick": [1], "brown": [2], "fox": [3], }, }]} - with mock.patch("research.sources.openalex.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.openalex.get", return_value=fixture): rs = openalex.search("x", 1) assert rs[0].snippet == "The quick brown fox" def test_stackoverflow_strips_html_in_body(): - from research.sources import stackoverflow as so + from cheetahclaws.research.sources import stackoverflow as so fixture = {"items": [{ "title": "Q title", "link": "https://stackoverflow.com/q/1", @@ -354,27 +354,27 @@ def test_stackoverflow_strips_html_in_body(): "last_activity_date": 1714000000, "is_answered": True, }]} - with mock.patch("research.sources.stackoverflow.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.stackoverflow.get", return_value=fixture): rs = so.search("t", 1) assert rs[0].snippet == "Some code here." def test_tavily_skips_without_key(monkeypatch): - from research.sources import SourceSkipped, tavily + from cheetahclaws.research.sources import SourceSkipped, tavily monkeypatch.delenv("TAVILY_API_KEY", raising=False) with pytest.raises(SourceSkipped): tavily.search("q", 5, {}) def test_brave_skips_without_key(monkeypatch): - from research.sources import SourceSkipped, brave + from cheetahclaws.research.sources import SourceSkipped, brave monkeypatch.delenv("BRAVE_API_KEY", raising=False) with pytest.raises(SourceSkipped): brave.search("q", 5, {}) def test_sec_edgar_builds_filing_url(): - from research.sources import sec_edgar + from cheetahclaws.research.sources import sec_edgar fixture = {"hits": {"hits": [{ "_id": "0000320193-26-000001:0001", "_source": { @@ -386,7 +386,7 @@ def test_sec_edgar_builds_filing_url(): "adsh": "0000320193-26-000001", }, }]}} - with mock.patch("research.sources.sec_edgar.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.sec_edgar.get", return_value=fixture): rs = sec_edgar.search("apple", 1) assert len(rs) == 1 assert "APPLE INC" in rs[0].author @@ -394,7 +394,7 @@ def test_sec_edgar_builds_filing_url(): def test_polymarket_filters_by_substring(): - from research.sources import polymarket + from cheetahclaws.research.sources import polymarket # Gamma returns [] or market list fixture = [ {"question": "Will NVIDIA top $200b revenue by EOY?", @@ -405,7 +405,7 @@ def test_polymarket_filters_by_substring(): "slug": "cat-meme", "volume": 1000, "liquidity": 100, "outcomes": "[\"Yes\", \"No\"]", "outcomePrices": "[\"0.1\", \"0.9\"]"}, ] - with mock.patch("research.sources.polymarket.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.polymarket.get", return_value=fixture): rs = polymarket.search("nvidia revenue", 5) assert len(rs) == 1 assert "NVIDIA" in rs[0].title @@ -415,9 +415,9 @@ def test_polymarket_filters_by_substring(): # ─── 6. aggregator ───────────────────────────────────────────────────────── def test_aggregator_fans_out_and_returns_brief(monkeypatch): - from research import aggregator - from research.sources import SOURCES - from research.types import Result + from cheetahclaws.research import aggregator + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.types import Result # Replace all source .search functions with a deterministic mock that # only returns 1 result tagged with the source name. @@ -442,8 +442,8 @@ def _fn(query, limit, config=None): def test_aggregator_reports_source_failures(monkeypatch): - from research import aggregator - from research.sources import SOURCES + from cheetahclaws.research import aggregator + from cheetahclaws.research.sources import SOURCES def boom(q, l, c=None): raise RuntimeError("scripted failure") @@ -460,9 +460,9 @@ def boom(q, l, c=None): def test_aggregator_caches_results(tmp_path, monkeypatch): - from research import aggregator, cache - from research.sources import SOURCES - from research.types import Result + from cheetahclaws.research import aggregator, cache + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.types import Result monkeypatch.setattr(cache, "_db_path", lambda: tmp_path / "c.db") @@ -490,8 +490,8 @@ def one_result(q, l, c=None): # ─── 7. synthesizer ──────────────────────────────────────────────────────── def test_synthesizer_fallback_without_model(): - from research.synthesizer import synthesize - from research.types import Brief, Result + from cheetahclaws.research.synthesizer import synthesize + from cheetahclaws.research.types import Brief, Result brief = Brief( topic="x", domains=["tech"], @@ -505,8 +505,8 @@ def test_synthesizer_fallback_without_model(): def test_synthesizer_citation_numbering(): - from research.synthesizer import render_citations - from research.types import Brief, Result + from cheetahclaws.research.synthesizer import render_citations + from cheetahclaws.research.types import Brief, Result rs = [ Result(source="arxiv", title="Paper A", url="https://a"), Result(source="hackernews", title="Post B", url="https://b", @@ -521,7 +521,7 @@ def test_synthesizer_citation_numbering(): # ─── 8. HTTP helper resilience ───────────────────────────────────────────── def test_http_get_retries_on_5xx(monkeypatch): - from research import http + from cheetahclaws.research import http import urllib.error calls = {"n": 0} @@ -540,8 +540,8 @@ def fake_urlopen(req, timeout=None): io.BytesIO(b"")) return FakeResp(b'{"ok": true}') - monkeypatch.setattr("research.http.urllib.request.urlopen", fake_urlopen) - monkeypatch.setattr("research.http.time.sleep", lambda s: None) + monkeypatch.setattr("cheetahclaws.research.http.urllib.request.urlopen", fake_urlopen) + monkeypatch.setattr("cheetahclaws.research.http.time.sleep", lambda s: None) data = http.get("https://example.com/api") assert data == {"ok": True} @@ -549,13 +549,13 @@ def fake_urlopen(req, timeout=None): def test_http_get_fails_after_retries_exhausted(monkeypatch): - from research import http + from cheetahclaws.research import http def fake_urlopen(req, timeout=None): raise TimeoutError("slow") - monkeypatch.setattr("research.http.urllib.request.urlopen", fake_urlopen) - monkeypatch.setattr("research.http.time.sleep", lambda s: None) + monkeypatch.setattr("cheetahclaws.research.http.urllib.request.urlopen", fake_urlopen) + monkeypatch.setattr("cheetahclaws.research.http.time.sleep", lambda s: None) with pytest.raises(TimeoutError): http.get("https://example.com/api", retries=2) @@ -564,9 +564,9 @@ def fake_urlopen(req, timeout=None): # ─── 9. tools/research.py integration ────────────────────────────────────── def test_research_tool_returns_brief_markdown(monkeypatch): - from research.sources import SOURCES - from research.types import Result - from tools.research import _research + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.types import Result + from cheetahclaws.tools.research import _research for spec in SOURCES.values(): spec.search = lambda q, l, c=None, _s=spec: [ @@ -584,7 +584,7 @@ def test_research_tool_returns_brief_markdown(monkeypatch): # ─── 10. HuggingFace / alphaXiv / Zhihu / Twitter ────────────────────────── def test_huggingface_filters_by_topic_substring(): - from research.sources import huggingface_papers as hf + from cheetahclaws.research.sources import huggingface_papers as hf fixture = [ { "paper": { @@ -604,7 +604,7 @@ def test_huggingface_filters_by_topic_substring(): "numComments": 1, }, ] - with mock.patch("research.sources.huggingface_papers.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.huggingface_papers.get", return_value=fixture): rs = hf.search("transformer", 5) assert len(rs) == 1 assert rs[0].title == "A Transformer Study" @@ -612,15 +612,15 @@ def test_huggingface_filters_by_topic_substring(): def test_huggingface_empty_query_still_filters(): - from research.sources import huggingface_papers as hf - with mock.patch("research.sources.huggingface_papers.get", return_value=[]): + from cheetahclaws.research.sources import huggingface_papers as hf + with mock.patch("cheetahclaws.research.sources.huggingface_papers.get", return_value=[]): rs = hf.search("x", 5) assert rs == [] def test_alphaxiv_wraps_arxiv_and_generates_discussion_urls(monkeypatch): - from research.sources import alphaxiv - from research.types import Result + from cheetahclaws.research.sources import alphaxiv + from cheetahclaws.research.types import Result fake_arxiv_hits = [ Result(source="arxiv", title="Paper A", url="http://arxiv.org/abs/2401.12345v2", @@ -628,7 +628,7 @@ def test_alphaxiv_wraps_arxiv_and_generates_discussion_urls(monkeypatch): Result(source="arxiv", title="Paper B", url="http://arxiv.org/abs/1706.03762", snippet="attention", author="Y", published="2017-06-12", domain="academic"), ] - with mock.patch("research.sources.arxiv.search", return_value=fake_arxiv_hits): + with mock.patch("cheetahclaws.research.sources.arxiv.search", return_value=fake_arxiv_hits): rs = alphaxiv.search("test", 5) assert len(rs) == 2 assert all(r.source == "alphaxiv" for r in rs) @@ -638,14 +638,14 @@ def test_alphaxiv_wraps_arxiv_and_generates_discussion_urls(monkeypatch): def test_zhihu_skips_without_cookie(monkeypatch): - from research.sources import SourceSkipped, zhihu + from cheetahclaws.research.sources import SourceSkipped, zhihu monkeypatch.delenv("ZHIHU_COOKIE", raising=False) with pytest.raises(SourceSkipped): zhihu.search("q", 5, {}) def test_zhihu_parses_answer_type(monkeypatch): - from research.sources import zhihu + from cheetahclaws.research.sources import zhihu monkeypatch.setenv("ZHIHU_COOKIE", "d_c0=abc; z_c0=xyz") fixture = {"data": [{ "type": "search_result", @@ -660,7 +660,7 @@ def test_zhihu_parses_answer_type(monkeypatch): "author": {"name": "张三"}, }, }]} - with mock.patch("research.sources.zhihu.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.zhihu.get", return_value=fixture): rs = zhihu.search("x", 3, {}) assert len(rs) == 1 r = rs[0] @@ -671,7 +671,7 @@ def test_zhihu_parses_answer_type(monkeypatch): def test_zhihu_parses_article_type(monkeypatch): - from research.sources import zhihu + from cheetahclaws.research.sources import zhihu monkeypatch.setenv("ZHIHU_COOKIE", "cookie_val") fixture = {"data": [{ "object": { @@ -683,14 +683,14 @@ def test_zhihu_parses_article_type(monkeypatch): "author": {"name": "李四"}, } }]} - with mock.patch("research.sources.zhihu.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.zhihu.get", return_value=fixture): rs = zhihu.search("x", 3, {}) assert rs[0].url == "https://zhuanlan.zhihu.com/p/123" assert "[article]" in rs[0].title def test_twitter_skips_without_token(monkeypatch): - from research.sources import SourceSkipped, twitter + from cheetahclaws.research.sources import SourceSkipped, twitter monkeypatch.delenv("X_API_BEARER_TOKEN", raising=False) monkeypatch.delenv("TWITTER_BEARER_TOKEN", raising=False) with pytest.raises(SourceSkipped): @@ -698,7 +698,7 @@ def test_twitter_skips_without_token(monkeypatch): def test_twitter_parses_v2_response(monkeypatch): - from research.sources import twitter + from cheetahclaws.research.sources import twitter monkeypatch.setenv("X_API_BEARER_TOKEN", "bearer-xyz") fixture = { "data": [{ @@ -712,7 +712,7 @@ def test_twitter_parses_v2_response(monkeypatch): "includes": {"users": [{"id": "11", "username": "alice", "name": "Alice", "verified": True}]}, } - with mock.patch("research.sources.twitter.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.twitter.get", return_value=fixture): rs = twitter.search("hello", 5) assert len(rs) == 1 r = rs[0] @@ -725,8 +725,8 @@ def test_twitter_parses_v2_response(monkeypatch): # ─── 11. Heat table renderer ─────────────────────────────────────────────── def test_format_heat_table_shows_counts_and_domains(): - from research.synthesizer import format_heat_table - from research.types import Brief, Result, SourceStatus + from cheetahclaws.research.synthesizer import format_heat_table + from cheetahclaws.research.types import Brief, Result, SourceStatus brief = Brief( topic="x", @@ -759,8 +759,8 @@ def test_format_heat_table_shows_counts_and_domains(): def test_format_heat_table_escapes_pipes_in_labels(): - from research.synthesizer import format_heat_table - from research.types import Brief, Result, SourceStatus + from cheetahclaws.research.synthesizer import format_heat_table + from cheetahclaws.research.types import Brief, Result, SourceStatus brief = Brief( topic="x", domains=["tech"], results=[Result(source="s", title="t", url="u", domain="tech", @@ -772,7 +772,7 @@ def test_format_heat_table_escapes_pipes_in_labels(): def test_heat_table_age_formatting(): - from research.synthesizer import _fmt_age + from cheetahclaws.research.synthesizer import _fmt_age assert _fmt_age(0.2) == "4h" assert _fmt_age(1.0) == "1d" assert _fmt_age(27.0) == "27d" @@ -783,7 +783,7 @@ def test_heat_table_age_formatting(): # ─── 12. TimeRange parsing ───────────────────────────────────────────────── def test_time_range_preset_tokens(): - from research.time_range import parse_range + from cheetahclaws.research.time_range import parse_range tr = parse_range("30d") assert tr.is_bounded assert tr.since is not None @@ -791,34 +791,34 @@ def test_time_range_preset_tokens(): def test_time_range_natural_language(): - from research.time_range import parse_range + from cheetahclaws.research.time_range import parse_range tr = parse_range("6months") assert tr.is_bounded assert tr.since is not None def test_time_range_all_means_unbounded(): - from research.time_range import parse_range + from cheetahclaws.research.time_range import parse_range tr = parse_range("all") assert not tr.is_bounded assert tr.since is None and tr.until is None def test_time_range_bad_token_raises(): - from research.time_range import parse_range + from cheetahclaws.research.time_range import parse_range with pytest.raises(ValueError): parse_range("zorp") def test_time_range_iso_date_parsed(): - from research.time_range import parse_iso + from cheetahclaws.research.time_range import parse_iso dt = parse_iso("2024-01-15") assert dt.year == 2024 and dt.month == 1 and dt.day == 15 assert dt.tzinfo is not None def test_time_range_build_combines(): - from research.time_range import build + from cheetahclaws.research.time_range import build tr = build(range_token="30d", since="2024-01-01", until="2024-06-30") # since/until override preset assert tr.since.year == 2024 and tr.since.month == 1 @@ -829,8 +829,8 @@ def test_time_range_build_combines(): # ─── 13. Sources honor time_range ────────────────────────────────────────── def test_arxiv_uses_submittedDate_when_ranged(): - from research.sources import arxiv - from research.time_range import parse_range + from cheetahclaws.research.sources import arxiv + from cheetahclaws.research.time_range import parse_range tr = parse_range("30d") captured = {} @@ -845,7 +845,7 @@ def fake_urlopen(req, timeout=None): captured["url"] = req.full_url return FakeResp(b"""""") - import research.sources.arxiv as ax + import cheetahclaws.research.sources.arxiv as ax ax.urllib.request.urlopen = fake_urlopen # type: ignore arxiv.search("test", 3, time_range=tr) # URL-encoded as `submittedDate%3A` @@ -853,28 +853,28 @@ def fake_urlopen(req, timeout=None): def test_hackernews_uses_numericFilters_when_ranged(): - from research.sources import hackernews - from research.time_range import parse_range + from cheetahclaws.research.sources import hackernews + from cheetahclaws.research.time_range import parse_range captured = {} def fake_get(url, params=None, headers=None, **kw): captured["params"] = params or {} return {"hits": []} - with mock.patch("research.sources.hackernews.get", side_effect=fake_get): + with mock.patch("cheetahclaws.research.sources.hackernews.get", side_effect=fake_get): hackernews.search("test", 5, time_range=parse_range("7d")) assert "numericFilters" in captured["params"] assert "created_at_i>" in captured["params"]["numericFilters"] def test_github_adds_pushed_qualifier_when_ranged(): - from research.sources import github - from research.time_range import parse_range + from cheetahclaws.research.sources import github + from cheetahclaws.research.time_range import parse_range captured = [] def fake_get(url, params=None, headers=None, **kw): captured.append((url, dict(params or {}))) return {"items": []} - with mock.patch("research.sources.github.get", side_effect=fake_get): + with mock.patch("cheetahclaws.research.sources.github.get", side_effect=fake_get): github.search("foo", 5, time_range=parse_range("30d")) # Both repo and issue searches should get date qualifiers qs = [p.get("q", "") for _, p in captured] @@ -883,29 +883,29 @@ def fake_get(url, params=None, headers=None, **kw): def test_openalex_uses_filter_when_ranged(): - from research.sources import openalex - from research.time_range import parse_range + from cheetahclaws.research.sources import openalex + from cheetahclaws.research.time_range import parse_range captured = {} def fake_get(url, params=None, headers=None, **kw): captured["params"] = params or {} return {"results": []} - with mock.patch("research.sources.openalex.get", side_effect=fake_get): + with mock.patch("cheetahclaws.research.sources.openalex.get", side_effect=fake_get): openalex.search("x", 3, time_range=parse_range("1y")) assert "filter" in captured["params"] assert "from_publication_date:" in captured["params"]["filter"] def test_reddit_maps_range_to_t(): - from research.sources import reddit - from research.time_range import parse_range + from cheetahclaws.research.sources import reddit + from cheetahclaws.research.time_range import parse_range captured = {} def fake_get(url, params=None, headers=None, **kw): captured["params"] = params or {} return {"data": {"children": []}} - with mock.patch("research.sources.reddit.get", side_effect=fake_get): + with mock.patch("cheetahclaws.research.sources.reddit.get", side_effect=fake_get): reddit.search("x", 3, time_range=parse_range("7d")) assert captured["params"]["t"] == "week" reddit.search("x", 3, time_range=parse_range("1y")) @@ -915,8 +915,8 @@ def fake_get(url, params=None, headers=None, **kw): # ─── 14. Reports save/load ───────────────────────────────────────────────── def test_report_save_and_read(tmp_path, monkeypatch): - from research import reports as _rep - from research.types import Brief, Result, SourceStatus + from cheetahclaws.research import reports as _rep + from cheetahclaws.research.types import Brief, Result, SourceStatus monkeypatch.setattr(_rep, "_reports_dir", lambda: (tmp_path / "reports").resolve()) @@ -944,8 +944,8 @@ def test_report_save_and_read(tmp_path, monkeypatch): def test_report_save_as_copies_file(tmp_path, monkeypatch): - from research import reports as _rep - from research.types import Brief + from cheetahclaws.research import reports as _rep + from cheetahclaws.research.types import Brief monkeypatch.setattr(_rep, "_reports_dir", lambda: (tmp_path / "rep").resolve()) brief = Brief(topic="x", domains=["tech"], results=[], statuses=[]) @@ -957,8 +957,8 @@ def test_report_save_as_copies_file(tmp_path, monkeypatch): def test_report_delete(tmp_path, monkeypatch): - from research import reports as _rep - from research.types import Brief + from cheetahclaws.research import reports as _rep + from cheetahclaws.research.types import Brief monkeypatch.setattr(_rep, "_reports_dir", lambda: (tmp_path / "r").resolve()) b = Brief(topic="abc", domains=[], results=[], statuses=[]) @@ -972,8 +972,8 @@ def test_report_delete(tmp_path, monkeypatch): def test_publication_trend_bars(): from datetime import datetime, timezone, timedelta - from research.synthesizer import format_publication_trend - from research.types import Brief, Result + from cheetahclaws.research.synthesizer import format_publication_trend + from cheetahclaws.research.types import Brief, Result now = datetime.now(timezone.utc) rs = [ Result(source="arxiv", title=f"p{i}", url=f"u{i}", @@ -988,8 +988,8 @@ def test_publication_trend_bars(): def test_publication_sparkline_uses_unicode_bars(): from datetime import datetime, timezone, timedelta - from research.synthesizer import format_publication_sparkline - from research.types import Brief, Result + from cheetahclaws.research.synthesizer import format_publication_sparkline + from cheetahclaws.research.types import Brief, Result now = datetime.now(timezone.utc) rs = [ Result(source="arxiv", title=f"p{i}", url=f"u{i}", @@ -1006,8 +1006,8 @@ def test_publication_sparkline_uses_unicode_bars(): # ─── 16. Citations helper ────────────────────────────────────────────────── def test_citation_extract_ss_id(): - from research.citations import _extract_ss_id - from research.types import Result + from cheetahclaws.research.citations import _extract_ss_id + from cheetahclaws.research.types import Result r1 = Result(source="semantic_scholar", title="T", url="https://www.semanticscholar.org/paper/abc/def0123") assert _extract_ss_id(r1) == "def0123" r2 = Result(source="semantic_scholar", title="T", url="https://arxiv.org/abs/2401.12345v1") @@ -1017,7 +1017,7 @@ def test_citation_extract_ss_id(): def test_notable_citers_rendering(): - from research.citations import NotableCiter, render_notable_section + from cheetahclaws.research.citations import NotableCiter, render_notable_section ns = [ NotableCiter(name="Yoshua Bengio", author_id="aa", total_citations=450000, h_index=230, @@ -1033,7 +1033,7 @@ def test_notable_citers_rendering(): # ─── 17. Google Scholar graceful skip ────────────────────────────────────── def test_google_scholar_skips_without_scholarly(monkeypatch): - from research.sources import SourceSkipped, google_scholar + from cheetahclaws.research.sources import SourceSkipped, google_scholar import sys # Pretend `scholarly` isn't installed by removing any cached module sys.modules.pop("scholarly", None) @@ -1056,7 +1056,7 @@ def fake_import(name, *args, **kwargs): # ─── 19. Chinese platform sources ────────────────────────────────────────── def test_bilibili_parses_video_group(): - from research.sources import bilibili + from cheetahclaws.research.sources import bilibili fixture = { "code": 0, "data": { @@ -1078,7 +1078,7 @@ def test_bilibili_parses_video_group(): }], }, } - with mock.patch("research.sources.bilibili.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.bilibili.get", return_value=fixture): rs = bilibili.search("transformer", 5) assert len(rs) == 1 r = rs[0] @@ -1091,22 +1091,22 @@ def test_bilibili_parses_video_group(): def test_bilibili_skips_non_ok_code(): - from research.sources import bilibili + from cheetahclaws.research.sources import bilibili fixture = {"code": -401, "message": "anti-bot", "data": None} - with mock.patch("research.sources.bilibili.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.bilibili.get", return_value=fixture): rs = bilibili.search("x", 5) assert rs == [] def test_weibo_skips_without_cookie(monkeypatch): - from research.sources import SourceSkipped, weibo + from cheetahclaws.research.sources import SourceSkipped, weibo monkeypatch.delenv("WEIBO_COOKIE", raising=False) with pytest.raises(SourceSkipped): weibo.search("x", 5, {}) def test_weibo_parses_mblog(monkeypatch): - from research.sources import weibo + from cheetahclaws.research.sources import weibo monkeypatch.setenv("WEIBO_COOKIE", "SUB=xxx;SUBP=yyy") fixture = { "ok": 1, @@ -1123,7 +1123,7 @@ def test_weibo_parses_mblog(monkeypatch): }, }]}, } - with mock.patch("research.sources.weibo.get", return_value=fixture): + with mock.patch("cheetahclaws.research.sources.weibo.get", return_value=fixture): rs = weibo.search("transformer", 5, {}) assert len(rs) == 1 r = rs[0] @@ -1135,7 +1135,7 @@ def test_weibo_parses_mblog(monkeypatch): def test_weibo_date_parser_relative(): - from research.sources.weibo import _parse_weibo_date + from cheetahclaws.research.sources.weibo import _parse_weibo_date import re assert re.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", _parse_weibo_date("5分钟前")) @@ -1145,7 +1145,7 @@ def test_weibo_date_parser_relative(): def test_xiaohongshu_skips_without_cookie(monkeypatch): - from research.sources import SourceSkipped, xiaohongshu + from cheetahclaws.research.sources import SourceSkipped, xiaohongshu monkeypatch.delenv("XHS_COOKIE", raising=False) monkeypatch.delenv("XIAOHONGSHU_COOKIE", raising=False) with pytest.raises(SourceSkipped): @@ -1153,8 +1153,8 @@ def test_xiaohongshu_skips_without_cookie(monkeypatch): def test_xiaohongshu_parses_localized_counts(monkeypatch): - from research.sources import xiaohongshu - from research.sources.xiaohongshu import _parse_count + from cheetahclaws.research.sources import xiaohongshu + from cheetahclaws.research.sources.xiaohongshu import _parse_count assert _parse_count("1.2w") == 12000 assert _parse_count("3万") == 30000 assert _parse_count("500") == 500 @@ -1164,7 +1164,7 @@ def test_xiaohongshu_parses_localized_counts(monkeypatch): def test_xiaohongshu_parses_success_response(monkeypatch): - from research.sources import xiaohongshu + from cheetahclaws.research.sources import xiaohongshu monkeypatch.setenv("XHS_COOKIE", "test-cookie") fixture = { "success": True, @@ -1184,7 +1184,7 @@ def test_xiaohongshu_parses_success_response(monkeypatch): }, }]}, } - with mock.patch("research.sources.xiaohongshu.post_json", + with mock.patch("cheetahclaws.research.sources.xiaohongshu.post_json", return_value=fixture): rs = xiaohongshu.search("transformer", 5, {}) assert len(rs) == 1 @@ -1198,10 +1198,10 @@ def test_xiaohongshu_parses_success_response(monkeypatch): # ─── 20. Monitor research: fetcher ────────────────────────────────── def test_monitor_fetcher_dispatches_research_prefix(monkeypatch, tmp_path): - from monitor import fetchers - from research import cache as _cache - from research.sources import SOURCES - from research.types import Result + from cheetahclaws.monitor import fetchers + from cheetahclaws.research import cache as _cache + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.types import Result monkeypatch.setattr(_cache, "_db_path", lambda: tmp_path / "c.db") @@ -1219,10 +1219,10 @@ def test_monitor_fetcher_dispatches_research_prefix(monkeypatch, tmp_path): def test_monitor_fetcher_research_with_range_prefix(monkeypatch, tmp_path): - from monitor import fetchers - from research import cache as _cache - from research.sources import SOURCES - from research.types import Result + from cheetahclaws.monitor import fetchers + from cheetahclaws.research import cache as _cache + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.types import Result # Isolate cache so prior tests' cached entries don't mask the mocks monkeypatch.setattr(_cache, "_db_path", lambda: tmp_path / "c.db") @@ -1244,8 +1244,8 @@ def _s(q, l, c=None, time_range=None, _name=spec.name, _dom=spec.domains[0]): # ─── 21. Entity extraction ───────────────────────────────────────────────── def test_entity_extraction_picks_up_known_models(): - from research.entities import extract - from research.types import Result + from cheetahclaws.research.entities import extract + from cheetahclaws.research.types import Result rs = [ Result(source="reddit", title="GPT-5 vs Claude-Opus-5: which is better", url="u1", snippet="GPT-5 dominates on MMLU but Claude-Opus-5 wins on coding", domain="social"), @@ -1269,8 +1269,8 @@ def test_entity_extraction_picks_up_known_models(): def test_entity_extraction_orgs(): - from research.entities import extract - from research.types import Result + from cheetahclaws.research.entities import extract + from cheetahclaws.research.types import Result rs = [ Result(source="news", title="OpenAI releases GPT-5", url="u", snippet="Anthropic and Google DeepMind respond.", domain="news"), @@ -1289,8 +1289,8 @@ def test_entity_extraction_orgs(): def test_entity_extraction_dedupes_within_single_result(): """A result mentioning the same model 10 times should count as 1.""" - from research.entities import extract - from research.types import Result + from cheetahclaws.research.entities import extract + from cheetahclaws.research.types import Result rs = [Result(source="x", title="GPT-5 is amazing", url="u", snippet="GPT-5 GPT-5 GPT-5 GPT-5 GPT-5 GPT-5", domain="tech")] @@ -1302,8 +1302,8 @@ def test_entity_extraction_dedupes_within_single_result(): def test_entity_extraction_people_from_author(): - from research.entities import extract - from research.types import Result + from cheetahclaws.research.entities import extract + from cheetahclaws.research.types import Result rs = [ Result(source="arxiv", title="paper 1", url="u1", author="Alice Smith, Bob Chen", domain="academic"), @@ -1317,7 +1317,7 @@ def test_entity_extraction_people_from_author(): def test_entity_table_renders_markdown(): - from research.entities import Entities, render_entities_table + from cheetahclaws.research.entities import Entities, render_entities_table e = Entities( models=[("GPT-5", 7), ("Claude-Opus-5", 4)], benchmarks=[("MMLU", 3)], @@ -1333,20 +1333,20 @@ def test_entity_table_renders_markdown(): def test_entity_table_empty_returns_empty_string(): - from research.entities import Entities, render_entities_table + from cheetahclaws.research.entities import Entities, render_entities_table assert render_entities_table(Entities()) == "" # ─── 22. Multi-query expansion ───────────────────────────────────────────── def test_expand_subqueries_no_model_returns_empty(): - from research.aggregator import _expand_subqueries + from cheetahclaws.research.aggregator import _expand_subqueries assert _expand_subqueries("topic", 4, config={}) == [] def test_expand_subqueries_parses_model_lines(monkeypatch): - from research import aggregator - from research.types import Result + from cheetahclaws.research import aggregator + from cheetahclaws.research.types import Result fake_lines = [ "LLM evaluation benchmarks safety", @@ -1373,25 +1373,25 @@ def fake_stream(**kwargs): # leak the stub into later tests. Previously this finally was a no-op, # which broke tests/test_setup_wizard.py and any other suite that ran # after this one and tried `from providers import PROVIDERS`. - real_providers = sys.modules.get("providers") - sys.modules["providers"] = fake_providers + real_providers = sys.modules.get("cheetahclaws.providers") + sys.modules["cheetahclaws.providers"] = fake_providers try: out = aggregator._expand_subqueries("frontier LLM benchmarks", 4, config={"model": "test"}) finally: if real_providers is not None: - sys.modules["providers"] = real_providers + sys.modules["cheetahclaws.providers"] = real_providers else: - sys.modules.pop("providers", None) + sys.modules.pop("cheetahclaws.providers", None) assert len(out) == 4 assert all(5 < len(ln) < 150 for ln in out) def test_aggregator_expand_produces_multi_query_cache_keys(monkeypatch, tmp_path): - from research import aggregator, cache as _cache - from research.sources import SOURCES - from research.types import Result + from cheetahclaws.research import aggregator, cache as _cache + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.types import Result monkeypatch.setattr(_cache, "_db_path", lambda: tmp_path / "c.db") @@ -1424,9 +1424,9 @@ def _s(q, l, c=None, time_range=None, _name=spec.name, _dom=spec.domains[0]): # ─── 23. Compare mode ────────────────────────────────────────────────────── def test_compare_runs_two_queries(monkeypatch, tmp_path): - from research import aggregator, cache as _cache - from research.sources import SOURCES - from research.types import Result + from cheetahclaws.research import aggregator, cache as _cache + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.types import Result monkeypatch.setattr(_cache, "_db_path", lambda: tmp_path / "c.db") for spec in SOURCES.values(): @@ -1447,9 +1447,9 @@ def _s(q, l, c=None, time_range=None, _name=spec.name, _dom=spec.domains[0]): def test_compare_three_topics(monkeypatch, tmp_path): - from research import aggregator, cache as _cache - from research.sources import SOURCES - from research.types import Result + from cheetahclaws.research import aggregator, cache as _cache + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.types import Result monkeypatch.setattr(_cache, "_db_path", lambda: tmp_path / "c.db") for spec in SOURCES.values(): @@ -1467,8 +1467,8 @@ def test_compare_three_topics(monkeypatch, tmp_path): def test_render_compare_brief_has_all_topics_cited(): - from research.synthesizer import render_compare_brief - from research.types import Brief, Result, SourceStatus + from cheetahclaws.research.synthesizer import render_compare_brief + from cheetahclaws.research.types import Brief, Result, SourceStatus b1 = Brief(topic="A", domains=["tech"], results=[Result(source="hn", title="T1", url="u1", domain="tech")], @@ -1487,10 +1487,10 @@ def test_render_compare_brief_has_all_topics_cited(): def test_aggregator_threads_time_range_into_sources(monkeypatch): - from research import aggregator - from research.sources import SOURCES - from research.time_range import parse_range - from research.types import Result + from cheetahclaws.research import aggregator + from cheetahclaws.research.sources import SOURCES + from cheetahclaws.research.time_range import parse_range + from cheetahclaws.research.types import Result received: dict[str, object] = {} diff --git a/tests/test_research_lab.py b/tests/test_research_lab.py index 82e703bf..e2aed009 100644 --- a/tests/test_research_lab.py +++ b/tests/test_research_lab.py @@ -25,12 +25,12 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from research.lab import storage as _storage -from research.lab import convergence as _conv -from research.lab import verifier as _verifier -from research.lab import roles as _roles -from research.lab import orchestrator as _orch -from research.lab import output as _output +from cheetahclaws.research.lab import storage as _storage +from cheetahclaws.research.lab import convergence as _conv +from cheetahclaws.research.lab import verifier as _verifier +from cheetahclaws.research.lab import roles as _roles +from cheetahclaws.research.lab import orchestrator as _orch +from cheetahclaws.research.lab import output as _output # ── Storage ──────────────────────────────────────────────────────────────── @@ -71,7 +71,7 @@ def test_verify_citations_per_citation_hard_timeout(monkeypatch): """If verify_one hangs, the wall-clock cap must kick in and mark the citation skipped — not block the whole stage forever (we observed 11 minutes of hang on a slow-loris arxiv socket in the field).""" - from research.lab.verifier import ( + from cheetahclaws.research.lab.verifier import ( verify_citations, Citation, CitationVerification, ) @@ -79,7 +79,7 @@ def _hangs_forever(*args, **kwargs): time.sleep(120) return CitationVerification(citation=args[0], status="verified") - monkeypatch.setattr("research.lab.verifier.verify_one", _hangs_forever) + monkeypatch.setattr("cheetahclaws.research.lab.verifier.verify_one", _hangs_forever) cits = [Citation(key=f"hung{i}", title=f"hung paper #{i}", authors=[]) for i in range(2)] t0 = time.time() @@ -99,7 +99,7 @@ def _hangs_forever(*args, **kwargs): def test_verify_citations_stage_budget(monkeypatch): """If the stage runs out of total wall time, remaining citations get marked skipped without being attempted.""" - from research.lab.verifier import verify_citations, Citation, CitationVerification + from cheetahclaws.research.lab.verifier import verify_citations, Citation, CitationVerification call_log = [] def _slow(citation, *, timeout_s=10.0): @@ -107,7 +107,7 @@ def _slow(citation, *, timeout_s=10.0): time.sleep(0.4) # each call eats some of the stage budget return CitationVerification(citation=citation, status="not_found") - monkeypatch.setattr("research.lab.verifier.verify_one", _slow) + monkeypatch.setattr("cheetahclaws.research.lab.verifier.verify_one", _slow) cits = [Citation(key=f"p{i}", title=f"paper {i}", authors=[]) for i in range(10)] result = verify_citations( @@ -127,9 +127,9 @@ def _slow(citation, *, timeout_s=10.0): def test_verify_citations_progress_callback(monkeypatch): - from research.lab.verifier import verify_citations, Citation, CitationVerification + from cheetahclaws.research.lab.verifier import verify_citations, Citation, CitationVerification monkeypatch.setattr( - "research.lab.verifier.verify_one", + "cheetahclaws.research.lab.verifier.verify_one", lambda c, **_: CitationVerification(citation=c, status="verified"), ) cits = [Citation(key=f"k{i}", title=f"p{i}", authors=[]) for i in range(3)] @@ -145,7 +145,7 @@ def test_verify_citations_progress_callback(monkeypatch): def test_slugify_basic(): - from research.lab.storage import _slugify + from cheetahclaws.research.lab.storage import _slugify assert _slugify("Post-Transformer architectures: SSM vs Mamba 2026") \ == "post-transformer-architectures-ssm-vs-mamba-2026" assert _slugify(" hello, world!! ") == "hello-world" @@ -153,7 +153,7 @@ def test_slugify_basic(): def test_slugify_truncates_at_word_boundary(): - from research.lab.storage import _slugify + from cheetahclaws.research.lab.storage import _slugify long_topic = "comparative analysis of state space models linear attention mixture of experts retentive networks 2026" s = _slugify(long_topic, max_len=60) assert len(s) <= 60 @@ -163,14 +163,14 @@ def test_slugify_truncates_at_word_boundary(): def test_slugify_chinese_falls_back_to_untitled(): - from research.lab.storage import _slugify + from cheetahclaws.research.lab.storage import _slugify assert _slugify("后 transformer 时代") == "transformer" # "transformer" is ASCII assert _slugify("纯中文话题") == "untitled" assert _slugify("") == "untitled" def test_human_dir_name_format(): - from research.lab.storage import human_dir_name + from cheetahclaws.research.lab.storage import human_dir_name import datetime as _dt # Fixed timestamp: 2026-05-07 18:15:00 local ts = _dt.datetime(2026, 5, 7, 18, 15).timestamp() @@ -187,7 +187,7 @@ def test_human_dir_name_format(): def test_human_dir_name_uniqueness_via_run_id_suffix(): """Two runs with the same topic + minute must NOT collide.""" - from research.lab.storage import human_dir_name + from cheetahclaws.research.lab.storage import human_dir_name import datetime as _dt ts = _dt.datetime(2026, 5, 7, 18, 15).timestamp() a = human_dir_name(run_id="lab_aaaaaaaaaaaa", topic="same topic", @@ -200,7 +200,7 @@ def test_human_dir_name_uniqueness_via_run_id_suffix(): def test_output_dir_for_uses_human_format(tmp_path): - from research.lab.storage import output_dir_for + from cheetahclaws.research.lab.storage import output_dir_for import datetime as _dt ts = _dt.datetime(2026, 5, 7, 18, 15).timestamp() p = output_dir_for( @@ -702,7 +702,7 @@ def test_write_markdown_report_assembles_artifacts(tmp_path): # Build a minimal LabRun shim; output.write_markdown_report just needs # state.run_id, state.topic, storage. - from research.lab.orchestrator import LabState, LabRun, Stage + from cheetahclaws.research.lab.orchestrator import LabState, LabRun, Stage state = LabState(run_id=rec.run_id, topic="My topic", stage=Stage.FINALIZATION) run = LabRun(state=state, storage=storage, roles=_roles.build_default_assignment({}), @@ -715,7 +715,7 @@ def test_write_markdown_report_assembles_artifacts(tmp_path): # Output dir is now _