diff --git a/plugins/kaizen/README.md b/plugins/kaizen/README.md index 75fb723a..fe378ae9 100644 --- a/plugins/kaizen/README.md +++ b/plugins/kaizen/README.md @@ -30,7 +30,7 @@ claude --plugin-dir /path/to/kaizen/repo/plugins/kaizen ### Entity Retrieval (Automatic) When you submit a prompt, the plugin automatically: -1. Loads all stored entities from `.kaizen/entities.json` +1. Loads all stored entities from `.kaizen/entities/` (one markdown file per entity) 2. Formats and injects them into the conversation context 3. Claude applies relevant entities to the current task @@ -41,7 +41,7 @@ By default, you must manually invoke the `/kaizen:learn` skill to extract entiti 2. Invoke `/kaizen:learn` 3. The plugin analyzes the conversation trajectory 4. Extracts actionable entities from what worked/failed -5. Saves new entities to `.kaizen/entities.json` +5. Saves new entities as markdown files in `.kaizen/entities/{type}/` ## Example Walkthrough @@ -87,24 +87,33 @@ Manually invoke to export the current conversation as a trajectory JSON file: ## Entities Storage -Entities are stored in `.kaizen/entities.json`: - -```json -{ - "entities": [ - { - "content": "Use Python PIL/Pillow for image metadata extraction in sandboxed environments", - "rationale": "System tools like exiftool may not be available", - "category": "strategy", - "trigger": "When extracting image metadata in containerized environments" - } - ] -} +Entities are stored as individual markdown files in `.kaizen/entities/`, nested by type: + +``` +.kaizen/entities/ + guideline/ + use-python-pil-for-image-metadata-extraction.md + cache-api-responses-locally.md +``` + +Each file uses markdown with YAML frontmatter: + +```markdown +--- +type: guideline +trigger: When extracting image metadata in containerized environments +--- + +Use Python PIL/Pillow for image metadata extraction in sandboxed environments + +## Rationale + +System tools like exiftool may not be available ``` ## Environment Variables -- `KAIZEN_ENTITIES_FILE`: Override the default entities storage location +- `KAIZEN_ENTITIES_DIR`: Override the default entities directory location - `CLAUDE_PROJECT_ROOT`: Set by Claude Code, used to locate project-level entities ## Verification diff --git a/plugins/kaizen/lib/__init__.py b/plugins/kaizen/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/kaizen/lib/entity_io.py b/plugins/kaizen/lib/entity_io.py new file mode 100644 index 00000000..ea1bd6c7 --- /dev/null +++ b/plugins/kaizen/lib/entity_io.py @@ -0,0 +1,290 @@ +"""Shared entity I/O utilities for the Kaizen plugin. + +Handles reading and writing entities as flat markdown files with YAML +frontmatter, organized in type-nested directories. +""" + +import datetime +import getpass +import os +import re +import tempfile +from pathlib import Path + + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- + + +def _get_log_dir(): + """Get user-scoped log directory with restrictive permissions.""" + try: + uid = os.getuid() + except AttributeError: + uid = getpass.getuser() + log_dir = os.path.join(tempfile.gettempdir(), f"kaizen-{uid}") + os.makedirs(log_dir, mode=0o700, exist_ok=True) + return log_dir + + +_LOG_FILE = os.path.join(_get_log_dir(), "kaizen-plugin.log") + + +def log(component, message): + """Append a timestamped message to the shared log file. + + Args: + component: Short label like "retrieve" or "save". + message: The log line. + """ + if not os.environ.get("KAIZEN_DEBUG"): + return + try: + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(_LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[{timestamp}] [{component}] {message}\n") + except OSError: + pass + + +# --------------------------------------------------------------------------- +# Directory discovery +# --------------------------------------------------------------------------- + + +def find_entities_dir(): + """Locate the entities directory. + + Search order: + 1. ``KAIZEN_ENTITIES_DIR`` env var (authoritative, no fallback) + 2. ``{CLAUDE_PROJECT_ROOT}/.kaizen/entities/`` + 3. ``.kaizen/entities/`` (cwd) + + Returns: + Path to the directory if it exists, else ``None``. + """ + env_dir = os.environ.get("KAIZEN_ENTITIES_DIR") + if env_dir: + p = Path(env_dir) + return p if p.is_dir() else None + + project_root = os.environ.get("CLAUDE_PROJECT_ROOT") + candidates = [] + if project_root: + candidates.append(Path(project_root) / ".kaizen" / "entities") + candidates.append(Path(".kaizen") / "entities") + + for c in candidates: + if c.is_dir(): + return c + return None + + +def get_default_entities_dir(): + """Return (and create) the default entities directory. + + Prefers ``{CLAUDE_PROJECT_ROOT}/.kaizen/entities/``, falls back to + ``.kaizen/entities/``. + """ + project_root = os.environ.get("CLAUDE_PROJECT_ROOT", "") + if project_root: + base = Path(project_root) / ".kaizen" / "entities" + else: + base = Path(".kaizen") / "entities" + base.mkdir(parents=True, exist_ok=True) + return base.resolve() + + +# --------------------------------------------------------------------------- +# Slugify / filename helpers +# --------------------------------------------------------------------------- + + +def slugify(text, max_length=60): + """Convert *text* to a filesystem-safe slug. + + >>> slugify("Use temp files for JSON transfer!") + 'use-temp-files-for-json-transfer' + """ + text = text.lower() + text = re.sub(r"[^a-z0-9]+", "-", text) + text = text.strip("-") + # Truncate at max_length, but don't break in the middle of a word + if len(text) > max_length: + text = text[:max_length].rsplit("-", 1)[0] + return text or "entity" + + +def unique_filename(directory, slug): + """Return a Path that doesn't collide with existing files in *directory*. + + Tries ``slug.md``, then ``slug-2.md``, ``slug-3.md``, etc. + """ + directory = Path(directory) + candidate = directory / f"{slug}.md" + if not candidate.exists(): + return candidate + n = 2 + while True: + candidate = directory / f"{slug}-{n}.md" + if not candidate.exists(): + return candidate + n += 1 + + +# --------------------------------------------------------------------------- +# Markdown <-> dict conversion +# --------------------------------------------------------------------------- + +_FRONTMATTER_KEYS = ("type", "trigger") + + +def entity_to_markdown(entity): + """Serialize an entity dict to markdown with YAML frontmatter. + + Args: + entity: dict with keys ``content``, and optionally ``type``, + ``trigger``, ``rationale``. + + Returns: + A string suitable for writing to a ``.md`` file. + """ + lines = ["---"] + for key in _FRONTMATTER_KEYS: + val = entity.get(key) + if val: + lines.append(f"{key}: {val}") + lines.append("---") + lines.append("") + + content = entity.get("content", "") + lines.append(content) + + rationale = entity.get("rationale") + if rationale: + lines.append("") + lines.append("## Rationale") + lines.append("") + lines.append(rationale) + + lines.append("") + return "\n".join(lines) + + +def markdown_to_entity(path): + """Parse a markdown entity file back into a dict. + + Handles YAML frontmatter with simple ``key: value`` lines (no nested + structures, no PyYAML dependency). + + Returns: + dict with ``content``, ``type``, ``trigger``, ``rationale`` keys. + """ + path = Path(path) + text = path.read_text(encoding="utf-8") + + entity = {} + + # Split frontmatter + if text.startswith("---"): + parts = text.split("---", 2) + if len(parts) >= 3: + frontmatter = parts[1].strip() + body = parts[2] + for line in frontmatter.splitlines(): + line = line.strip() + if not line: + continue + key, _, value = line.partition(":") + key = key.strip() + value = value.strip() + if key and value: + entity[key] = value + else: + body = text + else: + body = text + + # Split body into content and rationale + body = body.strip() + m = re.search(r"^## Rationale", body, re.MULTILINE) + if m: + content = body[: m.start()].strip() + rationale = body[m.end() :].strip() + if rationale: + entity["rationale"] = rationale + else: + content = body + + if content: + entity["content"] = content + + return entity + + +# --------------------------------------------------------------------------- +# Bulk load / write +# --------------------------------------------------------------------------- + + +def load_all_entities(entities_dir): + """Glob ``**/*.md`` under *entities_dir* and parse each file. + + Returns: + list of entity dicts. + """ + entities_dir = Path(entities_dir) + entities = [] + for md in sorted(entities_dir.glob("**/*.md")): + try: + entity = markdown_to_entity(md) + if entity.get("content"): + entities.append(entity) + except OSError: + pass + return entities + + +def write_entity_file(directory, entity): + """Write a single entity as a markdown file under *directory*. + + The file is placed in a ``{type}/`` subdirectory. Uses atomic + write (write to ``.tmp``, then ``os.rename``). + + Returns: + Path to the written file. + """ + entity_type = entity.get("type", "general") + if not re.fullmatch(r"[a-z0-9][a-z0-9_-]*", entity_type): + entity_type = "general" + type_dir = Path(directory) / entity_type + type_dir.mkdir(parents=True, exist_ok=True) + + slug = slugify(entity.get("content", "entity")) + content = entity_to_markdown(entity) + + # Write to a unique temp file first (avoids predictable .tmp collisions) + fd, tmp_path = tempfile.mkstemp(dir=type_dir, suffix=".tmp", prefix=slug) + try: + os.write(fd, content.encode("utf-8")) + os.close(fd) + fd = None + + # Atomically claim the target using O_EXCL to detect races + target = unique_filename(type_dir, slug) + try: + claim_fd = os.open(str(target), os.O_CREAT | os.O_EXCL | os.O_WRONLY) + os.close(claim_fd) + except FileExistsError: + # Another writer beat us — re-discover a free name + target = unique_filename(type_dir, slug) + + os.rename(tmp_path, target) + return target + except BaseException: + if fd is not None: + os.close(fd) + if os.path.exists(tmp_path): + os.unlink(tmp_path) + raise diff --git a/plugins/kaizen/skills/learn/SKILL.md b/plugins/kaizen/skills/learn/SKILL.md index 72d27e14..80a753a5 100644 --- a/plugins/kaizen/skills/learn/SKILL.md +++ b/plugins/kaizen/skills/learn/SKILL.md @@ -65,11 +65,6 @@ Follow these principles: - If 3 variations were tried before one worked, the entity should recommend the working variation directly - Eliminate the trial-and-error by encoding the answer -5. **Map error-derived entities to categories:** - - `strategy` — wrong approach was chosen → recommend the right approach from the start - - `recovery` — a fallback chain was needed → start from the approach that worked, only fall back to alternatives if it's unavailable - - `optimization` — effort was wasted on retries/timeouts → eliminate the waste - ### Step 4: Output Entities JSON Output entities in the following JSON format: @@ -80,7 +75,7 @@ Output entities in the following JSON format: { "content": "Proactive entity stating what TO DO", "rationale": "Why this approach works better", - "category": "strategy|recovery|optimization", + "type": "guideline", "trigger": "Situational context when this applies" } ] @@ -111,8 +106,9 @@ python3 ${CLAUDE_PLUGIN_ROOT}/skills/learn/scripts/save_entities.py ``` The script will: -- Find or create the entities file (`.kaizen/entities.json`) -- Merge new entities with existing ones (avoiding duplicates) +- Find or create the entities directory (`.kaizen/entities/`) +- Write each entity as a markdown file in `{type}/` subdirectories +- Deduplicate against existing entities (avoiding duplicates) - Display confirmation with the total count **Example:** @@ -122,7 +118,7 @@ echo '{ { "content": "Use Python PIL/Pillow for image metadata extraction", "rationale": "System tools may not be available in sandboxed environments", - "category": "strategy", + "type": "guideline", "trigger": "When extracting image metadata in containerized environments" } ] @@ -131,19 +127,13 @@ echo '{ **Output:** ```text -Creating new file: /path/to/project/.kaizen/entities.json +Created new entities dir: /path/to/project/.kaizen/entities Added 1 new entity(ies). Total: 1 -Entities stored in: /path/to/project/.kaizen/entities.json +Entities stored in: /path/to/project/.kaizen/entities ``` **Note:** Entities are also automatically saved when a conversation ends via the Stop hook. -## Entity Categories - -- **strategy**: High-level approach or methodology choices -- **recovery**: Handling errors, edge cases, or unexpected situations -- **optimization**: Improving efficiency, performance, or code quality - ## Examples ### Good vs Bad Entities @@ -161,7 +151,7 @@ Entities stored in: /path/to/project/.kaizen/entities.json { "content": "Use Python PIL/Pillow for image metadata extraction in sandboxed environments", "rationale": "System tools like exiftool may not be available; PIL is always installable via pip", - "category": "strategy", + "type": "guideline", "trigger": "When extracting image metadata in containerized or sandboxed environments" } ``` @@ -173,7 +163,7 @@ Entities stored in: /path/to/project/.kaizen/entities.json { "content": "When pushing a new branch, always use 'git push -u origin ' to set upstream tracking", "rationale": "Plain 'git push' fails on new branches without upstream configured; -u sets it in one step", - "category": "optimization", + "type": "guideline", "trigger": "When pushing a newly created git branch for the first time" } ``` @@ -183,7 +173,7 @@ Entities stored in: /path/to/project/.kaizen/entities.json { "content": "Use BeautifulSoup or lxml for HTML content extraction, never regex", "rationale": "Regex cannot reliably handle nested/malformed HTML; a proper parser handles edge cases", - "category": "strategy", + "type": "guideline", "trigger": "When extracting data from HTML documents or web pages" } ``` @@ -193,7 +183,7 @@ Entities stored in: /path/to/project/.kaizen/entities.json { "content": "Install Python packages with pip/uv instead of system package managers in sandboxed environments", "rationale": "apt-get and brew require root/sudo which sandboxed environments block; pip works in user space", - "category": "recovery", + "type": "guideline", "trigger": "When installing dependencies in containerized or sandboxed environments" } ``` diff --git a/plugins/kaizen/skills/learn/scripts/save_entities.py b/plugins/kaizen/skills/learn/scripts/save_entities.py index f702180e..a42cafef 100644 --- a/plugins/kaizen/skills/learn/scripts/save_entities.py +++ b/plugins/kaizen/skills/learn/scripts/save_entities.py @@ -1,111 +1,35 @@ #!/usr/bin/env python3 """ Save Entities Script -Reads entities from stdin and appends them to the entities file. +Reads entities from stdin JSON and writes each as a markdown file +in the entities directory, organized by type. """ -import getpass import json -import os import sys from pathlib import Path -import datetime -# Debug logging - use user-scoped directory for security -import tempfile - - -def _get_log_dir(): - """Get user-scoped log directory with restrictive permissions.""" - try: - uid = os.getuid() - except AttributeError: - # Windows doesn't have os.getuid(); fall back to username - uid = getpass.getuser() - log_dir = os.path.join(tempfile.gettempdir(), f"kaizen-{uid}") - os.makedirs(log_dir, mode=0o700, exist_ok=True) - return log_dir - - -LOG_FILE = os.path.join(_get_log_dir(), "kaizen-plugin.log") +# Add lib to path so we can import entity_io +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent.parent / "lib")) +from entity_io import ( + find_entities_dir, + get_default_entities_dir, + load_all_entities, + write_entity_file, + log as _log, +) def log(message): - """Append a timestamped message to the log file.""" - if not os.environ.get("KAIZEN_DEBUG"): - return - timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - with open(LOG_FILE, "a", encoding="utf-8") as f: - f.write(f"[{timestamp}] [save] {message}\n") + _log("save", message) log("Script started") -def find_entities_file(): - """Find existing entities file, checking multiple locations.""" - # If KAIZEN_ENTITIES_FILE is explicitly set, honor it even if the file doesn't exist yet - env_val = os.environ.get("KAIZEN_ENTITIES_FILE") - if env_val: - return Path(env_val).resolve() - - # Fall back to checking other candidate locations - project_root = os.environ.get("CLAUDE_PROJECT_ROOT") - locations = [ - # Current working directory - ".kaizen/entities.json", - # Plugin-relative path (fallback) - str(Path(__file__).parent.parent / "entities.json"), - ] - if project_root: - # Project root from Claude Code (prepend so it's checked first) - locations.insert(0, os.path.join(project_root, ".kaizen/entities.json")) - - for loc in locations: - if loc and Path(loc).exists(): - return Path(loc).resolve() - - return None - - -def get_default_entities_path(): - """Get default path for new entities file.""" - # Prefer project root if available - project_root = os.environ.get("CLAUDE_PROJECT_ROOT", "") - if project_root: - kaizen_dir = Path(project_root) / ".kaizen" - else: - # Fall back to current directory's .kaizen/ - kaizen_dir = Path(".kaizen") - - kaizen_dir.mkdir(parents=True, exist_ok=True) - return (kaizen_dir / "entities.json").resolve() - - -def load_existing_entities(path): - """Load existing entities from file. - - Returns: - list: The entities list on success or if file not found. - None: If the file exists but contains invalid JSON (to prevent data loss). - """ - try: - with open(path, encoding="utf-8") as f: - data = json.load(f) - return data.get("entities", []) - except FileNotFoundError: - return [] - except json.JSONDecodeError as e: - log(f"load_existing_entities: JSON decode error in {path}: {e}") - return None - - -def save_entities(path, entities): - """Save entities to file.""" - path.parent.mkdir(parents=True, exist_ok=True) - with open(path, "w", encoding="utf-8") as f: - json.dump({"entities": entities}, f, indent=2) - f.write("\n") +def normalize(text): + """Normalize content for dedup comparison.""" + return " ".join(text.lower().split()) def main(): @@ -126,44 +50,42 @@ def main(): log(f"Received {len(new_entities)} new entities") - # Find or create entities file - existing_path = find_entities_file() - - if existing_path: - entities_path = existing_path - existing_entities = load_existing_entities(entities_path) - if existing_entities is None: - log(f"Refusing to overwrite corrupted file: {entities_path}") - print(f"Error: {entities_path} contains invalid JSON. Fix or remove the file before adding entities.", file=sys.stderr) - sys.exit(1) - log(f"Found existing file: {entities_path} with {len(existing_entities)} entities") - print(f"Appending to existing file: {entities_path}") + # Find or create entities directory + entities_dir = find_entities_dir() + if entities_dir: + entities_dir = entities_dir.resolve() + log(f"Found existing dir: {entities_dir}") + print(f"Using existing entities dir: {entities_dir}") else: - entities_path = get_default_entities_path() - existing_entities = [] - log(f"Creating new file: {entities_path}") - print(f"Creating new file: {entities_path}") + entities_dir = get_default_entities_dir() + log(f"Created new dir: {entities_dir}") + print(f"Created new entities dir: {entities_dir}") - # Merge entities (avoid duplicates by content) - existing_contents = {e.get("content") for e in existing_entities if e.get("content")} - added_count = 0 + # Load existing entities for dedup + existing_entities = load_all_entities(entities_dir) + existing_contents = {normalize(e["content"]) for e in existing_entities if e.get("content")} + log(f"Existing entities: {len(existing_entities)}") + # Write new entities as markdown files + added_count = 0 for entity in new_entities: content = entity.get("content") if not content: log(f"Skipping entity without content: {entity}") continue - if content not in existing_contents: - existing_entities.append(entity) - existing_contents.add(content) - added_count += 1 + if normalize(content) in existing_contents: + log(f"Skipping duplicate: {content[:60]}") + continue - # Save merged entities - save_entities(entities_path, existing_entities) + path = write_entity_file(entities_dir, entity) + existing_contents.add(normalize(content)) + added_count += 1 + log(f"Wrote: {path}") - log(f"Added {added_count} new entities. Total: {len(existing_entities)}") - print(f"Added {added_count} new entity(ies). Total: {len(existing_entities)}") - print(f"Entities stored in: {entities_path}") + total = len(existing_entities) + added_count + log(f"Added {added_count} new entities. Total: {total}") + print(f"Added {added_count} new entity(ies). Total: {total}") + print(f"Entities stored in: {entities_dir}") if __name__ == "__main__": diff --git a/plugins/kaizen/skills/recall/SKILL.md b/plugins/kaizen/skills/recall/SKILL.md index 139862ea..08c20362 100644 --- a/plugins/kaizen/skills/recall/SKILL.md +++ b/plugins/kaizen/skills/recall/SKILL.md @@ -13,23 +13,32 @@ This skill retrieves relevant entities from a stored knowledge base based on the 1. Hook fires on user prompt submission 2. Script reads prompt from stdin (JSON with `prompt` field) -3. Loads all entities from the entities JSON file +3. Loads all entities from the entities directory (`.kaizen/entities/`) 4. Outputs formatted entities to stdout 5. Claude receives entities as additional context and applies relevant ones ## Entities Storage -Entities are stored in `.kaizen/entities.json` in the project root: - -```json -{ - "entities": [ - { - "content": "Use context managers for file operations", - "rationale": "Ensures proper resource cleanup", - "category": "strategy", - "trigger": "When processing files or managing resources" - } - ] -} +Entities are stored as individual markdown files in `.kaizen/entities/`, nested by type: + +``` +.kaizen/entities/ + guideline/ + use-context-managers-for-file-operations.md + cache-api-responses-locally.md +``` + +Each file uses markdown with YAML frontmatter: + +```markdown +--- +type: guideline +trigger: When processing files or managing resources +--- + +Use context managers for file operations + +## Rationale + +Ensures proper resource cleanup ``` diff --git a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py index 034dfc95..9e2c2f68 100644 --- a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py +++ b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py @@ -1,38 +1,18 @@ #!/usr/bin/env python3 """Retrieve and output entities for Claude to filter.""" -import getpass import json import os import sys from pathlib import Path -import datetime -import tempfile - -def _get_log_dir(): - """Get user-scoped log directory with restrictive permissions.""" - try: - uid = os.getuid() - except AttributeError: - # Windows doesn't have os.getuid(); fall back to username - uid = getpass.getuser() - log_dir = os.path.join(tempfile.gettempdir(), f"kaizen-{uid}") - os.makedirs(log_dir, mode=0o700, exist_ok=True) - return log_dir - - -# Debug logging - use user-scoped directory for security -LOG_FILE = os.path.join(_get_log_dir(), "kaizen-plugin.log") +# Add lib to path so we can import entity_io +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent.parent / "lib")) +from entity_io import find_entities_dir, load_all_entities, log as _log def log(message): - """Append a timestamped message to the log file.""" - if not os.environ.get("KAIZEN_DEBUG"): - return - timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - with open(LOG_FILE, "a", encoding="utf-8") as f: - f.write(f"[{timestamp}] [retrieve] {message}\n") + _log("retrieve", message) log("Script started") @@ -55,52 +35,6 @@ def log(message): log("=== End Command-Line Arguments ===") -def find_entities_file(): - """Find the entities file in common locations.""" - # If KAIZEN_ENTITIES_FILE env var is set, it is authoritative - no fallbacks - entities_file_env = os.environ.get("KAIZEN_ENTITIES_FILE") - if entities_file_env: - path = Path(entities_file_env) - return path if path.exists() else None - - # Fallback locations when KAIZEN_ENTITIES_FILE is not set - project_root = os.environ.get("CLAUDE_PROJECT_ROOT") - locations = [ - # Current working directory - ".kaizen/entities.json", - # Plugin-relative path (fallback) - str(Path(__file__).parent.parent / "entities.json"), - ] - if project_root: - # Project root from Claude Code (prepend so it's checked first) - locations.insert(0, os.path.join(project_root, ".kaizen/entities.json")) - for loc in locations: - if loc and Path(loc).exists(): - return Path(loc) - return None - - -def load_entities(): - """Load entities from the entities file. - - Returns: - list: The entities list on success or if file not found. - None: If the file exists but contains invalid JSON. - """ - entities_file = find_entities_file() - if not entities_file: - return [] - try: - with open(entities_file, encoding="utf-8") as f: - data = json.load(f) - return data.get("entities", []) - except IOError: - return [] - except json.JSONDecodeError as e: - log(f"load_entities: JSON decode error in {entities_file}: {e}") - return None - - def format_entities(entities): """Format all entities for Claude to review.""" header = """## Entities for this task @@ -113,7 +47,7 @@ def format_entities(entities): content = e.get("content") if not content: continue - item = f"- **[{e.get('category', 'general')}]** {content}" + item = f"- **[{e.get('type', 'general')}]** {content}" if e.get("rationale"): item += f"\n - _Rationale: {e['rationale']}_" if e.get("trigger"): @@ -135,15 +69,15 @@ def main(): log(f"Failed to parse JSON input: {e}") return - # Load all entities - entities_file = find_entities_file() - log(f"Entities file: {entities_file}") + # Load all entities from directory + entities_dir = find_entities_dir() + log(f"Entities dir: {entities_dir}") - entities = load_entities() - if entities is None: - log(f"Failed to load entities due to invalid JSON in {entities_file}") - print(f"Error: {entities_file} contains invalid JSON.", file=sys.stderr) + if not entities_dir: + log("No entities directory found") return + + entities = load_all_entities(entities_dir) if not entities: log("No entities found") return diff --git a/pyproject.toml b/pyproject.toml index 31eb64e1..9da2b03e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,5 +145,6 @@ module = [ "agents.*", "pgvector.*", "psycopg.*", + "entity_io.*", ] ignore_missing_imports = true