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>
reader.py and writer.py each independently computed the same path via
pyra_home(). Single definition in __init__.py; both modules import it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
expand() had zero callers anywhere in the codebase. The skills/bash,
skills/powershell, and skills/python directories were created on every
startup but the skills/ tree is not part of the current architecture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Introduces a standalone plugin system where every integration lives as
an independent Python script in ~/.pyra/plugins/, not hardcoded in core.
Plugin framework (src/pyra/plugins/):
- base.py: Tool dataclass, PyraPlugin Protocol, BasePlugin helper
- loader.py: importlib-based discovery; one bad plugin never crashes pyra
- registry.py: singleton aggregating tools, slash commands, system prompts
- executor.py: approval gate — scans args, prompts y/N, scans result, logs
- install.py: copies bundled_plugins/ to ~/.pyra/plugins/ on install
Chat integration:
- AI tool-use loop (litellm function calling, up to 10 iterations)
- Plugin system prompt additions injected per session
- Plugin slash commands merged with static commands
CLI additions:
- pyra plugin list/install/enable/disable/setup
- pyra daemon start/stop/status/restart/install/uninstall (stubs for 2.4)
Config: PluginConfig + DaemonConfig added to PyraConfig (backwards-compatible)
Bootstrap: ~/.pyra/plugins/ and ~/.pyra/logs/ created on startup
Security: tool args and results always injection-scanned; plugin dirs
validated with assert_safe_path() before loading (symlink protection)
Tests: 37 new tests (loader, registry, executor, plugin isolation security)
161 total, all passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
litellm requires the api_key field even for local OpenAI-compatible
servers (LM Studio, llama.cpp). Use "local" as a sentinel value for
providers that don't require a real key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vault/reader.py, vault/writer.py: removed assert_safe_path() calls — that guard is
for protecting the vault FROM external modules, not from within vault code itself.
Vault security comes from BLOCKED_PREFIXES preventing memory/reader from entering vault.
test_path_traversal.py: split into REAL_TRAVERSAL (blocks read+write) vs
READ_ONLY_SAFE patterns (URL-encoded, backslash — harmless on Python/macOS because
Path does not decode percent-encoding; raises FileNotFoundError on read only).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- memory/index.py: auto-regenerate MEMORY_INDEX.md on every write
- memory/reader.py: list_memories(), read_memory(), load_context_for_session()
all go through assert_safe_path() + relative_to check
- memory/writer.py: write_memory(), append_memory() — relative names only,
no absolute paths or traversal, calls update_index() after every write
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- vault/reader.py: get_key() reads from ~/.pyra/vault/secrets/api_keys.json
- vault/writer.py: set_key(), delete_key() — only writer callable from setup
- Both call assert_safe_path() as defense-in-depth
- Keys file stays chmod 400; temporarily 600 during write then locked again
- Config.yaml never touched by either module
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>