feat(config): add /config TUI with tab-based settings and plugin config framework
- textual-based ConfigApp with General, Plugins, and per-plugin tabs - GeneralConfig (user_name, assistant_name) + plugin_settings dict added to PyraConfig - ConfigField dataclass and config_fields() method added to plugin protocol - /config slash command in chat REPL launches the TUI - pyra auto-runs setup wizard on first invocation when no config.yaml exists - CLAUDE.md updated with config_fields() plugin guide and Code Inventory entries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,8 @@ the vault under namespaced keys (`plugin:{name}:{key}`).
|
||||
| `cli.py` | Click entrypoint. Subcommands: `setup`, `chat`, `memory`, `plugin`, `daemon` |
|
||||
| `setup/providers.py` | Provider registry — pure data, no I/O |
|
||||
| `setup/wizard.py` | questionary-based interactive setup wizard |
|
||||
| `config/schema.py` | Pydantic v2 models — `PyraConfig`, `PluginConfig`, `DaemonConfig` |
|
||||
| `config/schema.py` | Pydantic v2 models — `PyraConfig`, `GeneralConfig`, `PluginConfig`, `DaemonConfig`; `plugin_settings` dict |
|
||||
| `config/tui.py` | `textual`-based `/config` TUI — `ConfigApp`, `GENERAL_FIELDS`, `launch_config_tui()` |
|
||||
| `config/manager.py` | ruamel.yaml round-trip config read/write, chmod 600 enforced |
|
||||
| `config/dirs.py` | `bootstrap()` — creates `~/.pyra/` tree, checks vault sentinel every startup |
|
||||
| `chat/session.py` | prompt_toolkit REPL loop, AI tool-use loop, plugin slash commands |
|
||||
@@ -160,7 +161,7 @@ by convention in each plugin's `setup()` method.
|
||||
```
|
||||
2. Create `~/.pyra/plugins/<name>/plugin.py` exporting `get_plugin() -> BasePlugin`:
|
||||
```python
|
||||
from pyra.plugins.base import BasePlugin, Tool
|
||||
from pyra.plugins.base import BasePlugin, ConfigField, Tool
|
||||
|
||||
class MyPlugin(BasePlugin):
|
||||
name = "<name>"
|
||||
@@ -183,6 +184,15 @@ by convention in each plugin's `setup()` method.
|
||||
secret = console.input("Enter secret: ")
|
||||
vault_writer("plugin:<name>:secret", secret)
|
||||
|
||||
def config_fields(self):
|
||||
# Declare user-adjustable settings. Values are saved to config.yaml
|
||||
# under plugin_settings["<name>"] and rendered in /config → plugin tab.
|
||||
return [
|
||||
ConfigField("api_url", "API URL", "text", "https://example.com",
|
||||
description="Base URL for the service"),
|
||||
ConfigField("verify_ssl", "Verify SSL", "bool", True),
|
||||
]
|
||||
|
||||
def get_plugin():
|
||||
return MyPlugin()
|
||||
```
|
||||
@@ -192,6 +202,10 @@ by convention in each plugin's `setup()` method.
|
||||
- Never import from `pyra.vault` directly — use the `vault_reader`/`vault_writer` callables
|
||||
- All write/destructive tools must set `requires_approval=True`
|
||||
- Return strings from tool handlers (truncated to 4000 chars by executor)
|
||||
- Implement `config_fields()` for any user-adjustable settings beyond credentials.
|
||||
Return a list of `ConfigField` objects — the `/config` TUI renders them automatically
|
||||
and saves values to `config.yaml` under `plugin_settings["<name>"]`.
|
||||
Plugins that need no configuration can omit this method (base no-op is used).
|
||||
|
||||
---
|
||||
|
||||
@@ -322,6 +336,7 @@ Before writing any new utility function, class, or import block, check the **Cod
|
||||
| `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 |
|
||||
| `textual` | 1.0.0 | `config/tui.py` | Full-screen TUI framework — tabs, inputs, switches, data tables for `/config` |
|
||||
|
||||
Optional plugin extras (declared in `pyproject.toml [project.optional-dependencies]`):
|
||||
|
||||
@@ -388,6 +403,13 @@ Dataclass: `InjectionWarning(pattern_label: str, matched_text: str)`
|
||||
| `config_exists` | `() -> bool` | True if `config.yaml` exists |
|
||||
| `config_path` | `() -> Path` | Absolute path to `config.yaml` |
|
||||
|
||||
#### `config.tui`
|
||||
|
||||
| Symbol | Purpose |
|
||||
|--------|---------|
|
||||
| `launch_config_tui` | `() -> None` — opens the full-screen configuration TUI; blocks until user presses `q`/Escape |
|
||||
| `GENERAL_FIELDS` | List of `_CoreField` entries — the single place to add new core settings to the General tab |
|
||||
|
||||
#### `config.dirs`
|
||||
|
||||
| Function | Signature | Purpose |
|
||||
@@ -480,7 +502,8 @@ Import `console` from here; do not create a second `rich.Console()` in new code.
|
||||
|
||||
| Class | Module | Notes |
|
||||
|-------|--------|-------|
|
||||
| `PyraConfig` | `config.schema` | Top-level config; fields: `ai`, `memory`, `security`, `plugins`, `daemon` |
|
||||
| `PyraConfig` | `config.schema` | Top-level config; fields: `ai`, `general`, `memory`, `security`, `plugins`, `daemon`, `plugin_settings` |
|
||||
| `GeneralConfig` | `config.schema` | `general:` block — `user_name`, `assistant_name` |
|
||||
| `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 |
|
||||
@@ -489,6 +512,7 @@ Import `console` from here; do not create a second `rich.Console()` in new code.
|
||||
| `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()` |
|
||||
| `ConfigField` | `plugins.base` | Dataclass — declares one plugin config option (`key`, `label`, `type`, `default`, `options`, `description`); returned by `config_fields()` |
|
||||
| `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 |
|
||||
|
||||
Reference in New Issue
Block a user