- plan_and_execute: restrict to 3+ step tasks; prevents over-triggering on simple requests
- memory_read: hint to call memory_lookup first to find the correct path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds daemon/events.py — a lightweight asyncio.Queue-based publish/subscribe
bus that lets daemon tasks communicate without direct imports between plugins.
Email plugin publishes new_email events; messaging bots consume via
subscribe_forever(). Also adds email optional-dependency group to pyproject.toml
(imap-tools, google-api-python-client, google-auth-oauthlib, O365).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PluginSupervisor.reload(): cancels all running plugin tasks, resets
restart counters, and re-creates them with fresh coroutines
- IPC reload command now calls supervisor.reload() instead of being a stub
- run_foreground(): wrap PID file acquisition in try/except PidFileError
to produce a clean error if two daemon starts race on the PID file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The initial daemon_tasks() call to count tasks created coroutines that
were immediately discarded, triggering RuntimeWarning "coroutine never
awaited". Explicitly close them after counting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_install_signal_handlers() was called before asyncio.run(), registering
handlers on a throwaway loop that asyncio.get_event_loop() created — so
SIGTERM would never reach the supervisor. Move the call into _run_daemon()
and switch to asyncio.get_running_loop() so handlers are registered on the
actual running loop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
start/run/stop/status/restart/install/uninstall now call the real daemon
modules instead of printing stub messages. Includes a Rich status table
for `pyra daemon status` and friendly error messages when config is missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- core.py: asyncio event loop entry point, PluginSupervisor with per-task
restart (up to 10 times, 5s back-off), IPC dispatch, signal handling
(SIGTERM/SIGHUP on POSIX), RotatingFileHandler, start_background() helper
- daemon/__init__.py: export public API
- plugins/registry.py: add get_daemon_task_factories() so supervisor can
restart crashed tasks by re-calling plugin.daemon_tasks()[i]
- config/schema.py: add DaemonConfig.ipc_port for Windows TCP loopback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Newline-delimited JSON over Unix socket (macOS/Linux, chmod 600, UID-checked
via SO_PEERCRED/getpeereid) with TCP loopback fallback on Windows. Port written
to ~/.pyra/daemon.port for Windows clients. Sync send_command() wrapper for CLI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generates launchd plist (macOS), systemd user unit (Linux), and Task
Scheduler XML (Windows). Auto-detects platform; finds pyra executable
via shutil.which with venv-sibling fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Atomic write-then-rename, stale-PID detection via os.kill on POSIX and
ctypes.OpenProcess on Windows, context manager for cleanup on exit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Querying the live /api/v0/models endpoint shows LM Studio uses state="loaded"
for in-memory models (not "loaded_instance"), so the filter never matched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LM Studio's /v1/models returns all downloaded models, not just loaded
ones. Use /api/v0/models with state filtering in both fetch_loaded_models()
and _fetch_local_models() so only RAM-resident models are shown as loaded.
This also restores the _choose_model() fallback that offers downloaded-but-
unloaded models when nothing is active in LM Studio.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For Ollama, /api/tags returns all installed models, not running ones.
Add fetch_loaded_models() using /api/ps for Ollama (and /v1/models for
LM Studio/llama.cpp, which already return only loaded models).
_show_local_model_status() now calls fetch_loaded_models() so the
setup wizard correctly shows only in-memory models for Ollama.
At chat session startup, local providers warn when the configured model
is not currently loaded, or when nothing is loaded at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _test_connection() now returns the (possibly changed) model name and
offers a "Change model" option when the error is model-related
- _show_local_model_status() prints which models are currently loaded
immediately after selecting a local provider
- Draft persistence: each completed wizard step is saved to
~/.pyra/setup.draft.json (chmod 600); on the next run a yellow panel
summarises progress and offers [Resume / Start fresh]; draft is
deleted on successful completion or Ctrl-C with no completed steps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _classify_error() that maps litellm/httpx exceptions to human-readable
labels and resolution hints without requiring a top-level litellm import.
_check_local_server() now loops with Retry / Continue anyway / Abort instead
of printing a one-shot warning and silently continuing.
_test_connection() now loops with Retry / Re-enter API key (auth errors only) /
Skip test / Abort instead of printing the raw exception string and falling through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover _fetch_local_models (LM Studio and Ollama parsing, error paths,
missing base_url), _fetch_lmstudio_available_models (happy path and
errors), and _load_lmstudio_model (success, API failure, exception).
All mocked via monkeypatch/MagicMock — no real HTTP calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the static model text prompt with live API queries:
- _fetch_local_models(): queries /v1/models (LM Studio, llama.cpp) or
/api/tags (Ollama) and returns a questionary.select list
- _fetch_lmstudio_available_models(): queries LM Studio's beta
/api/v0/models to list downloaded-but-not-loaded models
- _load_lmstudio_model(): tries /api/v0/models/load to load a model
in-place; falls back to telling the user to load manually
- Cloud providers keep the existing text-input behaviour
Also replace hardcoded LMSTUDIO_MODEL in integration tests with a
lmstudio_model fixture that queries the API at runtime and uses
whichever model is currently loaded (skips if none).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover _USE_CASE_PLUGINS mapping, _suggest_plugins side effects, _build_system_base
output for all name/purpose combinations, and GeneralConfig.purpose round-trip.
Also update CLAUDE.md with the testing workflow rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a personalization step to `pyra setup` that asks for the user's name,
a one-sentence purpose, and interest areas, then surfaces relevant planned
plugins. Store purpose in GeneralConfig and use it in the system prompt so
Pyra stays task-focused rather than acting as a generic chatbot.
- config/schema.py: add `purpose: str = ""` to GeneralConfig
- setup/wizard.py: add _collect_user_profile(), _suggest_plugins(), _USE_CASE_PLUGINS
- chat/history.py: replace hardcoded _SYSTEM_BASE with _build_system_base() using config values
- config/tui.py: expose purpose field in /config General tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Providers that need /v1 (lmstudio, llamacpp, qwen) now declare a
url_suffix field. The AI settings save action appends it automatically
and notifies the user if they entered a URL without it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a local model rejects function calling (BadRequestError), the flag
is set in a session-scoped dict so subsequent messages skip the tool-use
path entirely — no repeated info message on every turn.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the user switches providers in the AI tab, the key Input now shows
"set" or "not set" based on what's actually stored in the vault for that
provider, and clears any in-progress key entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Local models (e.g. Gemma on LM Studio) return HTTP 400 when sent a
tools-spec request. Catch litellm.BadRequestError in the tool-use loop,
inform the user once that tools are disabled, and retry as a plain
streaming call so the conversation continues normally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a dedicated AI tab with provider Select, model Input, base URL Input,
and masked API key Input (write-only, stored in vault). Switching providers
reactively updates the model placeholder, base URL default, and shows/hides
the API key row for cloud vs. local providers. ctrl+s saves config and vault.
Extend GENERAL_FIELDS with Memory, Security, Plugin, and Daemon sections
using a new "section" header type and optional int cast for numeric fields.
_CoreField gains cast: type | None for automatic value coercion on save.
Add 5 new tests covering AI tab rendering, config save, vault key write,
vault key skip-on-empty, and section header rendering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove all Button widgets — saves and plugin toggles are keyboard-only
(ctrl+s, e, d). Replace Header with a plain _TitleBar Static. Apply a
dark monochrome ASCII theme: +---+ borders on inputs, DataTable, and
tab panes; #0d0d0d background; grey/white palette. Disable mouse at the
driver level via run(mouse=False). Update save test to drive via ctrl+s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Header/Footer with visible key hints, ctrl+right/ctrl+left tab navigation,
ctrl+s save bindings for General and plugin config tabs, e/d bindings for
plugin enable/disable in the Plugins tab. Extract shared _do_save() and
_toggle_plugin() helpers so button and key paths share one code path.
Add WordCompleter to the chat REPL so Tab completes slash commands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_GeneralTab and _PluginConfigTab inherited from Widget (height: auto), causing
the inner VerticalScroll to get height: 1fr of an auto-height parent, which
collapsed to 0. Fix: inherit from VerticalScroll directly and remove the inner
wrapper. _PluginsTab gets DEFAULT_CSS to fill its TabPane.
Also replace $text-muted (invalid in Textual 8.x) with $foreground 50%.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Adds a Workflow Rules section mandating worktrees so parallel plugin and
feature sessions never interfere with each other or with main. Includes
setup commands, rules, and updated Plugin Branches section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
conftest patches mdb._DB_PATH and calls init_db() after directory creation
so all existing tests continue to work with the new DB layer. New
test_memory_db.py covers upsert, search, remove, migration, and the
updated list_memories/lookup_memories integration paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- reader: list_memories() queries memory_meta; lookup_memories() uses FTS5 with
fallback to JSON index substring search
- writer: write_memory() and append_memory() upsert to DB after every file write
- dirs: bootstrap() calls init_db() + migrate_from_files() on startup
Existing .md files remain the canonical store; SQLite is the search index.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stages now reflect architectural milestones only (Memory DB → Vault →
Skills → Daemon → Audit → Web UI). Plugins move to a perpetual catalog
with per-plugin git branches. Always-push rule replaces the old
no-push default. Adds Plugin Branches workflow section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gives Pyra an active memory brain: memory_index.json tracks summary +
keywords per file (like an inode table), and three built-in tools let
the AI look up, read, and overwrite memory mid-session. write_memory
accepts summary/keywords; update_index() merges the JSON index without
losing existing metadata.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces TaskPlanner and AgentSpec so Pyra can decompose multi-step
tasks into sequential steps, each executed with a focused sub-agent
context rather than the full conversation history.
- plugins/base.py: AgentSpec dataclass + agent_spec() on Protocol/BasePlugin
- plugins/registry.py: register_builtin, get_agent, list_agents
- chat/planner.py: TaskPlanner with plan approval, per-step tool-use loop,
verification call, and agent-aware routing
- chat/session.py: wires plan_and_execute as a built-in tool after load_all
- chat/history.py: planning hint in system prompt + dynamic agents listing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
load_all() now builds a _tools: dict[str, Tool] index at startup.
get_all_tools() returns list(_tools.values()) and find_tool() is a
direct dict.get() instead of rebuilding the full tool list from every
plugin on every tool call during a session.
Updated test helper to populate _tools alongside _plugins to match
the actual load_all() behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_MEMORY_ROOT was defined independently in reader.py, writer.py, and
index.py. Moved to memory/__init__.py; all three import from there.
Also fixes a bug in append_memory where path.write_text() was called
before path.parent.mkdir(), which would crash when creating a file in
a new subdirectory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>