From 27cc9259656f1109a0c1373b308e3f701cf150cd Mon Sep 17 00:00:00 2001 From: curo1305 Date: Sun, 17 May 2026 18:09:31 +0200 Subject: [PATCH] docs: add workflow rules and full code inventory to CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents all third-party libraries, stdlib modules, internal utility functions, and classes with signatures and import paths. Adds workflow rules for bugfixes (≤50 lines), duplication avoidance, and commit discipline. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index b4ebfab..7ea3aa2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -233,3 +233,200 @@ test: description docs: description chore: description ``` + +--- + +## Workflow Rules + +### Bugfixes + +- **Stay under 50 lines changed.** Find the root cause and fix it directly. +- If the fix seems to require more than 50 lines, it is probably a refactor, not a bugfix — stop and discuss with the user before proceeding. +- Do not write workarounds, fallback layers, or compatibility shims to route around a bug. Remove the cause. + +### Committing Changes + +- **Commit after every logical unit of work** — do not batch unrelated changes into one commit and do not wait until the end of a session. +- **One commit per concern.** If a session touches a file for two different reasons (e.g. a bugfix and a cleanup), those are two separate commits — staged and committed independently, even if the file is the same. +- Use the project commit convention: `feat(module):`, `fix(module):`, `test:`, `docs:`, `chore:` followed by a short description. +- Always `git add` only the files relevant to that commit — never `git add .` blindly. +- Do **not** push unless explicitly asked to. + +### Avoid Duplication — Check the Inventory First + +Before writing any new utility function, class, or import block, check the **Code Inventory** section below. Everything listed there already exists and is importable. Writing a duplicate wastes code and introduces divergence. + +--- + +## Code Inventory + +### Third-party libraries (`pyproject.toml` dependencies) + +| Library | Min version | Used in | Purpose | +|---------|-------------|---------|---------| +| `litellm` | 1.40.0 | `chat/session.py`, `setup/wizard.py` | Multi-provider LLM completion (streaming + non-streaming) and tool-use dispatch | +| `rich` | 13.0.0 | `chat/renderer.py`, `cli.py`, `setup/wizard.py`, `plugins/executor.py` | Terminal UI — `Console`, `Panel`, `Markdown`, `Live`, `Text` | +| `click` | 8.1.0 | `cli.py` | CLI entrypoint, `@click.group`, `@click.command`, arguments | +| `prompt_toolkit` | 3.0.0 | `chat/session.py` | REPL input loop — `PromptSession`, `FileHistory` | +| `questionary` | 2.0.0 | `setup/wizard.py` | Interactive `select` / `text` / `password` prompts | +| `ruamel.yaml` | 0.18.0 | `config/manager.py` | Round-trip YAML read/write (preserves comments and formatting) | +| `pydantic` | 2.0.0 | `config/schema.py` | Config validation via `BaseModel` | +| `httpx` | 0.27.0 | `setup/wizard.py` | HTTP GET for local-server connectivity checks | + +Optional plugin extras (declared in `pyproject.toml [project.optional-dependencies]`): + +| Extra | Libraries | Intended for | +|-------|-----------|--------------| +| `nextcloud` | `caldav`, `webdav4`, `vobject` | CalDAV / CardDAV / WebDAV | +| `matrix` | `matrix-nio`, `aiofiles` | Matrix bot | +| `telegram` | `python-telegram-bot` | Telegram bot | +| `ssh` | `paramiko` | SSH plugin | +| `docker` | `docker` | Docker plugin | +| `gdrive` | `google-api-python-client`, `google-auth-oauthlib` | Google Drive | +| `onedrive` | `msal` | OneDrive device-flow auth | +| `dropbox` | `dropbox` | Dropbox | + +### Standard library modules in use + +| Module | Used in | Notes | +|--------|---------|-------| +| `pathlib.Path` | everywhere | Default for all paths — never use `os.path` string joins | +| `os` | `utils/paths.py` | Only for `os.name` (Windows guard) | +| `json` | `vault/reader.py`, `vault/writer.py`, `plugins/loader.py`, `plugins/executor.py`, `plugins/install.py` | Vault file, manifests, tool args/results | +| `re` | `security/injection.py` | Compiled injection-detection patterns | +| `datetime` | `security/injection.py`, `memory/reader.py`, `memory/index.py`, `plugins/loader.py`, `plugins/executor.py` | Log timestamps, file mtimes | +| `dataclasses` | `security/injection.py`, `memory/reader.py`, `plugins/base.py` | `@dataclass` — `InjectionWarning`, `MemoryFile`, `Tool` | +| `importlib.util` | `plugins/loader.py` | Dynamic plugin loading (`spec_from_file_location`) | +| `sys` | `cli.py`, `plugins/loader.py` | `sys.exit`, `sys.modules` for dynamic module registration | +| `shutil` | `plugins/install.py` | `copytree`, `rmtree` for bundled plugin installation | +| `typing` | `plugins/base.py`, `chat/history.py`, `plugins/registry.py` | `Protocol`, `Callable`, `Coroutine`, `Any`, `TYPE_CHECKING` | + +### Internal utility functions — import, do not rewrite + +#### `utils.paths` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `pyra_home` | `() -> Path` | Returns `~/.pyra/` | +| `ensure_dir` | `(path: Path, mode=0o700) -> Path` | `mkdir -p` + `chmod` in one call | +| `safe_chmod` | `(path: Path, mode: int) -> None` | `chmod` that silently skips on Windows | + +#### `security.boundaries` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `assert_safe_path` | `(path: Path) -> None` | Raises `VaultAccessError` if path resolves into vault | +| `check_vault_lock` | `() -> None` | Raises `PyraSecurityError` if vault sentinel is missing | + +Exceptions: `VaultAccessError(PermissionError)`, `PyraSecurityError(RuntimeError)` + +#### `security.injection` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `scan_response` | `(text: str) -> list[InjectionWarning]` | Runs 15 compiled regex patterns, logs hits to `security.log` | +| `redact_api_keys` | `(text: str) -> str` | Replaces key-shaped strings with `[REDACTED]` | + +Dataclass: `InjectionWarning(pattern_label: str, matched_text: str)` + +#### `config.manager` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `load_config` | `() -> PyraConfig` | Reads `config.yaml`, validates via Pydantic; raises `FileNotFoundError` if missing | +| `save_config` | `(cfg: PyraConfig) -> None` | Writes `config.yaml`, enforces `chmod 600` | +| `config_exists` | `() -> bool` | True if `config.yaml` exists | +| `config_path` | `() -> Path` | Absolute path to `config.yaml` | + +#### `config.dirs` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `bootstrap` | `() -> None` | Creates `~/.pyra/` directory tree and checks vault sentinel; called at every startup | + +#### `vault.reader` / `vault.writer` + +| Function | Module | Signature | Purpose | +|----------|--------|-----------|---------| +| `get_key` | `vault.reader` | `(provider_id: str) -> str \| None` | Sole vault reader — never call `open(api_keys.json)` anywhere else | +| `set_key` | `vault.writer` | `(provider_id: str, api_key: str) -> None` | Stores or overwrites a key in the vault | +| `delete_key` | `vault.writer` | `(provider_id: str) -> bool` | Removes a key; returns `True` if it existed | + +#### `memory.reader` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `list_memories` | `() -> list[MemoryFile]` | Scans `~/.pyra/memory/**/*.md`; each entry is a `MemoryFile` dataclass | +| `read_memory` | `(name: str) -> str` | Reads memory file by relative path; validates against vault/traversal | +| `load_context_for_session` | `() -> str` | Concatenates all memory files into a system-prompt block | + +Dataclass: `MemoryFile(name, path, category, size_bytes, modified)` + +#### `memory.writer` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `write_memory` | `(name: str, content: str) -> Path` | Creates/overwrites a memory `.md` file, updates index | +| `append_memory` | `(name: str, content: str) -> Path` | Appends to a memory file (creates if missing), updates index | + +#### `memory.index` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `update_index` | `() -> None` | Regenerates `MEMORY_INDEX.md` — called automatically by writer functions | + +#### `setup.providers` + +| Symbol | Kind | Purpose | +|--------|------|---------| +| `PROVIDERS` | `list[Provider]` | All registered providers in display order | +| `PROVIDERS_BY_ID` | `dict[str, Provider]` | Fast id lookup | +| `get_provider` | `(provider_id: str) -> Provider` | Raises `KeyError` for unknown ids | +| `Provider` | frozen dataclass | `id`, `display_name`, `requires_key`, `default_model`, `litellm_prefix`, `base_url`, `key_env_var`, `connectivity_check`, `group` | + +#### `plugins.loader` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `load_plugins` | `(plugins_dir: Path) -> list[PyraPlugin]` | Discovers all valid plugin directories | +| `load_plugin_by_name` | `(name: str, plugins_dir: Path) -> PyraPlugin \| None` | Loads a single plugin; returns `None` on any failure | + +#### `plugins.install` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `get_bundled_plugins_dir` | `() -> Path` | Path to `src/pyra/bundled_plugins/` | +| `install_bundled_plugin` | `(name, bundled_dir, plugins_dir) -> None` | Copies bundled plugin dir to `~/.pyra/plugins/`, sets permissions | +| `list_bundled_plugins` | `(bundled_dir: Path) -> list[str]` | Names of all bundled plugins that have a `manifest.json` | +| `read_manifest` | `(plugin_dir: Path) -> dict` | Reads `manifest.json`; returns `{}` if missing | + +#### `chat.renderer` — rendering functions and shared `console` + +Import `console` from here; do not create a second `rich.Console()` in new code. + +| Symbol | Purpose | +|--------|---------| +| `console` | Module-level `rich.Console` — the single shared terminal instance | +| `render_streaming_response(stream)` | Renders a litellm streaming response with `Live` + `Markdown`, returns final text | +| `render_text_response(text)` | Renders a complete string as `Markdown` | +| `render_injection_warning(warnings)` | Yellow `Panel` showing detected pattern labels | +| `render_error(message)` | Red `Panel` | +| `render_info(message)` | Dim plain text line | +| `render_system(message)` | Cyan `Panel` | + +### Internal classes + +| Class | Module | Notes | +|-------|--------|-------| +| `PyraConfig` | `config.schema` | Top-level config; fields: `ai`, `memory`, `security`, `plugins`, `daemon` | +| `ProviderConfig` | `config.schema` | `ai:` block — `provider_id`, `model`, `base_url` | +| `PluginConfig` | `config.schema` | `plugins:` block — `enabled`, `require_approval`, `log_executions` | +| `DaemonConfig` | `config.schema` | `daemon:` block | +| `MemoryConfig` | `config.schema` | `memory:` block — `max_tokens_in_context`, `auto_load` | +| `SecurityConfig` | `config.schema` | `security:` block — `injection_detection`, `log_injections` | +| `ConversationHistory` | `chat.history` | Holds message list; builds API payload via `build_for_api()`; trims to token budget | +| `PluginRegistry` | `plugins.registry` | Singleton (`instance()` / `reset()`); aggregates tools, slash commands, system prompt additions | +| `ToolExecutor` | `plugins.executor` | Approval gate + injection scan + logging; call via `execute()` or `execute_tool_call_batch()` | +| `Tool` | `plugins.base` | Dataclass — `name`, `description`, `parameters` (JSON Schema), `handler`, `requires_approval` | +| `PyraPlugin` | `plugins.base` | `@runtime_checkable` Protocol — the plugin interface | +| `BasePlugin` | `plugins.base` | Concrete base with no-op defaults; plugins should inherit this |