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>
Directory structure, pyproject.toml with hatchling build, and all
subpackage stubs for pyra Stage 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>