personal memory agent
at main 152 lines 4.2 kB view raw
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Utility functions for Convey app storage in journal.""" 5 6import re 7from datetime import datetime 8from pathlib import Path 9from typing import Any 10 11from convey import state 12 13# Compiled pattern for app name validation 14APP_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$") 15 16 17def get_app_storage_path( 18 app_name: str, 19 *sub_dirs: str, 20 ensure_exists: bool = True, 21) -> Path: 22 """ 23 Get path to app storage directory in journal. 24 25 Args: 26 app_name: App name (must match [a-z][a-z0-9_]*) 27 *sub_dirs: Optional subdirectory components 28 ensure_exists: Create directory if it doesn't exist (default: True) 29 30 Returns: 31 Path to <journal>/apps/<app_name>/<sub_dirs>/ 32 33 Raises: 34 ValueError: If app_name contains invalid characters 35 36 Examples: 37 get_app_storage_path("search") # → Path("<journal>/apps/search") 38 get_app_storage_path("search", "cache") # → Path("<journal>/apps/search/cache") 39 """ 40 # Validate app_name to prevent path traversal 41 if not APP_NAME_PATTERN.match(app_name): 42 raise ValueError(f"Invalid app name: {app_name}") 43 44 # Build path 45 path = Path(state.journal_root) / "apps" / app_name 46 for sub_dir in sub_dirs: 47 path = path / sub_dir 48 49 if ensure_exists: 50 path.mkdir(parents=True, exist_ok=True) 51 52 return path 53 54 55def load_app_config( 56 app_name: str, 57 default: dict[str, Any] | None = None, 58) -> dict[str, Any] | None: 59 """ 60 Load app configuration from <journal>/apps/<app_name>/config.json. 61 62 Args: 63 app_name: App name 64 default: Default value if config doesn't exist (default: None) 65 66 Returns: 67 Loaded JSON dict or default value if file doesn't exist 68 69 Examples: 70 config = load_app_config("my_app") # Returns None if missing 71 config = load_app_config("my_app", {}) # Returns {} if missing 72 """ 73 from convey.utils import load_json 74 75 storage_path = get_app_storage_path(app_name, ensure_exists=False) 76 config_path = storage_path / "config.json" 77 return load_json(config_path) or default 78 79 80def save_app_config( 81 app_name: str, 82 config: dict[str, Any], 83) -> bool: 84 """ 85 Save app configuration to <journal>/apps/<app_name>/config.json. 86 87 Args: 88 app_name: App name 89 config: Configuration dict to save 90 91 Returns: 92 True if successful, False otherwise 93 """ 94 from convey.utils import save_json 95 96 storage_path = get_app_storage_path(app_name, ensure_exists=True) 97 config_path = storage_path / "config.json" 98 return save_json(config_path, config) 99 100 101def log_app_action( 102 app: str, 103 facet: str | None, 104 action: str, 105 params: dict[str, Any], 106 day: str | None = None, 107) -> None: 108 """Log a user-initiated action from a Convey app. 109 110 Creates a JSONL log entry for tracking user actions made through the web UI. 111 112 When facet is provided, writes to facets/{facet}/logs/{day}.jsonl. 113 When facet is None, writes to config/actions/{day}.jsonl for journal-level 114 actions (settings changes, observer management, etc.). 115 116 Args: 117 app: App name where action originated (e.g., "entities", "todos") 118 facet: Facet where action occurred, or None for journal-level actions 119 action: Action type (e.g., "entity_add", "todo_complete") 120 params: Action-specific parameters to record 121 day: Day in YYYYMMDD format (defaults to today) 122 123 Examples: 124 # Facet-scoped action 125 log_app_action( 126 app="entities", 127 facet="work", 128 action="entity_add", 129 params={"type": "Person", "name": "Alice"}, 130 ) 131 132 # Journal-level action (no facet) 133 log_app_action( 134 app="observer", 135 facet=None, 136 action="observer_create", 137 params={"name": "laptop"}, 138 ) 139 """ 140 from think.facets import _write_action_log 141 142 if day is None: 143 day = datetime.now().strftime("%Y%m%d") 144 145 _write_action_log( 146 facet=facet, 147 action=action, 148 params=params, 149 source="app", 150 actor=app, 151 day=day, 152 )