67 Commits

Author SHA1 Message Date
curo1305 1cbb40ac93 chore(chat): tighten tool descriptions to reduce AI selection confusion
- 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>
2026-05-19 23:50:21 +02:00
curo1305 8d4917f7ca feat(daemon): add async event bus for inter-plugin notifications
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>
2026-05-19 23:12:17 +02:00
curo1305 bde0856979 feat(daemon): Stage 6 daemon infrastructure
Always-on asyncio daemon with IPC socket, OS service install/uninstall
(launchd/systemd/schtasks), and plugin task supervisor.

- daemon/pid.py: atomic PID file, stale detection (POSIX + Windows)
- daemon/ipc.py: Unix socket (chmod 600, UID-checked) on Linux/macOS;
  TCP loopback + port file on Windows; newline-delimited JSON protocol
- daemon/service.py: launchd plist, systemd user unit, schtasks XML;
  auto-detects platform; finds pyra executable via shutil.which
- daemon/core.py: asyncio event loop, PluginSupervisor (per-task
  restart up to 10x with 5s back-off, reload), IPC command dispatch,
  SIGTERM/SIGHUP signal handling via get_running_loop()
- cli.py: all 7 daemon stubs replaced with real commands
- 376 tests passing (13 new supervisor + IPC handler tests)
2026-05-19 16:14:51 +02:00
curo1305 4744cf819b docs: update CLAUDE.md for Stage 6 daemon infrastructure
- Current Status: add Stage 6 daemon infrastructure in progress
- Architecture table: expand daemon/__init__.py stub to all 5 daemon modules
- Code Inventory: add daemon.core, daemon.pid, daemon.ipc, daemon.service
  sections with function signatures and purposes
- Internal classes: add PluginSupervisor and PidFile; expand DaemonConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:10:49 +02:00
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 db6ca6ee57 feat(daemon): implement reload, fix PID race condition
- 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>
2026-05-19 15:54:56 +02:00
curo1305 cc24257ab0 fix(daemon): close discarded coroutines in get_daemon_task_factories
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>
2026-05-19 15:53:06 +02:00
curo1305 68f9007ef0 fix(daemon): install signal handlers inside running event loop
_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>
2026-05-19 15:52:11 +02:00
curo1305 c41ad0afc6 feat(daemon): wire up all 7 daemon CLI commands
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>
2026-05-19 15:38:50 +02:00
curo1305 3d3ce694b9 feat(daemon): add core asyncio daemon, supervisor, and registry factories
- 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>
2026-05-19 15:33:57 +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 cb390ad6af docs: update README and CLAUDE.md to reflect current state
Add daemon subcommands to README command table (Stage 6 stubs), add
Multi-step Planning section, add chat/planner.py to CLAUDE.md
architecture table, add TaskPlanner to internal classes inventory,
and remove stale test count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:35:44 +02:00
curo1305 01655124b5 Update description 2026-05-19 14:28:37 +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 019e8044a9 feat(setup): model re-entry, status indicator, and resumable setup wizard
- _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>
2026-05-19 13:43:49 +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 9a392410e7 feat(setup): error controller with retry loops in setup wizard
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>
2026-05-19 13:26:58 +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 ace9561c87 feat(setup): personalized setup wizard with purpose and plugin suggestions
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>
2026-05-19 10:43:15 +02:00
curo1305 cfebc3cb1f fix(providers): remove url_suffix from qwen (cloud provider, fixed endpoint)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 00:58:32 +02:00
curo1305 fd6313acd9 feat(tui): auto-correct base URL if required path suffix is missing
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>
2026-05-19 00:56:37 +02:00
curo1305 a523fa61a3 fix(chat): fall back to provider default base_url when config value is blank
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 00:53:11 +02:00
curo1305 1cf7bdf908 fix(chat): show "tools disabled" info message only once per session
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>
2026-05-19 00:18:11 +02:00
curo1305 bf29ffc7d8 fix(tui): refresh API key placeholder when switching providers
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>
2026-05-19 00:10:31 +02:00
curo1305 0b0cd07330 fix(chat): fall back to streaming when provider rejects function calling
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>
2026-05-19 00:03:14 +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 3b89d940de fix(tui): remove border-bottom from _TitleBar so title text renders
height: 1 + border-bottom: ascii left no row for content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:32:11 +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 1412ced7a8 feat(tui): full keyboard support for config TUI and chat slash completion
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>
2026-05-18 23:11:39 +02:00
curo1305 54241a9e4e fix(config): fix empty General tab — height collapse and invalid CSS variable
_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>
2026-05-18 22:15:45 +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 1201606187 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>
2026-05-18 21:28: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 928724ba39 docs: require git worktrees for all branch work
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>
2026-05-18 15:29:58 +02:00
curo1305 800b1e9494 docs: mark Stage 3 complete, update architecture and code inventory
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:28:06 +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 b9b0918d3a feat(memory): wire database into reader, writer, and bootstrap
- 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>
2026-05-18 15:23:49 +02:00
curo1305 45e6ec32ec feat(memory): add SQLite+FTS5 database layer
New memory/database.py with memory_meta table (path, category, size_bytes,
modified, summary, keywords, embedding BLOB reserved for Stage 8) and
memory_fts virtual table for full-text search. Public API: init_db, upsert,
remove, search, list_all, migrate_from_files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:23:43 +02:00
curo1305 84785967c3 docs: restructure roadmap and add plugin branch workflow rules
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>
2026-05-18 15:02:58 +02:00
curo1305 e56e9779ec feat(memory): add JSON index and runtime memory_lookup/read/write tools
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>
2026-05-17 23:01:54 +02:00
curo1305 ad024807bc feat(chat): add agent orchestration system with plan_and_execute
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>
2026-05-17 21:03:42 +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 bbe9bcfe0a refactor(memory): centralize _MEMORY_ROOT; fix mkdir order in append_memory
_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>
2026-05-17 18:09:45 +02:00