22 Commits

Author SHA1 Message Date
curo1305 1d5d0387d9 test(daemon): add supervisor and IPC handler tests
13 async tests covering: supervisor lifecycle (start/stop), task
completion, crash-and-restart, max-restart enforcement, status shape,
reload (task restart + counter reset), and IPC handler dispatch for all
4 commands plus unknown commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:57:49 +02:00
curo1305 d42b8b4a47 feat(daemon): add IPC transport module
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>
2026-05-19 15:26:25 +02:00
curo1305 513871ef96 feat(daemon): add OS service install/uninstall module
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>
2026-05-19 15:23:11 +02:00
curo1305 eaed52006f feat(daemon): add PID file management module
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>
2026-05-19 15:22:04 +02:00
curo1305 0e052c4992 fix(setup): correct LM Studio loaded state value to "loaded" not "loaded_instance"
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>
2026-05-19 15:00:01 +02:00
curo1305 40aa934431 fix(setup): filter LM Studio models by state == "loaded_instance"
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>
2026-05-19 14:54:06 +02:00
curo1305 833d1445f0 feat(setup,chat): detect actually-loaded local model via provider-specific API
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>
2026-05-19 14:46:54 +02:00
curo1305 b3851a2715 test: add tests for draft persistence, model status, and model re-entry
10 new tests covering:
- _save_draft / _load_draft / _delete_draft / _mark_step_done helpers
- draft file permissions (chmod 600)
- _show_local_model_status with zero, one, and multiple loaded models
- _test_connection change_model path (model error → change → retry succeeds)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:43:53 +02:00
curo1305 efc589cc56 test: add tests for _classify_error and _check_local_server retry behaviour
_classify_error: covers all litellm error types (auth, not-found, rate-limit,
service-unavailable, connection, timeout, bad-request), httpx connect and
timeout errors, and generic fallback — using dynamically constructed fake
exception classes to avoid importing litellm in tests.

_check_local_server: covers success, retry-then-success, abort (SystemExit),
and continue-anyway paths via monkeypatched httpx and questionary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:27:04 +02:00
curo1305 bafdafea02 test: add unit tests for wizard model-discovery helpers
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>
2026-05-19 10:53:22 +02:00
curo1305 5eb81404c2 feat(setup): dynamic model discovery for local providers in wizard
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>
2026-05-19 10:53:15 +02:00
curo1305 9735a5559e test: add tests for setup wizard personalization and system prompt builder
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>
2026-05-19 10:43:20 +02:00
curo1305 f1213e28c8 feat(tui): AI provider tab + expanded General settings
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>
2026-05-18 23:43:36 +02:00
curo1305 ee6c32b035 feat(tui): keyboard-only ASCII redesign of config TUI
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>
2026-05-18 23:28:59 +02:00
curo1305 51029d4a2d test: add coverage for config TUI, ConfigField, schema changes, and CLI auto-setup
- test_config.py: GeneralConfig defaults, plugin_settings round-trip
- test_config_field.py: ConfigField dataclass, BasePlugin.config_fields() no-op,
  plugin subclass override
- test_config_tui.py: _get/_set_nested, _fid/_pfid helpers, GENERAL_FIELDS validity,
  ConfigApp general tab rendering, save handler, plugins table, plugin tab visibility,
  q key exit — using Textual run_test() + Pilot
- test_cli.py: auto-setup wizard on first run, skip wizard when config exists,
  /config in _STATIC_COMMANDS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:53:19 +02:00
curo1305 6bb7c77692 test: add comprehensive coverage for cli, chat, renderer, dirs, install, paths
56 new tests covering previously untested modules:
- test_cli.py: memory write/read/append/list + plugin enable/disable + daemon stubs (via CliRunner)
- test_chat_history.py: ConversationHistory build_for_api, add_*/clear, _trim_to_budget
- test_chat_renderer.py: render_text_response return values, void render_* functions
- test_config_dirs.py: bootstrap idempotency, directory/template/vault/db creation
- test_plugin_install.py: list_bundled_plugins, read_manifest, install_bundled_plugin
- test_utils_paths.py: ensure_dir (nested, idempotent), safe_chmod

Total: 171 → 227 passing tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:16:25 +02:00
curo1305 399ed8b5df test: add memory database tests and update conftest for DB isolation
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>
2026-05-18 15:23:57 +02:00
curo1305 72dae1e048 perf(plugins): cache tool index in PluginRegistry for O(1) find_tool
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>
2026-05-17 18:09:51 +02:00
curo1305 c0c0156468 feat(plugins): Stage 2.1 — plugin framework and AI tool-use
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>
2026-05-17 15:35:20 +02:00
curo1305 6e138bcec2 fix: remove self-defeating assert_safe_path from vault modules, clarify traversal test scope
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>
2026-05-17 12:59:59 +02:00
curo1305 251e509ee0 test: comprehensive test suite
Unit tests:
- test_security_boundaries.py: vault block, vault lock sentinel
- test_security_injection.py: all 4 injection categories, case-insensitive
- test_vault_rw.py: roundtrip, file permissions (chmod 400), no key in config
- test_config.py: schema roundtrip, no api_key field, chmod 600 on config.yaml
- test_memory_reader.py: list, read, sandboxing, context loading
- test_memory_writer.py: write, append, index update, traversal blocked, chmod 600
- test_providers.py: required fields, unique IDs, litellm prefix format
- test_renderer.py: key redaction for sk-ant-, sk-, AIza patterns

Security tests:
- test_vault_ai_isolation.py: 7 traversal patterns blocked via memory read/write
- test_path_traversal.py: 20+ traversal patterns — all rejected for read and write
- test_prompt_injection.py: 21-item corpus + 5 clean texts (no false positives)

Integration tests:
- test_lmstudio.py: live call to localhost:1234, streaming, full stack session,
  injection scan on real output (skips if LM Studio not running)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:55:06 +02:00
curo1305 0a04e04490 chore: init project skeleton
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>
2026-05-17 12:48:32 +02:00