diff --git a/# Docker.md b/# Docker.md deleted file mode 100644 index f4bb861f..00000000 --- a/# Docker.md +++ /dev/null @@ -1,55 +0,0 @@ -# Docker - - -Docker stores images at /var/lib/docker/ (managed by the Docker daemon). You don't interact with it directly — Docker - handles the layers. - - Your cheat sheet: - - ┌────────────────────────────┬───────────────────────┐ - │ Command │ What it does │ - ├────────────────────────────┼───────────────────────┤ - │ docker start aipass-vscode │ Resume container │ - ├────────────────────────────┼───────────────────────┤ - │ docker stop aipass-vscode │ Pause container │ - ├────────────────────────────┼───────────────────────┤ - │ docker images │ List all images │ - ├────────────────────────────┼───────────────────────┤ - │ docker ps -a │ List all containers │ - ├────────────────────────────┼───────────────────────┤ - │ docker system df │ Disk usage │ - ├────────────────────────────┼───────────────────────┤ - │ docker system prune │ Clean up unused stuff │ - └────────────────────────────┴───────────────────────┘ - - The aipass-test image (~260MB) stays until you docker rmi aipass-test. The container stays until you docker rm it. - Both survive reboots. - - -## Git Workflow (inside the container) - -The container is a separate user. It clones from your fork, not the main repo. - - ┌─────────────────────────────────┬──────────────────────────────────────────┐ - │ Command │ What it does │ - ├─────────────────────────────────┼──────────────────────────────────────────┤ - │ git pull upstream main │ Pull latest from AIOSAI/AIPass (source) │ - ├─────────────────────────────────┼──────────────────────────────────────────┤ - │ git push origin main │ Push to your fork (Input-X/AIPass) │ - ├─────────────────────────────────┼──────────────────────────────────────────┤ - │ git remote -v │ Check which remotes are set │ - ├─────────────────────────────────┼──────────────────────────────────────────┤ - │ git log --oneline -5 │ See recent commits │ - └─────────────────────────────────┴──────────────────────────────────────────┘ - - Remotes: - origin = Input-X/AIPass (your fork — push here) - upstream = AIOSAI/AIPass (main repo — pull from here) - - Typical flow: - 1. git pull upstream main # Get latest changes - 2. Work on your code - 3. git add . && git commit -m "what you did" - 4. git push origin main # Push to your fork - - Nothing you do in the container touches AIOSAI/AIPass directly. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 450f87f8..e162c22d 100644 --- a/.gitignore +++ b/.gitignore @@ -60,8 +60,10 @@ aipass_json/ .standards/ # AIPASS_REGISTRY.json — NOT ignored. Ships with core modules pre-registered. +# Runtime log files (created by prax logging) +**/logs + +# Claude local settings (per-branch, not source code) +.claude/ setup-workspace.sh - -# Local cheat sheets (not source code) -# Docker.md diff --git a/DPLAN-047_path_home_purge_portability_2026-03-06.md b/DPLAN-047_path_home_purge_portability_2026-03-06.md deleted file mode 100644 index d14a32b9..00000000 --- a/DPLAN-047_path_home_purge_portability_2026-03-06.md +++ /dev/null @@ -1,168 +0,0 @@ -# DPLAN-047: Path.home() Purge — AIPass Portability Fix - -**Status:** READY -**Created:** 2026-03-06 -**Priority:** CRITICAL — blocks all Docker/container testing -**Parent:** MPLAN-001 (AIPass Public Repo Build) -**Triggered by:** Docker container testing revealed `Permission denied: '/home/aipass'` on `drone @seedgo audit aipass` - ---- - -## Problem - -Every module in AIPass uses `Path.home()` or hardcoded `/home/aipass` strings to resolve paths at module level (import time). This means: - -- **0 of 10 modules** are portable -- **107 module-level bindings** crash on any machine that isn't ours -- A pip user running `from aipass.prax import logger` gets `Permission denied` immediately -- Docker container (the acceptance test) fails on basic commands - -### Root Cause - -Code was transferred from Dev-Pass (where `/home/aipass` is the fixed root) to AIPass (pip package, root is wherever the user cloned it). The import rewire (FPLAN-0409) fixed `from` statements but didn't touch path resolution. - -### The Fix Pattern - -```python -# WRONG — Dev-Pass pattern (breaks everywhere else) -SYSTEM_LOGS_DIR = Path.home() / "system_logs" -BRANCH_REGISTRY = Path.home() / "BRANCH_REGISTRY.json" -API_ROOT = Path.home() / "aipass_core" / "api" - -# RIGHT — Package-relative (works everywhere) -# Option A: Walk up from __file__ to find repo root -REPO_ROOT = Path(__file__).resolve().parents[N] # N = depth to repo root - -# Option B: Walk-up finder (already used by drone/seedgo) -def _find_repo_root() -> Path: - current = Path(__file__).resolve().parent - for parent in [current] + list(current.parents): - if (parent / "AIPASS_REGISTRY.json").exists(): - return parent - return Path.cwd() -``` - ---- - -## Scope — Module Debt Count - -| Module | Path.home() | Hardcoded /home/aipass | Module-Level | Severity | -|--------|-------------|----------------------|--------------|----------| -| api | 40 | 20 | 33 | CRITICAL | -| ai_mail | 35 | 20 | 22 | CRITICAL | -| prax | 16 | 23 | 9 | CRITICAL | -| flow | 15 | 26 | 6 | HIGH | -| trigger | 8 | 14 | 8 | MEDIUM | -| seedgo | 10 | 66* | 0 | DONE (seed purged) | -| drone | 2 | 0 | 1 | LOW (fallback only) | -| cli | 2 | 11 | 2 | LOW | -| spawn | 2 | 0 | 2 | LOW | -| devpulse | 1 | 0 | 1 | LOW | - -*seedgo 66 count is 59 documentation strings + 7 code. Seed already fixed all runtime breakers. - ---- - -## Phase Plan - -### Phase 0: Immediate Unblock (DEV_CENTRAL — done) -- [x] AIPASS_REGISTRY.json DEVPULSE absolute path fixed -- [x] README.md fake import paths fixed -- [x] pyproject.toml missing watchdog dependency added -- [x] Seedgo Path.home() purge dispatched to @seed (12 files, AST-verified clean) -- [x] Prax setup.py:125 hardcoded `Path("/home/aipass")` fixed -- [ ] Commit + push all pending changes - -### Phase 1: Prax Logger Unblock (dispatch to @prax) -**Goal:** `from aipass.prax import logger` works in container -**Files:** 8 module-level bindings in prax -**Key targets:** -- config/load.py:51 — SYSTEM_LOGS_DIR = Path.home() -- config/load.py:55 — mkdir at import time -- setup.py:125 — branch_logs_dir (FIXED) -- introspection.py:103 — parts.index('aipass') username assumption -- registry/reader.py:34 — BRANCH_REGISTRY_PATH -- dashboard/agent_status_writer.py:45 — BRANCH_REGISTRY -- monitoring/telegram_command_bot.py:68 — AIPASS_HOME cascade -- monitoring/telegram_relay.py:60 — PRAX_MONITOR_CONFIG -- log_watchdog.py:44 — SYSTEM_LOGS_DIR duplicate - -**Fix pattern:** Lazy initialization. Don't mkdir at import time. Use repo-relative paths. - -### Phase 2: Core Module Purge (parallel dispatch) -**Goal:** drone, cli, spawn, devpulse portable -**These are small** — 1-2 fixes each. Can dispatch in parallel. -- drone config.py:41 — fallback Path.home() (already has walk-up, just remove fallback) -- cli json_handler.py:27 — CLI_ROOT -- spawn verify_branch.py — AIPASS_ROOT -- devpulse verify_branch.py — AIPASS_ROOT - -### Phase 3: Heavy Modules (parallel dispatch, larger scope) -**Goal:** flow, trigger, ai_mail, api portable -**These are big** — 6-33 module-level bindings each. - -Strategy: Many of these paths reference Dev-Pass infrastructure that doesn't exist in AIPass (MEMORY_BANK, AI_CENTRAL, BRANCH_REGISTRY.json, telegram bots, daemon). Two options per reference: -1. **Rewire to repo-relative** — if the feature exists in AIPass -2. **Guard with try/except or conditional** — if it's a Dev-Pass-only feature -3. **Remove entirely** — if the code shouldn't be in the public repo at all - -**Decision needed from Patrick:** Which modules should ship functional vs stub in v1.0? -- api telegram handlers — probably Dev-Pass only (not for pip users) -- ai_mail daemon/dispatch — probably Dev-Pass only -- flow AI_CENTRAL aggregation — probably Dev-Pass only -- trigger event handlers — probably Dev-Pass only - -### Phase 4: Docker Verification -**Goal:** Full test suite passes in container -- `pip install -e .` clean -- `drone --help` works -- `drone systems` works -- `drone @seedgo audit aipass` works -- `drone @seedgo verify` works -- `drone @seedgo list` works -- Python imports work: `from aipass.drone import ...`, `from aipass.prax import logger` - ---- - -## Key Decision: What Ships in v1.0? - -The massive path debt in api (40), ai_mail (35), flow (15) is because these modules contain Dev-Pass infrastructure code (telegram bots, daemon dispatch, AI_CENTRAL aggregation) that pip users will never use. - -**Option A: Fix everything** — rewire all 131 Path.home() calls. Massive effort, most code is dead weight for pip users. - -**Option B: Strip Dev-Pass code from public modules** — ship only the pip-relevant parts. Faster, cleaner, but requires deciding what's public vs private per module. - -**Option C: Guard at module boundary** — keep all code but wrap Dev-Pass features in `try/import` guards so they don't crash when infrastructure is missing. Middle ground. - -**Recommendation:** Option B for v1.0. Ship lean. The telegram bridge, daemon dispatch, and AI_CENTRAL aggregation are Dev-Pass features. Public users need: drone routing, seedgo auditing, prax logging (basic), flow planning (basic), cli formatting. - ---- - -## Container Test Checklist (Acceptance Criteria) - -```bash -# In Docker container (clean clone, pip install -e .) -source .venv/bin/activate -pip install -e . - -# Core commands -drone --help # Must work -drone systems # Must show 10 branches -drone @seedgo verify # Must pass 5/5 -drone @seedgo list # Must show packs -drone @seedgo audit aipass # Must run without Permission denied - -# Python imports -python3 -c "from aipass.drone.apps.modules.registry import load_registry; print(load_registry())" -python3 -c "from aipass.prax import logger; logger.info('test')" -python3 -c "from aipass.cli import console, header; header('test')" -``` - ---- - -## Notes - -- Shebangs (`#!/home/aipass/.venv/bin/python3`) are cosmetic — don't affect pip imports -- Display strings in seedgo standards checkers are documentation, not runtime -- The walk-up `_find_registry()` pattern is proven — drone and seedgo already use it -- Dev-Pass and AIPass can coexist on same machine (different venvs, different registries) diff --git a/Dockerfile b/Dockerfile index 9386efd6..9dc26551 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,4 +34,4 @@ ENV PATH="/opt/venv/bin:$PATH" EXPOSE 8080 # Use the official entrypoint (already in the base image at /usr/bin/entrypoint.sh) # It runs fixuid -> entrypoint.d scripts -> dumb-init code-server -ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "--auth", "none", "/home/coder/workspace"] +ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "--auth", "none", "/home/coder/workspace/AIPass"] diff --git a/ai_mail/logs/ai_mail.log b/ai_mail/logs/ai_mail.log deleted file mode 100644 index b01f0233..00000000 --- a/ai_mail/logs/ai_mail.log +++ /dev/null @@ -1,35 +0,0 @@ -2026-03-06 19:26:01 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 19:26:01 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 19:26:01 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 19:26:01 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 19:26:01 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 19:26:27 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 19:26:27 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 19:26:27 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 19:26:27 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 19:26:27 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:32:14 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:32:14 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:32:14 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:32:14 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:32:14 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:32:37 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:32:37 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:32:37 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:32:37 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:32:37 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:32:42 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:32:42 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:32:42 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:32:42 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:32:42 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:33:19 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:33:19 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:33:19 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:33:19 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:33:19 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:33:19 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:33:19 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:33:19 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:33:19 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:33:19 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules diff --git a/ai_mail/logs/dispatch.log b/ai_mail/logs/dispatch.log deleted file mode 100644 index 58f0a13f..00000000 --- a/ai_mail/logs/dispatch.log +++ /dev/null @@ -1 +0,0 @@ -2026-03-06 20:33:19 - captured_dispatch - INFO - [dispatch] Showing dispatch status diff --git a/api/logs/api.log b/api/logs/api.log deleted file mode 100644 index 85218703..00000000 --- a/api/logs/api.log +++ /dev/null @@ -1,29 +0,0 @@ -2026-03-06 19:25:57 - captured_api - INFO - [api] Discovering modules... -2026-03-06 19:25:57 - captured_api - INFO - [+] telegram_service -2026-03-06 19:25:57 - captured_api - INFO - [+] telegram_bot -2026-03-06 19:25:57 - captured_api - INFO - [+] api_key -2026-03-06 19:25:57 - captured_api - ERROR - [-] openrouter_client - import error: name 'OpenAI' is not defined -2026-03-06 19:25:57 - captured_api - INFO - [+] usage_tracker -2026-03-06 19:25:57 - captured_api - INFO - [api] Discovered 4 modules -2026-03-06 19:25:57 - captured_api - WARNING - Unknown command: test -2026-03-06 19:26:31 - captured_api - INFO - [api] Discovering modules... -2026-03-06 19:26:31 - captured_api - INFO - [+] telegram_service -2026-03-06 19:26:31 - captured_api - INFO - [+] telegram_bot -2026-03-06 19:26:31 - captured_api - INFO - [+] api_key -2026-03-06 19:26:31 - captured_api - ERROR - [-] openrouter_client - import error: name 'OpenAI' is not defined -2026-03-06 19:26:31 - captured_api - INFO - [+] usage_tracker -2026-03-06 19:26:31 - captured_api - INFO - [api] Discovered 4 modules -2026-03-06 19:26:38 - captured_api - INFO - [api] Discovering modules... -2026-03-06 19:26:38 - captured_api - INFO - [+] telegram_service -2026-03-06 19:26:38 - captured_api - INFO - [+] telegram_bot -2026-03-06 19:26:38 - captured_api - INFO - [+] api_key -2026-03-06 19:26:38 - captured_api - ERROR - [-] openrouter_client - import error: name 'OpenAI' is not defined -2026-03-06 19:26:38 - captured_api - INFO - [+] usage_tracker -2026-03-06 19:26:38 - captured_api - INFO - [api] Discovered 4 modules -2026-03-06 19:26:41 - captured_api - INFO - [api] Discovering modules... -2026-03-06 19:26:41 - captured_api - INFO - [+] telegram_service -2026-03-06 19:26:41 - captured_api - INFO - [+] telegram_bot -2026-03-06 19:26:41 - captured_api - INFO - [+] api_key -2026-03-06 19:26:41 - captured_api - ERROR - [-] openrouter_client - import error: name 'OpenAI' is not defined -2026-03-06 19:26:41 - captured_api - INFO - [+] usage_tracker -2026-03-06 19:26:41 - captured_api - INFO - [api] Discovered 4 modules diff --git a/flow/logs/aggregate_central.log b/flow/logs/aggregate_central.log deleted file mode 100644 index 16772a69..00000000 --- a/flow/logs/aggregate_central.log +++ /dev/null @@ -1,6 +0,0 @@ -2026-03-06 19:26:45 - captured_aggregate_central - INFO - [aggregate_central] Starting central aggregation -2026-03-06 19:26:45 - captured_aggregate_central - INFO - [aggregate_central] Processing branch: flow -2026-03-06 19:26:45 - captured_aggregate_central - INFO - [aggregate_central] SUCCESS: Aggregation complete: 0 active, 0 recently closed -2026-03-06 20:20:39 - captured_aggregate_central - INFO - [aggregate_central] Starting central aggregation -2026-03-06 20:20:39 - captured_aggregate_central - INFO - [aggregate_central] Processing branch: flow -2026-03-06 20:20:39 - captured_aggregate_central - INFO - [aggregate_central] SUCCESS: Aggregation complete: 1 active, 0 recently closed diff --git a/flow/logs/close_plan.log b/flow/logs/close_plan.log deleted file mode 100644 index f2bdde1a..00000000 --- a/flow/logs/close_plan.log +++ /dev/null @@ -1,4 +0,0 @@ -2026-03-06 19:26:52 - captured_close_plan - INFO - [close_plan] Deleted empty template file: /tmp/FPLAN-0001_test_plan_from_devpulse_2026-03-06.md -2026-03-06 19:26:52 - captured_close_plan - INFO - [close_plan] Removed FPLAN-0001 from registry -2026-03-06 20:21:02 - captured_close_plan - INFO - [close_plan] Deleted empty template file: /home/coder/workspace/src/aipass/flow/FPLAN-0002_2026-03-06.md -2026-03-06 20:21:02 - captured_close_plan - INFO - [close_plan] Removed FPLAN-0002 from registry diff --git a/flow/logs/create_plan.log b/flow/logs/create_plan.log deleted file mode 100644 index 5a9510b1..00000000 --- a/flow/logs/create_plan.log +++ /dev/null @@ -1,2 +0,0 @@ -2026-03-06 19:26:45 - captured_create_plan - INFO - [create_plan] Created FPLAN-0001 in /tmp -2026-03-06 20:20:39 - captured_create_plan - INFO - [create_plan] Created FPLAN-0002 in flow diff --git a/flow/logs/flow.log b/flow/logs/flow.log deleted file mode 100644 index ddf35a1b..00000000 --- a/flow/logs/flow.log +++ /dev/null @@ -1,77 +0,0 @@ -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: create_plan diff --git a/flow/logs/list_plans.log b/flow/logs/list_plans.log deleted file mode 100644 index 3aa9e8f2..00000000 --- a/flow/logs/list_plans.log +++ /dev/null @@ -1,2 +0,0 @@ -2026-03-06 19:25:53 - captured_list_plans - INFO - [list_plans] No plans in registry -2026-03-06 19:26:49 - captured_list_plans - INFO - [list_plans] Listed plans (filter: open) diff --git a/prax/logs/monitor_module.log b/prax/logs/monitor_module.log deleted file mode 100644 index 3194a590..00000000 --- a/prax/logs/monitor_module.log +++ /dev/null @@ -1,2 +0,0 @@ -2026-03-06 20:19:23 - captured_monitor_module - INFO - Starting unified monitoring (args: []) -2026-03-06 20:19:23 - captured_monitor_module - INFO - All monitoring threads started diff --git a/pyproject.toml b/pyproject.toml index b295d55c..c089263a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "aipass" -version = "1.0.0" +version = "2.0.0" description = "Orchestration framework for autonomous AI agent ecosystems" readme = "README.md" license = "MIT" diff --git a/seedgo/logs/seedgo_verify.log b/seedgo/logs/seedgo_verify.log deleted file mode 100644 index 367ee470..00000000 --- a/seedgo/logs/seedgo_verify.log +++ /dev/null @@ -1 +0,0 @@ -2026-03-06 19:24:36 - captured_seedgo_verify - INFO - Seedgo verify: 5/5 (100%) diff --git a/seedgo/logs/standards_checklist.log b/seedgo/logs/standards_checklist.log deleted file mode 100644 index 1cce6cb5..00000000 --- a/seedgo/logs/standards_checklist.log +++ /dev/null @@ -1,78 +0,0 @@ -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Starting standards compliance check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - ERROR - [standards_checklist] Error reading branch registry: name 'BRANCH_REGISTRY_PATH' is not defined -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running IMPORTS standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] IMPORTS check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running ARCHITECTURE standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] ARCHITECTURE check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running NAMING standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] NAMING check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running CLI standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] CLI check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running HANDLERS standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] HANDLERS check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running MODULES standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] MODULES check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running DOCUMENTATION standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] DOCUMENTATION check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running JSON_STRUCTURE standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] JSON_STRUCTURE check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running TESTING standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] TESTING check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running ERROR_HANDLING standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] ERROR_HANDLING check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running ENCAPSULATION standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] ENCAPSULATION check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running TRIGGER standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] TRIGGER check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_LEVEL standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] LOG_LEVEL check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running CLI_FLAGS standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] CLI_FLAGS check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_HANDLER standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] LOG_HANDLER check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_VISIBILITY standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] LOG_VISIBILITY check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running PERMISSION_FLAGS standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] PERMISSION_FLAGS check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running SHEBANG standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] SHEBANG check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Standards check complete: 0% average compliance -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Starting standards compliance check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - ERROR - [standards_checklist] Error reading branch registry: name 'BRANCH_REGISTRY_PATH' is not defined -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running IMPORTS standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] IMPORTS check complete: 80/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running ARCHITECTURE standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] ARCHITECTURE check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running NAMING standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] NAMING check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running CLI standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] CLI check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running HANDLERS standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] HANDLERS check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running MODULES standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] MODULES check complete: 60/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running DOCUMENTATION standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] DOCUMENTATION check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running JSON_STRUCTURE standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] JSON_STRUCTURE check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running TESTING standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] TESTING check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running ERROR_HANDLING standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] ERROR_HANDLING check complete: 66/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running ENCAPSULATION standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] ENCAPSULATION check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running TRIGGER standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] TRIGGER check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_LEVEL standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] LOG_LEVEL check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running CLI_FLAGS standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] CLI_FLAGS check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_HANDLER standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] LOG_HANDLER check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_VISIBILITY standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] LOG_VISIBILITY check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running PERMISSION_FLAGS standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] PERMISSION_FLAGS check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running SHEBANG standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] SHEBANG check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Standards check complete: 94% average compliance diff --git a/src/aipass/ai_mail/.claude/settings.local.json b/src/aipass/ai_mail/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/ai_mail/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/ai_mail/.seed/bypass.json b/src/aipass/ai_mail/.seed/bypass.json index 3c5076e3..6baed8ad 100644 --- a/src/aipass/ai_mail/.seed/bypass.json +++ b/src/aipass/ai_mail/.seed/bypass.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "created": "2026-03-06T03:16:10.842297", + "created": "2026-03-06T14:27:27.887566", "description": "Standards bypass configuration for this branch" }, "bypass": [], diff --git a/src/aipass/ai_mail/README.md b/src/aipass/ai_mail/README.md index 6258dbb9..b552a468 100644 --- a/src/aipass/ai_mail/README.md +++ b/src/aipass/ai_mail/README.md @@ -1,308 +1,85 @@ -# AI_MAIL - Branch Documentation +# ai_mail -## Overview +Inter-agent messaging for AIPass. File-based email system that lets agents send, receive, and process messages using `@branch` addresses. No SMTP, no external services — just JSON files and symbolic routing. -**Location**: `/home/aipass/aipass_core/ai_mail` -**Profile**: Communication Infrastructure -**Purpose**: Branch-to-branch email system providing file-based messaging for AI branches -**Created**: 2025-11-07 - -## What I Do - -AI_MAIL provides internal email system for AI branches within AIPass. Core responsibilities include: - -- Deliver email messages between AI branches -- Manage inbox, sent, and deleted mailboxes for each branch -- Track email lifecycle: new → opened → closed -- Support reply chains with auto-close behavior -- Auto-generate per-branch configs (user_config.json in [branch]_json/) -- Auto-detect calling branch via PWD/CWD for sender identity -- Update dashboard on delivery (DASHBOARD.local.json) -- Dispatch daemon: continuous polling engine spawns agents for `--dispatch` emails -- Single instance per branch locking (PID-based dispatch lock) -- Reply chain validation (sender must match dispatched_to) -- Maintain 100% CLI standards compliance -- Preserve branch email identities (no impersonation) - -## What I Don't Do - -- No SMTP/IMAP protocols -- No external email delivery -- No human email accounts -- No centralized identity (each branch has independent config) -- No manual setup required from branches - -## How I Work - -File-based architecture using JSON for message storage. PWD auto-detection finds calling branch, checks for local config in [branch]_json/, generates config if missing, sends from detected branch identity. - -3-layer handler architecture: -- **Modules**: Orchestrate commands -- **Handlers**: Execute business logic -- **Prax**: Provides logging infrastructure - -## Architecture - -**Pattern**: Modular architecture -**Structure**: apps/ directory with modules/ and handlers/ subdirectories -**Orchestrator**: apps/ai_mail.py - auto-discovers and routes to modules -**Module Interface**: All modules implement handle_command(args) -> bool - -### Directory Structure - -``` -apps/ -├── ai_mail.py # Main orchestrator -├── __init__.py -├── modules/ -│ ├── email.py # Core email functionality -│ ├── branch_ping.py # Branch health monitoring -│ ├── dispatch.py # Dispatch status tracking -│ └── extensions/ # Module extensions -├── handlers/ -│ ├── central_writer.py # Central file updates -│ ├── dispatch/ # Dispatch operations -│ │ ├── daemon.py # Continuous dispatch daemon (polling + spawning) -│ │ ├── pending_work.py # Per-branch pending work tracking -│ │ └── status.py # Dispatch log and status -│ ├── email/ # Email operations -│ │ ├── create.py # Create email messages -│ │ ├── delivery.py # Deliver to branch inboxes (write-only, no spawning) -│ │ ├── format.py # Email formatting -│ │ ├── footer.py # Auto-footer for outgoing emails -│ │ ├── header.py # Email header formatting -│ │ ├── inbox_cleanup.py # Inbox cleanup operations -│ │ ├── inbox_lock.py # Inbox file locking -│ │ ├── inbox_ops.py # Inbox operations -│ │ ├── lock_utils.py # PID-based dispatch lock per branch -│ │ ├── purge.py # Auto-purge sent/deleted when >10 items -│ │ └── reply.py # Reply handling with chain validation -│ ├── json_utils/ # JSON operations -│ │ └── json_handler.py # JSON file operations -│ ├── monitoring/ # System monitoring -│ │ ├── data_ops.py # Data operations -│ │ ├── errors.py # Error handling -│ │ └── memory.py # Memory health checks -│ ├── persistence/ # Data persistence -│ │ └── json_ops.py # JSON persistence ops -│ ├── registry/ # Branch registry -│ │ ├── load.py # Load registry -│ │ ├── read.py # Read registry data -│ │ ├── update.py # Update registry -│ │ └── validate.py # Validate registry -│ ├── trigger/ # Event consumers -│ │ └── error_handler.py # Handle error_detected events -│ └── users/ # User/branch config -│ ├── branch_detection.py # Auto-detect calling branch -│ ├── config_generator.py # Generate branch configs -│ ├── load.py # Load configurations -│ └── user.py # User operations -├── extensions/ # App extensions (placeholder) -├── plugins/ # Plugins (placeholder) -└── json_templates/ # JSON templates - ├── custom/ # Custom templates - ├── default/ # Default templates - └── registry/ # Registry templates -``` - -### Modules - -- `email` - Core email functionality (send, inbox, view, reply, close, sent, contacts) -- `branch_ping` - Branch memory health monitoring -- `dispatch` - Dispatch status tracking, log, and daemon management - -### Handlers (by domain) - -**Dispatch**: daemon, pending_work, status -**Email**: create, delivery, footer, format, header, inbox_cleanup, inbox_lock, inbox_ops, lock_utils, purge, reply -**JSON Utils**: json_handler -**Monitoring**: data_ops, errors, memory -**Persistence**: json_ops -**Registry**: load, read, update, validate -**Trigger**: error_handler -**Users**: branch_detection, config_generator, load, user +**Status:** Building. Core email workflow (send/inbox/reply/close) is functional. Dispatch system is working. ## Usage -Primary commands: `inbox`, `view`, `reply`, `close`, `send`, `sent`, `contacts`, `ping`, `dispatch`. - -### Email Lifecycle v2 - -AI_MAIL uses a 3-state email model: - -**States:** -- `new` - Email just delivered, never viewed -- `opened` - Email content has been viewed, awaiting resolution -- `closed` - Email resolved (replied or dismissed), archived to deleted - -**Flow:** -``` -Email sent → inbox (status: "new") - ↓ - view → status: "opened" - ↓ - ┌──────────┴──────────┐ - ↓ ↓ -reply "msg" close - ↓ ↓ -auto-closes closes - └──────────┬──────────┘ - ↓ - archived to deleted/ -``` - -### Basic Commands +### CLI (via drone) ```bash -# Send email -drone @ai_mail send @recipient "Subject" "Message" +# Send a message +drone @ai_mail send @flow "Bug Report" "Found an issue in plan closing" -# Send with dispatch (auto-execute at recipient) -drone @ai_mail send @recipient "Task" "Details" --dispatch - -# Broadcast to all branches -drone @ai_mail send @all "Announcement" "Message" - -# Check inbox (shows new + opened emails) +# Check inbox drone @ai_mail inbox -# View email content (marks as opened) +# View a message (marks as opened) drone @ai_mail view -# Reply to email (sends reply + auto-closes original) -drone @ai_mail reply "Your reply message" +# Reply (auto-closes original) +drone @ai_mail reply "Fixed in v2.1" -# Close email without replying (archives to deleted) +# Close without reply drone @ai_mail close -# View sent messages -drone @ai_mail sent - -# List contacts -drone @ai_mail contacts +# Send with dispatch flag (recipient auto-executes the task) +drone @ai_mail send @flow "Task" "Details here" --dispatch ``` -### Backward Compatibility +### Python -```bash -# 'read' command now behaves like 'view' -# (marks as opened, does NOT archive) -drone @ai_mail read +```python +from aipass.ai_mail.apps.modules.email import handle_command + +# Module interface — all commands go through handle_command +handle_command(["send", "@flow", "Subject", "Message body"]) +handle_command(["inbox"]) ``` -### Dispatch System (v3.0) +## Email Lifecycle -The `--dispatch` flag marks emails for autonomous execution. The **dispatch daemon** polls inboxes and spawns agents — delivery.py is write-only. +Messages follow a 3-state model: -**Architecture:** -- `delivery.py` writes to inbox + sends desktop notification. No spawning. -- `daemon.py` polls all branch inboxes every 5 min for `--dispatch` emails -- Agents spawned via `claude -c -p` from the branch's CWD (auto-continues most recent session) -- Agents are ephemeral (wake, do work, exit). The daemon is the continuity. -- All safety limits configured in `safety_config.json` - -**Running the daemon:** -```bash -python3 apps/handlers/dispatch/daemon.py # Standalone -ai_mail dispatch daemon # Via module -tmux new-session -d -s dispatch 'python3 apps/handlers/dispatch/daemon.py' +``` +new → opened → closed ``` -**Safety features:** -- **Kill switch**: `touch /home/aipass/.aipass/autonomous_pause` freezes all dispatches -- **Max turns**: 15 per wake (configurable) -- **Max dispatches/branch/day**: 10 (configurable, resets at midnight) -- **Single instance lock**: PID-based `.dispatch.lock` prevents concurrent agents per branch -- **DEV_CENTRAL protection**: @dev_central is never auto-dispatched -- **Notification throttling**: Max 3 desktop notifications per recipient in 30s window -- **Stale lock timeout**: 10 minutes (auto-cleanup of dead locks) +- **new** — Delivered to inbox, never viewed +- **opened** — Viewed by recipient, awaiting action +- **closed** — Replied or dismissed, archived automatically -### Direct Module Access +## Dispatch System -```bash -# Email operations -python3 apps/ai_mail.py email [command] +The `--dispatch` flag marks emails for autonomous execution. A polling daemon watches inboxes and spawns agents to process dispatch emails automatically. -# Branch health check -python3 apps/ai_mail.py ping +- Agents are ephemeral (wake, do work, exit) +- Safety limits: max turns per wake, max dispatches per branch per day +- PID-based locking prevents concurrent agents per branch +- Failed agents trigger bounce emails back to sender -# Dispatch status -python3 apps/ai_mail.py dispatch status +## Architecture -# Start dispatch daemon -python3 apps/ai_mail.py dispatch daemon +Follows the standard AIPass 3-layer pattern: + +``` +ai_mail/ +├── apps/ +│ ├── ai_mail.py # Entry point (auto-discovers modules) +│ ├── modules/ +│ │ ├── email.py # Send, inbox, view, reply, close, contacts +│ │ ├── dispatch.py # Dispatch status, daemon, wake +│ │ └── branch_ping.py # Branch health monitoring +│ └── handlers/ +│ ├── email/ # Delivery, formatting, inbox ops, purge +│ ├── dispatch/ # Daemon, wake, monitoring +│ ├── registry/ # Branch registry read/update +│ └── users/ # Branch detection, config generation ``` -## Integration Points +## Dependencies -**Depends On**: -- PRAX - Logging infrastructure -- CLI - Display and formatting -- DRONE - Command routing - -**Provides To**: -- All branches - Email communication -- AIPASS - System coordination - -## Memory System - -### Core Files -- `AI_MAIL.id.json` - Branch identity and architecture -- `AI_MAIL.local.json` - Session history (max 600 lines) -- `AI_MAIL.observations.json` - Collaboration patterns (max 600 lines) -- `AI_MAIL.ai_mail.json` - Email dashboard - -### Mailbox Structure -- `ai_mail.local/inbox.json` - Incoming messages (new + opened status) -- `ai_mail.local/sent/` - Sent messages (individual files, auto-purged at >10) -- `ai_mail.local/deleted/` - Closed/archived messages (individual files, auto-purged at >10) -- `ai_mail.local/.dispatch.lock` - PID-based dispatch lock (when agent active) -- `ai_mail.local/daemon_state.json` - Daemon daily counts and session tracking -- `ai_mail.local/dispatch_daemon.log` - Daemon activity log -- `safety_config.json` - Kill switch, poll interval, max turns/dispatches, autonomous branch list - -**Email Schema (v2):** -```json -{ - "id": "abc123", - "from": "@sender", - "from_name": "SENDER Branch", - "to": "@recipient", - "subject": "Subject line", - "message": "Message body", - "status": "new", // new | opened | closed - "timestamp": "2025-11-30 12:00:00", - "thread_id": null, // For future threading - "reply_to": null // Original message ID if reply -} -``` - -### Health Monitoring -- 🟢 **Healthy**: Under 80% of limits -- 🟡 **Warning**: 80-100% of limits -- 🔴 **Critical**: Over limits (compression needed) - -## Standards Compliance - -Seed audit score: **89%** (passing) - -Following AIPass code standards: -- 100% CLI integration -- Proper import patterns -- 3-layer architecture -- Handler independence -- Auto-discovery patterns - -## Development Notes - -- Code is truth - fail honestly -- Simple solutions over complex architecture -- Test incrementally, preserve what works -- Each instance isolated with own memory -- Never explain context again - memories persist - ---- - - -*Last Updated: 2026-02-28* -*Maintained by: AI_MAIL Branch* +- `prax` — Logging +- `cli` — Display formatting +- `drone` — Command routing and `@branch` resolution diff --git a/src/aipass/ai_mail/__init__.py b/src/aipass/ai_mail/__init__.py new file mode 100644 index 00000000..bfbc3fb8 --- /dev/null +++ b/src/aipass/ai_mail/__init__.py @@ -0,0 +1 @@ +"""AI Mail - Inter-branch messaging for AIPass.""" diff --git a/src/aipass/ai_mail/ai_mail_json/branch_ping_config.json b/src/aipass/ai_mail/ai_mail_json/branch_ping_config.json deleted file mode 100644 index f2586079..00000000 --- a/src/aipass/ai_mail/ai_mail_json/branch_ping_config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "module_name": "branch_ping", - "version": "1.0.0", - "timestamp": "2025-11-13", - "config": { - "auto_save": true, - "enabled": true - } -} \ No newline at end of file diff --git a/src/aipass/ai_mail/ai_mail_json/branch_ping_data.json b/src/aipass/ai_mail/ai_mail_json/branch_ping_data.json deleted file mode 100644 index 1e5ca465..00000000 --- a/src/aipass/ai_mail/ai_mail_json/branch_ping_data.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "module_name": "branch_ping", - "created": "2025-11-13", - "last_updated": "2025-11-13", - "operations_total": 0, - "operations_successful": 0, - "operations_failed": 0 -} \ No newline at end of file diff --git a/src/aipass/ai_mail/ai_mail_json/branch_ping_log.json b/src/aipass/ai_mail/ai_mail_json/branch_ping_log.json deleted file mode 100644 index 41ce4c35..00000000 --- a/src/aipass/ai_mail/ai_mail_json/branch_ping_log.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "timestamp": "2026-03-06T20:33:19.802974", - "operation": "ping_executed", - "data": { - "branch": "AI_MAIL", - "local_count": 0, - "obs_count": 0 - } - } -] \ No newline at end of file diff --git a/src/aipass/ai_mail/ai_mail_json/local_memory_monitor_registry.json b/src/aipass/ai_mail/ai_mail_json/local_memory_monitor_registry.json deleted file mode 100644 index 4c863277..00000000 --- a/src/aipass/ai_mail/ai_mail_json/local_memory_monitor_registry.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "last_updated": "2026-03-06T20:33:19.797905", - "active_branches": { - "/home/coder/workspace/src/aipass/ai_mail": { - "branch_name": "AI_MAIL", - "last_ping": "2026-03-06T20:33:19.797892", - "local_md": { - "line_count": 0, - "status": "\ud83d\udfe2 Healthy" - }, - "observations_md": { - "line_count": 0, - "status": "\ud83d\udfe2 Healthy" - } - } - }, - "statistics": { - "total_branches": 1, - "green_status": 0, - "yellow_status": 0, - "red_status": 0 - } -} \ No newline at end of file diff --git a/src/aipass/ai_mail/apps/ai_mail.py b/src/aipass/ai_mail/apps/ai_mail.py index 882a7437..a9554670 100644 --- a/src/aipass/ai_mail/apps/ai_mail.py +++ b/src/aipass/ai_mail/apps/ai_mail.py @@ -33,13 +33,8 @@ from typing import Dict, Any, Optional, List # Handle broken pipe gracefully (e.g. output piped to head) signal.signal(signal.SIGPIPE, signal.SIG_DFL) -# Pre-import dashboard to ensure path is resolved before module discovery -# This prevents import failures in handlers that depend on devpulse dashboard -try: - from aipass.dev_central.devpulse.apps.modules.dashboard import update_section as _update_section # noqa: F401 -except ImportError: - # Dashboard is optional - branch works without it - _update_section = None # type: ignore +# Dashboard integration (optional, requires dev_central package) +_update_section = None # type: ignore # AIPass infrastructure imports from aipass.prax.apps.modules.logger import system_logger as logger diff --git a/src/aipass/ai_mail/apps/handlers/__init__.py b/src/aipass/ai_mail/apps/handlers/__init__.py index fe5985f6..469e7134 100755 --- a/src/aipass/ai_mail/apps/handlers/__init__.py +++ b/src/aipass/ai_mail/apps/handlers/__init__.py @@ -43,7 +43,7 @@ def _find_real_caller(): def _extract_branch_name(filepath: str) -> str: """Extract branch name from a file path.""" - parts = filepath.split("/") + parts = Path(filepath).parts for i, part in enumerate(parts): if part in ("aipass", "MEMORY_BANK", "Nexus"): if i + 1 < len(parts): diff --git a/src/aipass/ai_mail/apps/handlers/central_writer.py b/src/aipass/ai_mail/apps/handlers/central_writer.py index 7bbbd879..9a413678 100644 --- a/src/aipass/ai_mail/apps/handlers/central_writer.py +++ b/src/aipass/ai_mail/apps/handlers/central_writer.py @@ -22,7 +22,7 @@ Aggregates branch inbox stats and writes to AI_MAIL.central.json. This file serves as AI_MAIL's API output for AIPASS dashboard integration. Architecture: -- Scans all ai_mail.local/inbox.json files across the system +- Scans all .ai_mail.local/inbox.json files across the system - Calculates per-branch unread/total message counts - Writes aggregated stats to AI_CENTRAL/AI_MAIL.central.json (under repo root) """ @@ -71,9 +71,9 @@ BRANCH_REGISTRY = _REPO_ROOT / "AIPASS_REGISTRY.json" def find_all_inbox_files() -> List[Path]: """ - Find all inbox.json files in ai_mail.local directories. + Find all inbox.json files in .ai_mail.local directories. - Scans the repo root directory for ai_mail.local/inbox.json files. + Scans the repo root directory for .ai_mail.local/inbox.json files. Excludes backup directories to avoid counting archived data. Returns: @@ -84,8 +84,8 @@ def find_all_inbox_files() -> List[Path]: """ inbox_files = [] - # Search pattern: any directory ending in ai_mail.local containing inbox.json - for ai_mail_dir in _REPO_ROOT.rglob("ai_mail.local"): + # Search pattern: any directory ending in .ai_mail.local containing inbox.json + for ai_mail_dir in _REPO_ROOT.rglob(".ai_mail.local"): # Skip backup/archive directories (but NOT backup_system branch itself) path_str = str(ai_mail_dir) if ".backup" in path_str or ".archive" in path_str or "/backups/" in path_str: @@ -102,10 +102,10 @@ def extract_branch_name(inbox_path: Path) -> str: """ Extract branch name from inbox.json path. - Given: .../seed/ai_mail.local/inbox.json + Given: .../seed/.ai_mail.local/inbox.json Returns: SEED - Given: .../prax/ai_mail.local/inbox.json + Given: .../prax/.ai_mail.local/inbox.json Returns: PRAX Args: @@ -114,7 +114,7 @@ def extract_branch_name(inbox_path: Path) -> str: Returns: Uppercase branch name """ - # Parent of ai_mail.local is the branch directory + # Parent of .ai_mail.local is the branch directory branch_dir = inbox_path.parent.parent branch_name = branch_dir.name.upper() @@ -280,7 +280,7 @@ def update_central() -> Dict[str, Any]: Should be called whenever mail is sent/received to keep dashboard in sync. Process: - 1. Scans all branch ai_mail.local/inbox.json files + 1. Scans all branch .ai_mail.local/inbox.json files 2. Aggregates unread and total message counts per branch 3. Calculates system-wide totals 4. Writes results to AI_CENTRAL/AI_MAIL.central.json (under repo root) diff --git a/src/aipass/ai_mail/apps/handlers/dispatch/daemon.py b/src/aipass/ai_mail/apps/handlers/dispatch/daemon.py index 2fbf2e28..d1344311 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/daemon.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/daemon.py @@ -64,9 +64,9 @@ _AI_MAIL_DIR = Path(__file__).resolve().parents[3] # ai_mail/ # Paths CONFIG_FILE = _AI_MAIL_DIR / "safety_config.json" -DAEMON_STATE_FILE = _AI_MAIL_DIR / "ai_mail.local" / "daemon_state.json" -DAEMON_LOG_FILE = _AI_MAIL_DIR / "ai_mail.local" / "dispatch_daemon.log" -DAEMON_PID_FILE = _AI_MAIL_DIR / "ai_mail.local" / "daemon.pid" +DAEMON_STATE_FILE = _AI_MAIL_DIR / ".ai_mail.local" / "daemon_state.json" +DAEMON_LOG_FILE = _AI_MAIL_DIR / ".ai_mail.local" / "dispatch_daemon.log" +DAEMON_PID_FILE = _AI_MAIL_DIR / ".ai_mail.local" / "daemon.pid" BRANCH_REGISTRY = _REPO_ROOT / "AIPASS_REGISTRY.json" # Telegram notifications (scheduler bot) @@ -168,7 +168,7 @@ def _set_session_name(branch_path: Path, name: str) -> bool: def _check_lock(branch_path: Path) -> Optional[Dict[str, Any]]: """Check if branch has an active dispatch lock. Returns lock data or None.""" - lock_file = branch_path / "ai_mail.local" / ".dispatch.lock" + lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock" if not lock_file.exists(): return None try: @@ -212,7 +212,7 @@ def _check_lock(branch_path: Path) -> Optional[Dict[str, Any]]: def _acquire_lock(branch_path: Path, pid: int) -> tuple[bool, str]: """Acquire dispatch lock for branch. Atomic creation via O_CREAT|O_EXCL.""" - lock_file = branch_path / "ai_mail.local" / ".dispatch.lock" + lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock" lock_data = { "pid": pid, "timestamp": datetime.now().isoformat(), @@ -336,7 +336,7 @@ def check_inbox_for_dispatch(branch_path: Path) -> Optional[Dict[str, Any]]: Also retries opened dispatch emails orphaned for >30 min (agent crashed before completing). """ - inbox_file = branch_path / "ai_mail.local" / "inbox.json" + inbox_file = branch_path / ".ai_mail.local" / "inbox.json" inbox_data = _read_json(inbox_file) if inbox_data is None: return None @@ -372,7 +372,7 @@ def check_inbox_for_dispatch(branch_path: Path) -> Optional[Dict[str, Any]]: def count_new_emails(branch_path: Path) -> int: """Count new (unread) emails in a branch's inbox.""" - inbox_file = branch_path / "ai_mail.local" / "inbox.json" + inbox_file = branch_path / ".ai_mail.local" / "inbox.json" inbox_data = _read_json(inbox_file) if inbox_data is None: return 0 @@ -409,7 +409,7 @@ def spawn_agent( subject = message.get("subject", "") max_turns = config.get("max_turns_per_wake", 15) - lock_file_path = str(branch_path / "ai_mail.local" / ".dispatch.lock") + lock_file_path = str(branch_path / ".ai_mail.local" / ".dispatch.lock") # Prompt — no lock cleanup instruction (dispatch_monitor handles it) prompt = ( @@ -426,7 +426,7 @@ def spawn_agent( # Build monitor command (dispatch_monitor wraps claude, handles bounce + lock cleanup) MONITOR_SCRIPT = Path(__file__).resolve().parent / "dispatch_monitor.py" - LOG_DIR = branch_path / "ai_mail.local" + LOG_DIR = branch_path / ".ai_mail.local" LOG_DIR.mkdir(parents=True, exist_ok=True) STDERR_LOG = str(LOG_DIR / "agent_stderr.log") @@ -504,6 +504,8 @@ def is_protected_branch(branch_email: str) -> bool: def _read_session_type(pid_str: str) -> str: """Read AIPASS_SESSION_TYPE from /proc/{pid}/environ. Returns 'interactive' if unset.""" + if sys.platform != "linux": + return 'interactive' try: with open(f'/proc/{pid_str}/environ', 'rb') as f: data = f.read() @@ -540,6 +542,8 @@ def _is_branch_occupied(branch_path: Path) -> bool: if not pid_str: continue try: + if sys.platform != "linux": + continue cwd = os.readlink(f'/proc/{pid_str}/cwd') if Path(cwd).resolve() == resolved: session_type = _read_session_type(pid_str) diff --git a/src/aipass/ai_mail/apps/handlers/dispatch/dispatch_monitor.py b/src/aipass/ai_mail/apps/handlers/dispatch/dispatch_monitor.py index f23d2c26..f97d1e4b 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/dispatch_monitor.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/dispatch_monitor.py @@ -124,7 +124,7 @@ def main(): if key.startswith("CLAUDE") or key == "AIPASS_BOT_ID": spawn_env.pop(key) - # Extract CWD from lock file path (branch_path/ai_mail.local/.dispatch.lock) + # Extract CWD from lock file path (branch_path/.ai_mail.local/.dispatch.lock) lock_path = Path(lock_file) branch_path = lock_path.parent.parent cwd = str(branch_path) diff --git a/src/aipass/ai_mail/apps/handlers/dispatch/pending_work.py b/src/aipass/ai_mail/apps/handlers/dispatch/pending_work.py index ecd3f886..192751dd 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/pending_work.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/pending_work.py @@ -32,8 +32,8 @@ PENDING_WORK_FILENAME = ".pending_work.json" def _get_pending_path(branch_path: Path) -> Path: """Get the pending work file path for a branch.""" if branch_path == Path("/"): - return Path.cwd() / "ai_mail.local" / PENDING_WORK_FILENAME - return branch_path / "ai_mail.local" / PENDING_WORK_FILENAME + return Path.cwd() / ".ai_mail.local" / PENDING_WORK_FILENAME + return branch_path / ".ai_mail.local" / PENDING_WORK_FILENAME def load_pending_work(branch_path: Path) -> Dict[str, Any]: diff --git a/src/aipass/ai_mail/apps/handlers/dispatch/status.py b/src/aipass/ai_mail/apps/handlers/dispatch/status.py index 35e981bc..e8ac8172 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/status.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/status.py @@ -29,7 +29,7 @@ from typing import Dict, Any, List, Optional # Dispatch log location (package-relative) _AI_MAIL_DIR = Path(__file__).resolve().parents[3] # ai_mail/ -DISPATCH_LOG_FILE = _AI_MAIL_DIR / "ai_mail.local" / "dispatch_log.json" +DISPATCH_LOG_FILE = _AI_MAIL_DIR / ".ai_mail.local" / "dispatch_log.json" def load_dispatch_log() -> List[Dict[str, Any]]: diff --git a/src/aipass/ai_mail/apps/handlers/dispatch/wake.py b/src/aipass/ai_mail/apps/handlers/dispatch/wake.py index eb0d4d17..3cd977d4 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/wake.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/wake.py @@ -122,7 +122,7 @@ def _read_json(filepath: Path) -> Optional[dict]: def _check_lock(branch_path: Path) -> Optional[dict]: """Check if branch has an active dispatch lock. Returns lock data or None.""" - lock_file = branch_path / "ai_mail.local" / ".dispatch.lock" + lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock" if not lock_file.exists(): return None try: @@ -158,7 +158,7 @@ def _check_lock(branch_path: Path) -> Optional[dict]: def _acquire_lock(branch_path: Path, pid: int) -> Tuple[bool, str]: """Acquire dispatch lock for branch. Atomic creation.""" - lock_file = branch_path / "ai_mail.local" / ".dispatch.lock" + lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock" lock_data = { "pid": pid, "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), @@ -218,6 +218,8 @@ def _set_session_name(branch_path: Path, name: str) -> bool: def _read_session_type(pid_str: str) -> str: """Read AIPASS_SESSION_TYPE from /proc/{pid}/environ. Returns 'interactive' if unset.""" + if sys.platform != "linux": + return 'interactive' try: with open(f'/proc/{pid_str}/environ', 'rb') as f: data = f.read() @@ -248,6 +250,8 @@ def _is_branch_occupied(branch_path: Path) -> bool: if not pid_str: continue try: + if sys.platform != "linux": + continue cwd = os.readlink(f'/proc/{pid_str}/cwd') if str(Path(cwd).resolve()) == resolved: session_type = _read_session_type(pid_str) @@ -283,11 +287,12 @@ def _check_pid_alive(pid: int) -> bool: """Check if a process is alive (not zombie).""" try: os.kill(pid, 0) - # Also verify not zombie - with open(f'/proc/{pid}/status', 'r') as f: - for line in f: - if line.startswith('State:'): - return 'Z' not in line + # Also verify not zombie via /proc (Linux only) + if sys.platform == "linux": + with open(f'/proc/{pid}/status', 'r') as f: + for line in f: + if line.startswith('State:'): + return 'Z' not in line return True except (ProcessLookupError, FileNotFoundError): return False @@ -376,7 +381,7 @@ def wake_branch(branch_email: str, custom_message: Optional[str] = None, config = _load_config() max_turns = config.get("max_turns_per_wake", 50) - lock_file_path = str(branch_path / "ai_mail.local" / ".dispatch.lock") + lock_file_path = str(branch_path / ".ai_mail.local" / ".dispatch.lock") if custom_message: prompt = f"Hi. {custom_message} " else: @@ -406,7 +411,7 @@ def wake_branch(branch_email: str, custom_message: Optional[str] = None, _set_session_name(branch_path, session_label) # Step 7: Spawn via dispatch_monitor - log_dir = branch_path / "ai_mail.local" + log_dir = branch_path / ".ai_mail.local" log_dir.mkdir(parents=True, exist_ok=True) stderr_log = str(log_dir / "agent_stderr.log") @@ -459,7 +464,7 @@ def wake_branch(branch_email: str, custom_message: Optional[str] = None, else: status.fail("alive", f"Agent died immediately (PID {monitor_pid})") # Clean up lock - lock_file = branch_path / "ai_mail.local" / ".dispatch.lock" + lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock" lock_file.unlink(missing_ok=True) return status, False diff --git a/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py b/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py index a2d4cddb..3f68d33b 100644 --- a/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py +++ b/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py @@ -154,7 +154,7 @@ def push_dashboard_update(branch_path: Path) -> bool: Read branch inbox and push ai_mail section to dashboard. This is the primary public function. It: - 1. Reads the branch's ai_mail.local/inbox.json + 1. Reads the branch's .ai_mail.local/inbox.json 2. Calculates section data (new, opened, total, oldest_unread_age, etc.) 3. Calls write_section() to update DASHBOARD.local.json @@ -171,9 +171,9 @@ def push_dashboard_update(branch_path: Path) -> bool: # Determine inbox path if branch_path == Path("/"): - inbox_file = Path.cwd() / "ai_mail.local" / "inbox.json" + inbox_file = Path.cwd() / ".ai_mail.local" / "inbox.json" else: - inbox_file = branch_path / "ai_mail.local" / "inbox.json" + inbox_file = branch_path / ".ai_mail.local" / "inbox.json" # Read inbox (BYPASS: direct json.load - this is a data file, not a template) if not inbox_file.exists(): diff --git a/src/aipass/ai_mail/apps/handlers/email/delivery.py b/src/aipass/ai_mail/apps/handlers/email/delivery.py index 860aae8d..56dfe4ce 100644 --- a/src/aipass/ai_mail/apps/handlers/email/delivery.py +++ b/src/aipass/ai_mail/apps/handlers/email/delivery.py @@ -229,7 +229,7 @@ def deliver_email_to_branch( on_delivered: Optional[Callable] = None ) -> Tuple[bool, str]: """ - Deliver email to target branch's ai_mail.local/inbox.json file. + Deliver email to target branch's .ai_mail.local/inbox.json file. Appends message to inbox JSON messages array. @@ -283,11 +283,11 @@ def deliver_email_to_branch( branch_path = Path(branches[to_branch]) - # Find the branch's ai_mail.local/inbox.json file + # Find the branch's .ai_mail.local/inbox.json file if branch_path == Path("/") or branch_path == _REPO_ROOT: - inbox_file = _REPO_ROOT / "ai_mail.local" / "inbox.json" + inbox_file = _REPO_ROOT / ".ai_mail.local" / "inbox.json" else: - inbox_file = branch_path / "ai_mail.local" / "inbox.json" + inbox_file = branch_path / ".ai_mail.local" / "inbox.json" if not inbox_file.exists(): # Auto-provision inbox for new branches (self-healing) diff --git a/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py b/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py index ceda2ed0..0cb12c34 100644 --- a/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py +++ b/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py @@ -80,7 +80,7 @@ def _save_to_deleted_folder(mailbox_path: Path, message: Dict) -> Path: Save a message to the deleted/ folder as individual JSON file. Args: - mailbox_path: Path to ai_mail.local directory + mailbox_path: Path to .ai_mail.local directory message: Email message dict to archive Returns: @@ -111,7 +111,7 @@ def _migrate_deleted_json_if_exists(mailbox_path: Path) -> int: Migrate existing deleted.json to deleted/ directory on first access. Args: - mailbox_path: Path to ai_mail.local directory + mailbox_path: Path to .ai_mail.local directory Returns: Number of messages migrated @@ -165,7 +165,7 @@ def mark_read_and_archive(branch_path: Path, message_id: str) -> Tuple[bool, str Returns: Tuple of (success: bool, message: str) """ - mailbox_path = branch_path / "ai_mail.local" + mailbox_path = branch_path / ".ai_mail.local" inbox_file = mailbox_path / "inbox.json" if not inbox_file.exists(): @@ -240,7 +240,7 @@ def mark_all_read_and_archive(branch_path: Path) -> Tuple[bool, str, int]: Returns: Tuple of (success: bool, message: str, count: int) """ - mailbox_path = branch_path / "ai_mail.local" + mailbox_path = branch_path / ".ai_mail.local" inbox_file = mailbox_path / "inbox.json" if not inbox_file.exists(): @@ -308,7 +308,7 @@ def _trigger_deleted_purge(branch_path: Path) -> None: """ try: from aipass.ai_mail.apps.handlers.email.purge import purge_deleted_folder - mailbox_path = branch_path / "ai_mail.local" + mailbox_path = branch_path / ".ai_mail.local" purge_deleted_folder(mailbox_path) except Exception: pass # Silent fail - purge is best-effort @@ -331,7 +331,7 @@ def mark_as_opened(branch_path: Path, message_id: str) -> Tuple[bool, str, Optio Returns: Tuple of (success: bool, message: str, email_data: dict or None) """ - inbox_file = branch_path / "ai_mail.local" / "inbox.json" + inbox_file = branch_path / ".ai_mail.local" / "inbox.json" if not inbox_file.exists(): return False, f"Inbox not found: {inbox_file}", None @@ -390,7 +390,7 @@ def mark_as_closed_and_archive(branch_path: Path, message_id: str, skip_post_ops Returns: Tuple of (success: bool, message: str) """ - mailbox_path = branch_path / "ai_mail.local" + mailbox_path = branch_path / ".ai_mail.local" inbox_file = mailbox_path / "inbox.json" if not inbox_file.exists(): diff --git a/src/aipass/ai_mail/apps/handlers/email/lock_utils.py b/src/aipass/ai_mail/apps/handlers/email/lock_utils.py index f4cd2eff..9d9f67a2 100644 --- a/src/aipass/ai_mail/apps/handlers/email/lock_utils.py +++ b/src/aipass/ai_mail/apps/handlers/email/lock_utils.py @@ -31,7 +31,7 @@ from datetime import datetime # Standard logging -# Lock file name - placed in branch's ai_mail.local/ directory +# Lock file name - placed in branch's .ai_mail.local/ directory LOCK_FILENAME = ".dispatch.lock" # Stale lock timeout in seconds (10 minutes) @@ -41,8 +41,8 @@ STALE_LOCK_TIMEOUT = 600 def _get_lock_path(branch_path: Path) -> Path: """Get the lock file path for a branch.""" if branch_path == Path("/"): - return Path.cwd() / "ai_mail.local" / LOCK_FILENAME - return branch_path / "ai_mail.local" / LOCK_FILENAME + return Path.cwd() / ".ai_mail.local" / LOCK_FILENAME + return branch_path / ".ai_mail.local" / LOCK_FILENAME def _is_pid_running(pid: int) -> bool: diff --git a/src/aipass/ai_mail/apps/handlers/email/purge.py b/src/aipass/ai_mail/apps/handlers/email/purge.py index 124abd97..dbcee3e1 100644 --- a/src/aipass/ai_mail/apps/handlers/email/purge.py +++ b/src/aipass/ai_mail/apps/handlers/email/purge.py @@ -63,7 +63,7 @@ def purge_sent_folder(mailbox_path: Path) -> Dict[str, Any]: Keeps 10 most recent emails, vectorizes and archives older ones. Args: - mailbox_path: Path to ai_mail.local directory + mailbox_path: Path to .ai_mail.local directory Returns: Dict with success, purged_count, archived_paths @@ -97,7 +97,7 @@ def purge_deleted_folder(mailbox_path: Path) -> Dict[str, Any]: Keeps 10 most recent emails, vectorizes and archives older ones. Args: - mailbox_path: Path to ai_mail.local directory + mailbox_path: Path to .ai_mail.local directory Returns: Dict with success, purged_count, archived_count @@ -129,7 +129,7 @@ def _purge_email_files(mailbox_path: Path, files: List[Path], folder_type: str) Purge list of email files (vectorize, archive, delete). Args: - mailbox_path: Path to ai_mail.local directory + mailbox_path: Path to .ai_mail.local directory files: List of file paths to purge folder_type: "sent" or "deleted" for logging @@ -246,7 +246,7 @@ def _archive_email_files(mailbox_path: Path, files: List[Path], folder_type: str Archive email files to .archive/ directory. Args: - mailbox_path: Path to ai_mail.local directory + mailbox_path: Path to .ai_mail.local directory files: List of file paths to archive folder_type: "sent" or "deleted" for subdirectory @@ -280,7 +280,7 @@ def run_purge(mailbox_path: Path) -> Dict[str, Any]: Convenience function to run both purges. Args: - mailbox_path: Path to ai_mail.local directory + mailbox_path: Path to .ai_mail.local directory Returns: Dict with combined results diff --git a/src/aipass/ai_mail/apps/handlers/email/reply.py b/src/aipass/ai_mail/apps/handlers/email/reply.py index df20883d..cbd81f3d 100644 --- a/src/aipass/ai_mail/apps/handlers/email/reply.py +++ b/src/aipass/ai_mail/apps/handlers/email/reply.py @@ -151,7 +151,7 @@ def send_reply( return False, f"Failed to deliver reply: {error_msg}", None # Save to sender's sent folder - sent_folder = from_branch_path / "ai_mail.local" / "sent" + sent_folder = from_branch_path / ".ai_mail.local" / "sent" sent_folder.mkdir(parents=True, exist_ok=True) reply_id = str(uuid.uuid4())[:8] diff --git a/src/aipass/ai_mail/apps/handlers/json/json_handler.py b/src/aipass/ai_mail/apps/handlers/json/json_handler.py index fa52c55c..13c7d16f 100644 --- a/src/aipass/ai_mail/apps/handlers/json/json_handler.py +++ b/src/aipass/ai_mail/apps/handlers/json/json_handler.py @@ -25,7 +25,7 @@ from pathlib import Path # Infrastructure paths (package-relative) _AI_MAIL_ROOT = Path(__file__).resolve().parents[3] # ai_mail/ -AI_MAIL_JSON_DIR = _AI_MAIL_ROOT / "ai_mail_json" +AI_MAIL_JSON_DIR = _AI_MAIL_ROOT / ".ai_mail.local" from aipass.ai_mail.apps.handlers.json_utils.json_handler import ( # noqa: F401 load_json, diff --git a/src/aipass/ai_mail/apps/handlers/json_utils/json_handler.py b/src/aipass/ai_mail/apps/handlers/json_utils/json_handler.py index fa97f5de..e233cb82 100644 --- a/src/aipass/ai_mail/apps/handlers/json_utils/json_handler.py +++ b/src/aipass/ai_mail/apps/handlers/json_utils/json_handler.py @@ -32,7 +32,7 @@ import inspect _AI_MAIL_ROOT = Path(__file__).resolve().parents[3] # ai_mail/ # Constants - Updated for AI_MAIL -AI_MAIL_JSON_DIR = _AI_MAIL_ROOT / "ai_mail_json" +AI_MAIL_JSON_DIR = _AI_MAIL_ROOT / ".ai_mail.local" JSON_TEMPLATES_DIR = _AI_MAIL_ROOT / "apps" / "json_templates" diff --git a/src/aipass/ai_mail/apps/handlers/persistence/json_ops.py b/src/aipass/ai_mail/apps/handlers/persistence/json_ops.py deleted file mode 100644 index ad6e7333..00000000 --- a/src/aipass/ai_mail/apps/handlers/persistence/json_ops.py +++ /dev/null @@ -1,260 +0,0 @@ - -# ============================================= -# META DATA HEADER -# Name: json_ops.py - Persistence Handler (JSON Operations) -# Date: 2025-11-15 -# Version: 1.0.0 -# Category: ai_mail/handlers/persistence -# -# CHANGELOG: -# - v1.0.0 (2025-11-15): Initial version with auto-creating & self-healing JSON system -# -# CODE STANDARDS: -# - Auto-creates JSON files from templates -# - Self-healing for corrupted files -# - Auto-detects calling module for logging -# - Implements log rotation based on config limits -# ============================================= - -""" -Persistence Handler - JSON Operations - -Handles persistent data operations for ai_mail modules. -Manages default JSON files (config, data, log). -Auto-creating & self-healing JSON system. -""" - -import json -from pathlib import Path -from datetime import datetime -from typing import Dict, List, Any, Optional -import inspect - -# Infrastructure paths (package-relative) -_AI_MAIL_ROOT = Path(__file__).resolve().parents[3] # ai_mail/ - -# Constants -AI_MAIL_JSON_DIR = _AI_MAIL_ROOT / "ai_mail_json" -JSON_TEMPLATES_DIR = _AI_MAIL_ROOT / "apps" / "json_templates" - - -def _get_caller_module_name() -> str: - """ - Auto-detect calling module name from call stack - - Returns: - Module name (e.g., "imports_standard" from imports_standard.py) - """ - try: - stack = inspect.stack() - # Skip frames: [0]=this function, [1]=log_operation, [2]=actual caller - if len(stack) > 2: - caller_frame = stack[2] - caller_path = Path(caller_frame.filename) - module_name = caller_path.stem - - # Validate module name - if module_name and not module_name.startswith('_'): - return module_name - - # Fallback - return "unknown" - except Exception: - return "unknown" - - -def load_template(json_type: str, module_name: str) -> Any: - """Load JSON template from template file""" - template_path = JSON_TEMPLATES_DIR / "default" / f"{json_type}.json" - - if not template_path.exists(): - return None - - try: - with open(template_path, 'r', encoding='utf-8') as f: - template = json.load(f) - - # Replace placeholders - template_str = json.dumps(template) - template_str = template_str.replace("{{MODULE_NAME}}", module_name) - template_str = template_str.replace("{{TIMESTAMP}}", datetime.now().date().isoformat()) - - return json.loads(template_str) - except Exception: - return None - - -def validate_json_structure(data: Any, json_type: str) -> bool: - """Validate JSON structure matches expected type""" - if json_type == "config": - if not isinstance(data, dict): - return False - required = ["module_name", "version", "config"] - return all(key in data for key in required) - - elif json_type == "data": - if not isinstance(data, dict): - return False - required = ["created", "last_updated"] - return all(key in data for key in required) - - elif json_type == "log": - return isinstance(data, list) - - return False - - -def get_json_path(module_name: str, json_type: str) -> Path: - """Get path for module JSON file""" - filename = f"{module_name}_{json_type}.json" - return AI_MAIL_JSON_DIR / filename - - -def ensure_json_exists(module_name: str, json_type: str) -> bool: - """Ensure JSON file exists, create from template if missing""" - AI_MAIL_JSON_DIR.mkdir(parents=True, exist_ok=True) - - json_path = get_json_path(module_name, json_type) - - if json_path.exists(): - try: - with open(json_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - if validate_json_structure(data, json_type): - return True - except Exception: - pass - - template = load_template(json_type, module_name) - if template is None: - return False - - try: - with open(json_path, 'w', encoding='utf-8') as f: - json.dump(template, f, indent=2, ensure_ascii=False) - return True - except Exception: - return False - - -def load_json(module_name: str, json_type: str) -> Optional[Any]: - """Load JSON file, auto-create if missing""" - if not ensure_json_exists(module_name, json_type): - return None - - json_path = get_json_path(module_name, json_type) - - try: - with open(json_path, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception: - return None - - -def save_json(module_name: str, json_type: str, data: Any) -> bool: - """Save JSON file""" - json_path = get_json_path(module_name, json_type) - - if not validate_json_structure(data, json_type): - return False - - if json_type == "data" and isinstance(data, dict): - data["last_updated"] = datetime.now().date().isoformat() - - try: - with open(json_path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, ensure_ascii=False) - return True - except Exception: - return False - - -def ensure_module_jsons(module_name: str) -> bool: - """Ensure all 3 JSON files exist for a module""" - ensure_json_exists(module_name, "config") - ensure_json_exists(module_name, "data") - ensure_json_exists(module_name, "log") - return True - - -def log_operation(operation: str, data: Dict[str, Any] | None = None, module_name: str | None = None) -> bool: - """ - Add entry to module log with automatic rotation - - Auto-detects calling module if module_name not provided. - Implements config-controlled log limits to prevent unbounded growth. - When max_log_entries is reached, removes oldest entries (FIFO). - - Args: - operation: Operation name to log - data: Optional data dict - module_name: Optional module name (auto-detected if not provided) - - Returns: - True if successful, False otherwise - """ - # Auto-detect module name if not provided - if module_name is None: - module_name = _get_caller_module_name() - - ensure_module_jsons(module_name) - - # Load config to get max_log_entries - config = load_json(module_name, "config") - max_entries = 100 # Default - if config and "config" in config: - max_entries = config["config"].get("max_log_entries", 100) - - # Load existing log - log = load_json(module_name, "log") - if log is None: - log = [] - - # Create new entry - entry: Dict[str, Any] = { - "timestamp": datetime.now().isoformat(), - "operation": operation - } - - if data: - entry["data"] = data - - # Add new entry - log.append(entry) - - # Rotate if exceeds max (keep most recent entries) - if len(log) > max_entries: - log = log[-max_entries:] - - return save_json(module_name, "log", log) - - -def increment_counter(module_name: str, counter_name: str, amount: int = 1) -> bool: - """Increment a counter in data JSON""" - ensure_module_jsons(module_name) - - data = load_json(module_name, "data") - if data is None: - return False - - if counter_name not in data: - data[counter_name] = 0 - - data[counter_name] += amount - - return save_json(module_name, "data", data) - - -def update_data_metrics(module_name: str, **metrics: Any) -> bool: - """Update data metrics""" - ensure_module_jsons(module_name) - - data = load_json(module_name, "data") - if data is None: - return False - - for key, value in metrics.items(): - data[key] = value - - return save_json(module_name, "data", data) diff --git a/src/aipass/ai_mail/apps/handlers/registry/update.py b/src/aipass/ai_mail/apps/handlers/registry/update.py index ecfadcb2..9d77e6af 100644 --- a/src/aipass/ai_mail/apps/handlers/registry/update.py +++ b/src/aipass/ai_mail/apps/handlers/registry/update.py @@ -39,7 +39,7 @@ from aipass.cli.apps.modules import console # Constants MODULE_NAME = "registry.update" _AI_MAIL_ROOT = Path(__file__).resolve().parents[3] # ai_mail/ -AI_MAIL_JSON = _AI_MAIL_ROOT / "ai_mail_json" +AI_MAIL_JSON = _AI_MAIL_ROOT / ".ai_mail.local" REGISTRY_PATH = AI_MAIL_JSON / "local_memory_monitor_registry.json" THRESHOLDS = { "green": (0, 400), diff --git a/src/aipass/ai_mail/apps/handlers/users/branch_detection.py b/src/aipass/ai_mail/apps/handlers/users/branch_detection.py index e2b713d5..71343e77 100644 --- a/src/aipass/ai_mail/apps/handlers/users/branch_detection.py +++ b/src/aipass/ai_mail/apps/handlers/users/branch_detection.py @@ -67,8 +67,9 @@ def detect_branch_from_pwd() -> Dict | None: None if no branch detected """ try: - # Get current working directory - cwd = Path.cwd() + # Use caller's CWD if passed by drone, otherwise fall back to process CWD + caller_cwd = os.environ.get("AIPASS_CALLER_CWD") + cwd = Path(caller_cwd) if caller_cwd else Path.cwd() # Find branch root branch_root = find_branch_root(cwd) @@ -103,9 +104,12 @@ def find_branch_root(start_path: Path) -> Path | None: # Walk up directory tree (max 10 levels to prevent infinite loop) for _ in range(10): - # Check if this directory has a [BRANCH].id.json file - for file in current.glob("*.id.json"): - # Found a .id.json file - this is likely a branch root + # Check for dev-pass pattern: [BRANCH].id.json + for _ in current.glob("*.id.json"): + return current + + # Check for public/trinity pattern: .trinity/passport.json + if (current / ".trinity" / "passport.json").exists(): return current # Move up one level @@ -134,13 +138,18 @@ def get_branch_info_from_registry(branch_path: Path) -> Dict | None: with open(BRANCH_REGISTRY_PATH, 'r', encoding='utf-8') as f: registry = json.load(f) - # Normalize branch_path for comparison - branch_path_str = str(branch_path.resolve()) + registry_dir = BRANCH_REGISTRY_PATH.parent + branch_path_resolved = branch_path.resolve() # Search registry for matching path for branch in registry.get("branches", []): - if Path(branch["path"]).resolve() == Path(branch_path_str): - # Found match - return branch info + reg_path = Path(branch["path"]) + # Resolve relative paths against registry location, not CWD + if not reg_path.is_absolute(): + reg_path = (registry_dir / reg_path).resolve() + else: + reg_path = reg_path.resolve() + if reg_path == branch_path_resolved: return branch return None diff --git a/src/aipass/ai_mail/apps/handlers/users/config_generator.py b/src/aipass/ai_mail/apps/handlers/users/config_generator.py index 0b57009c..2b6d2eed 100644 --- a/src/aipass/ai_mail/apps/handlers/users/config_generator.py +++ b/src/aipass/ai_mail/apps/handlers/users/config_generator.py @@ -54,8 +54,8 @@ def generate_local_config(branch_info: Dict) -> Dict: # Generate display name from branch info display_name = generate_display_name(branch_info) - # Generate mailbox path (ai_mail.local/ in branch directory) - mailbox_path = str(branch_path / "ai_mail.local") + # Generate mailbox path (.ai_mail.local/ in branch directory) + mailbox_path = str(branch_path / ".ai_mail.local") config = { "version": "1.0.0", @@ -155,7 +155,7 @@ def create_local_config_file(branch_info: Dict, force: bool = False) -> Path | N def create_mailbox_directory(branch_path: Path) -> Path | None: """ - Create ai_mail.local/ mailbox directory for a branch. + Create .ai_mail.local/ mailbox directory for a branch. Creates subdirectories: inbox/, sent/, deleted/ @@ -166,7 +166,7 @@ def create_mailbox_directory(branch_path: Path) -> Path | None: Path to mailbox directory, or None if failed """ try: - mailbox_dir = branch_path / "ai_mail.local" + mailbox_dir = branch_path / ".ai_mail.local" mailbox_dir.mkdir(parents=True, exist_ok=True) # Create subdirectories @@ -207,9 +207,9 @@ def setup_branch_for_aimail(branch_info: Dict, force: bool = False) -> bool: Creates: - ai_mail_config/user_config.json - - ai_mail.local/ mailbox directory - - ai_mail.local/inbox.json - - ai_mail.local/sent.json + - .ai_mail.local/ mailbox directory + - .ai_mail.local/inbox.json + - .ai_mail.local/sent.json Args: branch_info: Branch info dict from registry @@ -263,7 +263,7 @@ if __name__ == "__main__": console.print(" 1. Generate config from branch registry info") console.print(" 2. Create ai_mail_config/ directory") console.print(" 3. Write user_config.json with branch-specific settings") - console.print(" 4. Create ai_mail.local/ mailbox directory") + console.print(" 4. Create .ai_mail.local/ mailbox directory") console.print(" 5. Initialize inbox.json and sent.json") console.print() console.print("="*70 + "\n") diff --git a/src/aipass/ai_mail/apps/handlers/users/load.py b/src/aipass/ai_mail/apps/handlers/users/load.py index 0bdfa1f2..d8b38daa 100644 --- a/src/aipass/ai_mail/apps/handlers/users/load.py +++ b/src/aipass/ai_mail/apps/handlers/users/load.py @@ -35,7 +35,7 @@ from typing import Dict # CONSTANTS # ============================================= _AI_MAIL_ROOT = Path(__file__).resolve().parents[3] # ai_mail/ -AI_MAIL_JSON = _AI_MAIL_ROOT / "ai_mail_json" +AI_MAIL_JSON = _AI_MAIL_ROOT / ".ai_mail.local" USER_CONFIG_FILE = AI_MAIL_JSON / "user_config.json" # Import branch detection (after constants defined) diff --git a/src/aipass/ai_mail/apps/handlers/users/user.py b/src/aipass/ai_mail/apps/handlers/users/user.py index 32438df4..a85470d8 100644 --- a/src/aipass/ai_mail/apps/handlers/users/user.py +++ b/src/aipass/ai_mail/apps/handlers/users/user.py @@ -46,7 +46,7 @@ def get_current_user() -> Dict: { "email_address": "@branch", "display_name": "BRANCH_NAME", - "mailbox_path": "/path/to/branch/ai_mail.local", + "mailbox_path": "/path/to/branch/.ai_mail.local", "timestamp_format": "%Y-%m-%d %H:%M:%S" } @@ -81,7 +81,7 @@ def get_current_user() -> Dict: # Construct mailbox path (branch_path guaranteed non-None by check above) assert branch_path is not None - mailbox_path = branch_path / "ai_mail.local" + mailbox_path = branch_path / ".ai_mail.local" # Return user info in expected format return { @@ -121,7 +121,7 @@ def get_user_by_email(email: str) -> Dict | None: return { "email_address": branch.get("email"), "display_name": branch.get("name"), - "mailbox_path": str(branch_path / "ai_mail.local"), + "mailbox_path": str(branch_path / ".ai_mail.local"), "timestamp_format": "%Y-%m-%d %H:%M:%S" } return None @@ -154,7 +154,7 @@ def get_all_users() -> Dict[str, Dict]: users[email] = { "email_address": email, "display_name": branch.get("name"), - "mailbox_path": str(branch_path / "ai_mail.local"), + "mailbox_path": str(branch_path / ".ai_mail.local"), "timestamp_format": "%Y-%m-%d %H:%M:%S" } return users diff --git a/src/aipass/ai_mail/apps/modules/branch_ping.py b/src/aipass/ai_mail/apps/modules/branch_ping.py index 409208d1..6ecc1c05 100644 --- a/src/aipass/ai_mail/apps/modules/branch_ping.py +++ b/src/aipass/ai_mail/apps/modules/branch_ping.py @@ -41,7 +41,7 @@ from aipass.ai_mail.apps.handlers.registry.update import ( get_branch_context, update_json_memory_health ) -from aipass.ai_mail.apps.handlers.persistence.json_ops import log_operation +from aipass.ai_mail.apps.handlers.json_utils.json_handler import log_operation MODULE_NAME = "branch_ping" THRESHOLDS = {"green": (0, 400), "yellow": (401, 550), "red": (551, float('inf'))} diff --git a/src/aipass/ai_mail/apps/modules/email.py b/src/aipass/ai_mail/apps/modules/email.py index c9caea80..e2d4db92 100644 --- a/src/aipass/ai_mail/apps/modules/email.py +++ b/src/aipass/ai_mail/apps/modules/email.py @@ -68,7 +68,7 @@ from aipass.ai_mail.apps.handlers.email.reply import get_email_by_id, send_reply from aipass.ai_mail.apps.handlers.email.header import prepend_dispatch_header from aipass.ai_mail.apps.handlers.users.user import get_current_user from aipass.ai_mail.apps.handlers.registry.read import get_all_branches, get_branch_by_email -from aipass.ai_mail.apps.handlers.persistence.json_ops import log_operation +from aipass.ai_mail.apps.handlers.json_utils.json_handler import log_operation def _on_email_delivered(branch_path, new_count, opened_count, total): @@ -457,7 +457,7 @@ def send_email_direct(to_branch: str, subject: str, message: str, auto_execute: user_info = { "email_address": email_addr, "display_name": branch_info["name"], - "mailbox_path": str(branch_path / "ai_mail.local"), + "mailbox_path": str(branch_path / ".ai_mail.local"), "timestamp_format": "%Y-%m-%d %H:%M:%S" } else: @@ -466,7 +466,7 @@ def send_email_direct(to_branch: str, subject: str, message: str, auto_execute: user_info = { "email_address": email_addr, "display_name": branch_name, - "mailbox_path": str(_AI_MAIL_DIR.parent / from_branch.lstrip('@').lower() / "ai_mail.local"), + "mailbox_path": str(_AI_MAIL_DIR.parent / from_branch.lstrip('@').lower() / ".ai_mail.local"), "timestamp_format": "%Y-%m-%d %H:%M:%S" } else: @@ -605,7 +605,7 @@ def handle_inbox(args: List[str]) -> bool: console.print(f"❌ Unknown branch: {target_branch}") return False branch_path = Path(branch_info["path"]) - mailbox_path = branch_path / "ai_mail.local" + mailbox_path = branch_path / ".ai_mail.local" display_name = branch_info.get("name", target_branch) else: # Use current branch (detected from PWD) @@ -670,7 +670,7 @@ def handle_read(args: List[str]) -> bool: try: user_info = get_current_user() - # mailbox_path is ai_mail.local/, branch_path is parent + # mailbox_path is .ai_mail.local/, branch_path is parent branch_path = Path(user_info["mailbox_path"]).parent # Archive all messages @@ -801,7 +801,7 @@ def handle_close(args: List[str]) -> bool: pass # Dashboard/central update is best-effort try: from aipass.ai_mail.apps.handlers.email.purge import purge_deleted_folder - purge_deleted_folder(branch_path / "ai_mail.local") + purge_deleted_folder(branch_path / ".ai_mail.local") except Exception: pass @@ -830,7 +830,7 @@ def handle_reply(args: List[str]) -> bool: try: user_info = get_current_user() branch_path = Path(user_info["mailbox_path"]).parent - inbox_file = branch_path / "ai_mail.local" / "inbox.json" + inbox_file = branch_path / ".ai_mail.local" / "inbox.json" message_id = args[0] reply_message = args[1] diff --git a/src/aipass/api/.claude/settings.local.json b/src/aipass/api/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/api/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/api/.seed/bypass.json b/src/aipass/api/.seed/bypass.json index f60f3f56..d2d15ce2 100644 --- a/src/aipass/api/.seed/bypass.json +++ b/src/aipass/api/.seed/bypass.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "created": "2026-03-06T03:16:10.876557", + "created": "2026-03-06T14:27:31.471013", "description": "Standards bypass configuration for this branch" }, "bypass": [], diff --git a/src/aipass/api/README.md b/src/aipass/api/README.md index ef58fc72..f811abef 100644 --- a/src/aipass/api/README.md +++ b/src/aipass/api/README.md @@ -1,51 +1,78 @@ # API -**Purpose:** Model routing, provider abstraction, LLM API access layer +**Purpose:** LLM API access layer with provider abstraction, key management, model routing, and usage tracking. **Module:** `aipass.api` -**Created:** 2026-03-05 --- ## Overview ### What I Do - +- Manage API keys for LLM providers (currently OpenRouter) +- Route requests to LLM models with provider-level abstraction +- Track API usage metrics and provide statistics +- Validate credentials and test provider connectivity +- Discover available models from configured providers ### How I Work -- **Entry Point:** `apps/api.py` -- **Pattern:** Auto-discovers and routes to modules +- **Entry Point:** `apps/api.py` -- auto-discovers and routes to modules +- **Pattern:** Standard AIPass 3-tier architecture (entry point / modules / handlers) + +--- + +## CLI + +```bash +drone @api get-key # Retrieve API key for provider +drone @api validate # Validate API credentials and connection +drone @api test # Test OpenRouter connection status +drone @api models # List available models from provider +drone @api track # Track API usage metrics +drone @api stats # Display API usage statistics +drone @api --help # Full help output +``` --- ## Architecture ``` -API/ +api/ +├── __init__.py # Public API exports ├── apps/ -│ ├── api.py # Entry point -│ ├── modules/ # Business logic -│ ├── handlers/ # Implementation -│ └── plugins/ # Extensions -├── docs/ -├── tests/ -├── passport.json # Identity -├── local.json # Session history -├── observations.json # Collaboration patterns +│ ├── api.py # Entry point (module discovery, command routing) +│ ├── modules/ +│ │ ├── api_key.py # Key retrieval and validation logic +│ │ ├── openrouter_client.py # OpenRouter API client +│ │ └── usage_tracker.py # Usage metrics tracking +│ └── handlers/ +│ ├── auth/ +│ │ ├── env.py # Environment variable credential loading +│ │ └── keys.py # API key storage and retrieval +│ ├── config/ +│ │ └── provider.py # Provider configuration management +│ ├── json/ +│ │ └── json_handler.py # JSON operation logging +│ ├── openrouter/ +│ │ ├── caller.py # HTTP request execution +│ │ ├── client.py # OpenRouter client implementation +│ │ ├── models.py # Model discovery and listing +│ │ └── provision.py # Provider provisioning +│ └── usage/ +│ ├── aggregation.py # Usage data aggregation +│ ├── cleanup.py # Usage data cleanup +│ └── tracking.py # Usage event tracking └── README.md ``` --- -## Commands - -*Configure after initialization* - ---- - ## Integration Points ### Depends On - +- `aipass.prax` -- structured logging via `system_logger` +- `aipass.cli` -- Rich console output formatting ### Provides To - +- All modules -- LLM API access for any branch that needs model inference +- System-wide API key management and credential validation diff --git a/src/aipass/api/__init__.py b/src/aipass/api/__init__.py new file mode 100644 index 00000000..f2c62315 --- /dev/null +++ b/src/aipass/api/__init__.py @@ -0,0 +1 @@ +"""API - External API integrations for AIPass.""" diff --git a/src/aipass/api/apps/api.py b/src/aipass/api/apps/api.py index 906585d4..52240db6 100644 --- a/src/aipass/api/apps/api.py +++ b/src/aipass/api/apps/api.py @@ -24,7 +24,7 @@ from pathlib import Path # Standard library imports import importlib -from typing import Dict, Any, Optional, List +from typing import Any, List # AIPass infrastructure imports from aipass.prax.apps.modules.logger import system_logger as logger @@ -164,10 +164,6 @@ def print_help(): table.add_row("models", "List available models from provider") table.add_row("track", "Track API usage metrics") table.add_row("stats", "Display API usage statistics") - table.add_row("telegram start", "Start Telegram bridge service") - table.add_row("telegram stop", "Stop Telegram bridge service") - table.add_row("telegram status", "Check Telegram bridge status") - table.add_row("telegram logs", "View Telegram bridge logs") console.print(table) console.print() diff --git a/src/aipass/api/apps/handlers/__init__.py b/src/aipass/api/apps/handlers/__init__.py index f107843b..f9ca1ae2 100644 --- a/src/aipass/api/apps/handlers/__init__.py +++ b/src/aipass/api/apps/handlers/__init__.py @@ -26,7 +26,7 @@ def _find_real_caller(): def _extract_branch_name(filepath: str) -> str: """Extract branch name from a file path.""" - parts = filepath.split("/") + parts = Path(filepath).parts for i, part in enumerate(parts): if part in ("MEMORY_BANK", "seed", ".vscode"): if i + 1 < len(parts): diff --git a/src/aipass/api/apps/handlers/telegram/__init__.py b/src/aipass/api/apps/handlers/telegram/__init__.py deleted file mode 100644 index bd1534d0..00000000 --- a/src/aipass/api/apps/handlers/telegram/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: __init__.py - Telegram Handlers Package -# Date: 2026-02-03 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-03): Initial package for Telegram bridge -# ============================================= - -""" -Telegram Handlers Package - -Provides Telegram bot integration for AIPass: -- config.py: Token and configuration loading -- bridge.py: Core bot service with message handling -""" - -from aipass.api.apps.handlers.telegram.config import ( - load_telegram_config, - get_bot_token, - get_bot_username -) - -__all__ = [ - "load_telegram_config", - "get_bot_token", - "get_bot_username" -] diff --git a/src/aipass/api/apps/handlers/telegram/base_bot.py b/src/aipass/api/apps/handlers/telegram/base_bot.py deleted file mode 100644 index 61b9991d..00000000 --- a/src/aipass/api/apps/handlers/telegram/base_bot.py +++ /dev/null @@ -1,1791 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: base_bot.py - BaseBot class for Telegram multi-bot architecture -# Date: 2026-02-24 -# Version: 1.2.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.3.0 (2026-02-26): Integrate LogStreamer for system log streaming to Telegram chat -# - v1.2.0 (2026-02-24): Automated BotFather creation via Telethon, manual token fallback -# - v1.1.0 (2026-02-24): Add /create chat @branch command, /status registry info, conversation state -# - v1.0.0 (2026-02-24): Initial - stdlib-only BaseBot with polling, tmux injection, heartbeat, hooks -# -# CODE STANDARDS: -# - Uses Prax get_direct_logger() — no event pipeline (FPLAN-0382 migration) -# - Class-based: BaseBot is both runnable and inheritable -# - Imports standard commands from telegram_standards.py -# - Imports file detection from file_handler.py (no duplication) -# ============================================= - -""" -BaseBot - Foundation class for AIPass Telegram multi-bot architecture. - -Each AIPass branch gets its own dedicated Telegram bot. BaseBot is both a -runnable bot (for the base @aipass_bot) AND the template all branch bots inherit. - -Stdlib-only implementation using urllib for Telegram API. No python-telegram-bot -dependency. Follows the same polling/tmux injection pattern as direct_chat.py. - -Flow: - Patrick sends Telegram message - -> BaseBot receives it via getUpdates long-polling - -> If /command -> handle via telegram_standards, reply, return - -> If /new -> kill tmux session, reply, return - -> Else -> ensure tmux session exists (running Claude) - -> Send "Processing..." message - -> Write pending file for Stop hook coordination - -> Start heartbeat thread (updates "Processing..." with elapsed time) - -> Inject message into tmux session via send-keys - -> Claude processes and hits Stop event - -> Stop hook reads pending file, extracts response, sends to Telegram - -Usage: - bot = BaseBot( - bot_id="dev_central", - bot_token="123:ABC", - work_dir=Path.cwd(), - bot_name="AIPass Dev Central Bot", - allowed_user_ids=[7235222625], - ) - sys.exit(bot.run()) -""" - -# Infrastructure -import sys -from pathlib import Path - -# ============================================= -# IMPORTS (stdlib only) -# ============================================= - -import argparse -import atexit -import json -import os -import shutil -import signal -import subprocess -import threading -import time -import uuid -from datetime import datetime -from typing import Optional -from urllib.error import URLError -from urllib.request import Request, urlopen - -# Logging (Prax direct logger — FPLAN-0382, no event pipeline) -from aipass.prax.apps.modules.logger import get_direct_logger - -# ============================================= -# SIBLING IMPORTS -# ============================================= - -from aipass.api.apps.handlers.telegram.telegram_standards import ( # noqa: F401 - parse_command, - handle_standard_command, - STANDARD_COMMANDS, - build_welcome_text, - build_help_text, - build_status_text, - PROCESSING_MSG, -) -from aipass.api.apps.handlers.telegram.file_handler import ( - detect_file_type, - build_file_prompt, -) -from aipass.api.apps.handlers.telegram.bot_factory import ( - create_bot, - validate_branch, - validate_token, -) -from aipass.api.apps.handlers.telegram.bot_registry import ( - list_bots as registry_list_bots, - get_bot_by_branch, -) -from aipass.api.apps.handlers.telegram.botfather_client import ( - create_bot_via_botfather, - check_telethon_setup, -) -from aipass.api.apps.handlers.telegram.log_streamer import LogStreamer - - -# ============================================= -# MODULE-LEVEL CONSTANTS -# ============================================= - -def _aipass_data_dir() -> Path: - """User data directory for AIPass runtime files (config, state, pending).""" - env = os.environ.get("AIPASS_DATA_DIR") - if env: - return Path(env) - return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "aipass" - -_DATA_DIR = _aipass_data_dir() - -PENDING_DIR = _DATA_DIR / "telegram_pending" -PENDING_TTL = 3600 # 1 hour -TELEGRAM_CHAR_LIMIT = 4096 -RATE_LIMIT_MESSAGES = 5 -RATE_LIMIT_WINDOW = 60 -POLL_TIMEOUT = 30 -SEND_KEYS_DELAY = 0.5 -HEARTBEAT_INTERVAL = 30 # seconds -CLAUDE_BIN = shutil.which("claude") or "claude" -TEMP_DIR = Path("/tmp/telegram_uploads") -MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB - - -# ============================================= -# BaseBot CLASS -# ============================================= - -class BaseBot: - """ - Base Telegram bot for AIPass multi-bot architecture. - - Both a runnable bot (for the base @aipass_bot) and the template that - all branch bots inherit from. Uses stdlib urllib for Telegram API, - tmux for Claude sessions, and a heartbeat thread for progress updates. - """ - - def __init__( - self, - bot_id: str, - bot_token: str, - work_dir: Path, - bot_name: str = "AIPass Bot", - allowed_user_ids: Optional[list[int]] = None, - custom_commands: Optional[dict] = None, - branch_name: Optional[str] = None, - shared_session: Optional[str] = None, - ) -> None: - """ - Initialize BaseBot. - - Args: - bot_id: Unique identifier for this bot (e.g., "dev_central") - bot_token: Telegram bot API token - work_dir: Working directory for the tmux Claude session - bot_name: Display name shown in /start and /status - allowed_user_ids: List of Telegram user IDs allowed to use the bot. - Empty list or None means allow all. - custom_commands: Dict of bot-specific commands in telegram_standards format - branch_name: Branch name for log streaming (None = no streaming, e.g. base bot) - shared_session: tmux session name to inject into instead of creating own session. - When set, the bot attaches to an existing session (e.g., Patrick's - running Claude Code on PC). Falls back to own session if not found. - """ - self.bot_id = bot_id - self.bot_token = bot_token - self.work_dir = Path(work_dir) - self.bot_name = bot_name - self.allowed_user_ids = allowed_user_ids or [] - self.custom_commands = custom_commands or {} - # branch_name may already be set by subclass (e.g. BranchPlugin) before super().__init__ - if not hasattr(self, "branch_name"): - self.branch_name = branch_name - - self.session_name = f"telegram-{bot_id}" - self.pending_file = PENDING_DIR / f"bot-{bot_id}.json" - - # Shared-session mode: inject into an existing tmux session instead of creating own - self._shared_session_name = shared_session - self._using_shared_session = False - - self.state = { - "running": True, - "message_count": 0, - "start_time": time.time(), - "last_message_time": 0.0, - } - - self._health = { - "started_at": None, - "last_message_at": None, - "messages_received": 0, - "messages_failed": 0, - "errors": 0, - } - - self._rate_limit_tracker: dict[int, list] = {} - self._heartbeat_thread: threading.Thread | None = None - self._heartbeat_stop = threading.Event() - - # Conversation state for /create flow (keyed by chat_id) - self._create_state: dict[int, dict] = {} - self._create_state_ttl = 300 # 5 minutes - - # Log streamer (started on first message when branch_name is set) - self._log_streamer: Optional[LogStreamer] = None - self._active_chat_id: Optional[int] = None - - # Logger (Prax direct logger — no event pipeline, avoids recursion with log streamer) - self.logger = get_direct_logger() - - # Lock file - self._lock_file = _DATA_DIR / "telegram_bots" / f".{bot_id}.lock" - - # Offset file - self._offset_file = _DATA_DIR / "telegram_bots" / f"{bot_id}_offset.json" - - # ============================================= - # MAIN ENTRY POINT - # ============================================= - - def run(self) -> int: - """ - Main entry point. Start polling and process messages. - - Returns: - 0 on clean exit, 1 on error - """ - self.logger.info("=" * 60) - self.logger.info("%s starting (bot_id=%s)", self.bot_name, self.bot_id) - - # Verify connection - if not self.verify_connection(): - self.logger.error("Startup health check FAILED - cannot reach Telegram API") - return 1 - - self.logger.info("Connected to Telegram API") - self._health["started_at"] = datetime.now().isoformat() - - # Check for existing lock - if self._check_lock(): - self.logger.error("Another instance of bot-%s is already running", self.bot_id) - return 1 - - # Create lock file - self._create_lock() - - # Signal handlers - signal.signal(signal.SIGTERM, self._shutdown_handler) - signal.signal(signal.SIGINT, self._shutdown_handler) - atexit.register(self._cleanup) - - # Ensure pending directory - PENDING_DIR.mkdir(parents=True, exist_ok=True) - - # Clean stale pending file - self.clean_stale_pending() - - # Load offset - offset = self._load_offset() - self.logger.info("Starting poll loop (offset=%d)", offset) - - # Retry backoff sequence: 5s, 10s, 20s, 40s, 60s max - retry_delay = 5 - max_retry_delay = 60 - - while self.state["running"]: - try: - updates = self.poll_updates(offset) - - # Reset backoff on successful poll - retry_delay = 5 - - for update in updates: - if not self.state["running"]: - break - - self.process_update(update) - - # Advance offset - new_offset = update.get("update_id", 0) + 1 - if new_offset > offset: - offset = new_offset - self._save_offset(offset) - - except KeyboardInterrupt: - self.logger.info("KeyboardInterrupt received") - break - except Exception as e: - self._health["errors"] = self._health.get("errors", 0) + 1 - self.logger.error("Error in poll loop: %s: %s", type(e).__name__, e) - time.sleep(retry_delay) - retry_delay = min(retry_delay * 2, max_retry_delay) - - self.logger.info("Poll loop exited") - return 0 - - # ============================================= - # TELEGRAM API (stdlib urllib) - # ============================================= - - def verify_connection(self, timeout: int = 15) -> bool: - """ - Verify connection to Telegram API by calling getMe. - - Args: - timeout: Connection timeout in seconds - - Returns: - True if connection succeeded - """ - url = f"https://api.telegram.org/bot{self.bot_token}/getMe" - try: - req = Request(url) - with urlopen(req, timeout=timeout) as resp: - data = json.loads(resp.read().decode("utf-8")) - - if data.get("ok"): - bot_info = data.get("result", {}) - self.logger.info( - "Telegram API OK - @%s", bot_info.get("username", "unknown") - ) - return True - - self.logger.error( - "Telegram API rejected: %s", data.get("description", "unknown") - ) - return False - - except URLError as e: - self.logger.error("Telegram API connection failed: %s", e) - return False - except Exception as e: - self.logger.error("Telegram API health check error: %s", e) - return False - - def poll_updates(self, offset: int) -> list: - """ - Long-poll Telegram for new updates via getUpdates. - - Args: - offset: Update offset to avoid reprocessing - - Returns: - List of update dicts - """ - url = ( - f"https://api.telegram.org/bot{self.bot_token}/getUpdates" - f"?offset={offset}&timeout={POLL_TIMEOUT}" - ) - - try: - req = Request(url) - with urlopen(req, timeout=POLL_TIMEOUT + 10) as resp: - data = json.loads(resp.read().decode("utf-8")) - - if not data.get("ok"): - self.logger.error( - "Telegram API error: %s", data.get("description", "unknown") - ) - return [] - - return data.get("result", []) - - except URLError as e: - self.logger.error("Poll error: %s", e) - return [] - except Exception as e: - self.logger.error("Unexpected poll error: %s", e) - return [] - - def send_message( - self, chat_id: int, text: str, reply_to: Optional[int] = None - ) -> dict | None: - """ - Send a message via Telegram sendMessage API. - - Args: - chat_id: Target chat ID - text: Message text - reply_to: Optional message ID to reply to - - Returns: - Parsed JSON response dict (contains message_id), or None on failure - """ - url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage" - - payload: dict = { - "chat_id": chat_id, - "text": text, - } - if reply_to is not None: - payload["reply_to_message_id"] = reply_to - - for attempt in range(3): - try: - data = json.dumps(payload).encode("utf-8") - req = Request(url, data=data, headers={"Content-Type": "application/json"}) - with urlopen(req, timeout=15) as resp: - result = json.loads(resp.read().decode("utf-8")) - - if result.get("ok"): - return result.get("result") - else: - self.logger.warning( - "sendMessage failed (attempt %d): %s", - attempt + 1, - result.get("description", "unknown"), - ) - except Exception as e: - self.logger.warning( - "sendMessage error (attempt %d): %s", attempt + 1, e - ) - - if attempt < 2: - time.sleep(1.0 * (2 ** attempt)) - - self.logger.error("sendMessage failed after 3 attempts") - self._health["messages_failed"] = self._health.get("messages_failed", 0) + 1 - return None - - def edit_message(self, chat_id: int, message_id: int, text: str) -> bool: - """ - Edit a message via Telegram editMessageText API. - - Args: - chat_id: Chat ID containing the message - message_id: ID of the message to edit - text: New text for the message - - Returns: - True if edit succeeded - """ - url = f"https://api.telegram.org/bot{self.bot_token}/editMessageText" - - payload = { - "chat_id": chat_id, - "message_id": message_id, - "text": text, - } - - try: - data = json.dumps(payload).encode("utf-8") - req = Request(url, data=data, headers={"Content-Type": "application/json"}) - with urlopen(req, timeout=15) as resp: - result = json.loads(resp.read().decode("utf-8")) - - return result.get("ok", False) - - except Exception as e: - self.logger.warning("editMessageText error: %s", e) - return False - - # ============================================= - # UPDATE PROCESSING - # ============================================= - - def process_update(self, update: dict) -> None: - """ - Process a single Telegram update. - - Routes to command handling, message handling, or file handling - based on update contents. - - Args: - update: Telegram update dict - """ - message = update.get("message") - if not message: - return - - text = message.get("text", "") - chat = message.get("chat", {}) - chat_id = chat.get("id", 0) - from_user = message.get("from", {}) - user_id = from_user.get("id", 0) - username = from_user.get("username", "unknown") - _ = message.get("message_id", 0) # Available for future use - - # Start log streamer on first valid message (if branch has a name) - if self._active_chat_id is None and chat_id: - self._active_chat_id = chat_id - if self.branch_name is not None and self._log_streamer is None: - self._log_streamer = LogStreamer( - self.bot_token, chat_id, self.branch_name - ) - self._log_streamer.start() - self.logger.info("Log streamer started for branch: %s", self.branch_name) - - # Allowlist check - if not self.is_user_allowed(user_id): - self.logger.warning( - "Blocked message from unauthorized user_id: %s (@%s)", user_id, username - ) - return - - # Rate limit check - if not self.check_rate_limit(user_id): - self.logger.warning("Rate limited user_id: %s", user_id) - self.send_message( - chat_id, "Rate limit exceeded. Please wait before sending more messages." - ) - return - - # Health tracking - self._health["last_message_at"] = datetime.now().isoformat() - self._health["messages_received"] = self._health.get("messages_received", 0) + 1 - - # Check for file uploads (photo/document) - photo_list = message.get("photo") - document = message.get("document") - - if photo_list or document: - self.handle_file(chat_id, message) - return - - # Check if user is in /create flow (awaiting token paste) - if chat_id in self._create_state and text and not text.startswith("/"): - self._handle_create_token(chat_id, text) - return - - # Command handling - if text: - parsed = parse_command(text) - if parsed is not None: - cmd_name, cmd_args = parsed - - # /create command — multi-step bot creation - if cmd_name == "create": - self._handle_create_command(chat_id, cmd_args) - return - - # /cancel command — cancel active /create flow - if cmd_name == "cancel": - if chat_id in self._create_state: - del self._create_state[chat_id] - self.send_message(chat_id, "Bot creation cancelled.") - else: - self.send_message(chat_id, "Nothing to cancel.") - return - - # Compute uptime for /status - elapsed = time.time() - self.state["start_time"] - hours, remainder = divmod(int(elapsed), 3600) - minutes, seconds = divmod(remainder, 60) - uptime_str = f"{hours}h {minutes}m {seconds}s" - - # Merge custom commands from constructor and hook - merged_commands = {**self.custom_commands, **self.get_custom_commands()} - - # /status — enhance with registry info - if cmd_name == "status": - status_text = build_status_text( - session_name=self.session_name, - branch_name=self.bot_id, - uptime=uptime_str, - message_count=self.state.get("message_count"), - chat_id=chat_id, - ) - registry_text = self._build_registry_status() - if registry_text: - status_text += f"\n\n{registry_text}" - self.send_message(chat_id, status_text) - self.logger.info("Handled /status command") - return - - result = handle_standard_command( - command=cmd_name, - session_name=self.session_name, - branch_name=self.bot_id, - bot_name=self.bot_name, - custom_commands=merged_commands or None, - chat_id=chat_id, - message_count=self.state.get("message_count"), - uptime=uptime_str, - ) - - if result is not None: - if isinstance(result, tuple): - action, response_text = result - if action == "new": - self._kill_tmux_session() - self.send_message(chat_id, response_text) - self.logger.info("Handled /new command - session killed") - return - else: - self.send_message(chat_id, result) - self.logger.info("Handled /%s command", cmd_name) - return - - # Not a standard command - fall through to regular message processing - - # Regular message handling - if text: - self.handle_message(chat_id, text, message) - else: - self.logger.info("Ignoring unsupported message type") - - # ============================================= - # MESSAGE HANDLING - # ============================================= - - def handle_message(self, chat_id: int, text: str, message: dict) -> None: - """ - Handle a regular text message. - - Pre-processes via on_message hook, ensures tmux session, writes - pending file, starts heartbeat, and injects into tmux. - - Args: - chat_id: Telegram chat ID - text: Message text - message: Full message dict - """ - message_id = message.get("message_id", 0) - - # Hook: pre-process message text - prompt = self.on_message(text) - - # Track message - self.state["message_count"] = self.state.get("message_count", 0) + 1 - self.state["last_message_time"] = time.time() - - self.logger.info("Processing message (msg_id=%d)", message_id) - - # Ensure tmux session - if not self.ensure_tmux_session(): - self.logger.error("Cannot process message - tmux session unavailable") - self.send_message( - chat_id, "Failed to start Claude session. Check logs." - ) - return - - # Send processing indicator - processing_result = self.send_message(chat_id, PROCESSING_MSG) - processing_msg_id = ( - processing_result.get("message_id") if processing_result else None - ) - - # Write pending file - if not self.write_pending_file(chat_id, message_id, processing_msg_id): - self.logger.error("Failed to write pending file") - self.send_message(chat_id, "Internal error writing pending file.") - return - - # Start heartbeat - if processing_msg_id: - self._start_heartbeat(chat_id, processing_msg_id) - - # Inject into tmux - if not self.inject_message(prompt): - self.logger.error("Failed to inject message into tmux") - self._stop_heartbeat() - self.pending_file.unlink(missing_ok=True) - self.send_message( - chat_id, "Failed to send message to Claude session." - ) - return - - self.logger.info("Message processed successfully (msg_id=%d)", message_id) - - def handle_file(self, chat_id: int, message: dict) -> None: - """ - Handle file uploads (photos and documents). - - Downloads the file via Telegram API, detects type, builds prompt, - then follows the same flow as handle_message. - - Args: - chat_id: Telegram chat ID - message: Full message dict containing photo or document - """ - message_id = message.get("message_id", 0) - caption = message.get("caption", "") - photo_list = message.get("photo") - document = message.get("document") - - file_id = None - filename = None - - if photo_list: - # Use highest quality photo (last in array) - best_photo = photo_list[-1] - file_id = best_photo.get("file_id", "") - self.logger.info( - "Photo from user (file_id=%s, caption=%s)", - file_id[:20] if file_id else "none", - caption[:50] if caption else "none", - ) - elif document: - file_id = document.get("file_id", "") - filename = document.get("file_name", "") - file_size = document.get("file_size", 0) - self.logger.info("Document from user: %s (%d bytes)", filename, file_size) - - if file_size > MAX_FILE_SIZE: - self.send_message( - chat_id, - f"File too large ({file_size // 1024}KB). Max is 10MB.", - ) - return - - if not file_id: - return - - # Download file via Telegram API - file_path = self._download_file(file_id, filename) - if not file_path: - self.send_message(chat_id, "Failed to download file. Try again?") - return - - # Detect type and build prompt - file_type = detect_file_type(file_path) - prompt = build_file_prompt( - file_path, file_type, caption=caption or None, sender_name="Patrick" - ) - - # Hook: pre-process - prompt = self.on_message(prompt) - - # Track message - self.state["message_count"] = self.state.get("message_count", 0) + 1 - self.state["last_message_time"] = time.time() - - # Ensure tmux session - if not self.ensure_tmux_session(): - self.logger.error("Cannot process file - tmux session unavailable") - self.send_message(chat_id, "Failed to start Claude session. Check logs.") - if file_type == "text": - file_path.unlink(missing_ok=True) - return - - # Send processing indicator - processing_result = self.send_message( - chat_id, f"Processing {file_type} file..." - ) - processing_msg_id = ( - processing_result.get("message_id") if processing_result else None - ) - - # Write pending file - if not self.write_pending_file(chat_id, message_id, processing_msg_id): - self.logger.error("Failed to write pending file for file upload") - self.send_message(chat_id, "Internal error writing pending file.") - return - - # Clean up text files immediately (content is inline in prompt) - if file_type == "text": - file_path.unlink(missing_ok=True) - - # Start heartbeat - if processing_msg_id: - self._start_heartbeat(chat_id, processing_msg_id) - - # Inject into tmux - if not self.inject_message(prompt): - self.logger.error("Failed to inject file message into tmux") - self._stop_heartbeat() - self.pending_file.unlink(missing_ok=True) - self.send_message(chat_id, "Failed to send file to Claude session.") - return - - self.logger.info("File processed successfully (msg_id=%d)", message_id) - - def _download_file( - self, file_id: str, filename: Optional[str] = None - ) -> Optional[Path]: - """ - Download a file from Telegram via getFile API + urllib. - - Args: - file_id: Telegram file_id from the message - filename: Optional original filename - - Returns: - Path to the downloaded file, or None on failure - """ - # Step 1: Get file info - url = f"https://api.telegram.org/bot{self.bot_token}/getFile?file_id={file_id}" - try: - with urlopen(Request(url), timeout=15) as resp: - data = json.loads(resp.read().decode("utf-8")) - except Exception as e: - self.logger.error("getFile API failed: %s", e) - return None - - if not data.get("ok"): - self.logger.error( - "getFile error: %s", data.get("description", "unknown") - ) - return None - - file_info = data.get("result", {}) - file_path_remote = file_info.get("file_path", "") - file_size = file_info.get("file_size", 0) - - if not file_path_remote: - self.logger.error("No file_path in getFile response") - return None - - if file_size > MAX_FILE_SIZE: - self.logger.warning( - "File too large: %d bytes (max %d)", file_size, MAX_FILE_SIZE - ) - return None - - # Step 2: Download - download_url = ( - f"https://api.telegram.org/file/bot{self.bot_token}/{file_path_remote}" - ) - - TEMP_DIR.mkdir(parents=True, exist_ok=True) - - if filename: - safe_name = "".join( - c if c.isalnum() or c in ".-_" else "_" for c in Path(filename).name - ) - else: - ext = Path(file_path_remote).suffix or ".jpg" - safe_name = f"{uuid.uuid4()}{ext}" - - dest = TEMP_DIR / safe_name - - try: - with urlopen(Request(download_url), timeout=30) as resp: - dest.write_bytes(resp.read()) - self.logger.info("Downloaded file to %s (%d bytes)", dest, file_size) - return dest - except Exception as e: - self.logger.error("File download failed: %s", e) - return None - - # ============================================= - # /CREATE CHAT COMMAND - # ============================================= - - def _handle_create_command(self, chat_id: int, args: str) -> None: - """ - Handle /create chat @branch — automated or manual bot creation. - - If Telethon is configured, creates the bot via BotFather automatically. - Otherwise, falls back to the manual token-paste flow. - - Args: - chat_id: Telegram chat ID - args: Command arguments (e.g., "chat dev_central" or "chat @dev_central") - """ - # Parse args: /create chat - parts = args.strip().split() - - if len(parts) < 2 or parts[0].lower() != "chat": - self.send_message( - chat_id, - "Usage: /create chat \n\n" - "Example: /create chat dev_central", - ) - return - - branch_name = parts[1].lstrip("@").lower() - - # Validate branch exists in BRANCH_REGISTRY.json - branch_info = validate_branch(branch_name) - if not branch_info: - self.send_message( - chat_id, - f"Branch '@{branch_name}' not found in BRANCH_REGISTRY.json.\n\n" - "Check available branches and try again.", - ) - return - - # Check if branch already has a bot - existing = get_bot_by_branch(branch_name) - if existing: - self.send_message( - chat_id, - f"Branch '@{branch_name}' already has a bot: " - f"@{existing.get('username', '?')} (bot_id={existing.get('bot_id')})", - ) - return - - branch_path = branch_info.get("path", "") - - # Check if Telethon automation is available - telethon_ready, telethon_reason = check_telethon_setup() - - if telethon_ready: - # Automated flow — create bot via BotFather + register in one step - self._handle_create_automated(chat_id, branch_name, branch_path) - else: - # Manual fallback — ask user to paste a BotFather token - self.logger.info( - "Telethon not ready (%s), falling back to manual token flow", - telethon_reason, - ) - self._create_state[chat_id] = { - "branch_name": branch_name, - "branch_path": branch_path, - "started_at": time.time(), - } - self.send_message( - chat_id, - f"Branch @{branch_name} found at {branch_path}.\n\n" - "Now paste the BotFather token for the new bot.\n" - "(Get one from @BotFather -> /newbot)\n\n" - "/cancel to abort.", - ) - self.logger.info( - "/create chat: branch @%s validated, awaiting token from chat %d", - branch_name, chat_id, - ) - - def _handle_create_automated(self, chat_id: int, branch_name: str, branch_path: str) -> None: - """ - Fully automated bot creation via Telethon BotFather client. - - Creates the bot with @BotFather, then registers it via bot_factory. - - Args: - chat_id: Telegram chat ID - branch_name: Branch name (e.g., "dev_central") - branch_path: Branch working directory path - """ - self.send_message( - chat_id, - f"Creating bot for @{branch_name} via BotFather...\n" - "This takes a few seconds.", - ) - - # Step 1: Create bot via BotFather automation - bf_result = create_bot_via_botfather(branch_name) - if not bf_result: - self.send_message( - chat_id, - f"BotFather automation failed for @{branch_name}.\n" - "Check system logs. You can retry or use manual token mode:\n" - "Paste a BotFather token to create manually.", - ) - # Fall back to manual mode - self._create_state[chat_id] = { - "branch_name": branch_name, - "branch_path": branch_path, - "started_at": time.time(), - } - return - - bot_token = bf_result["token"] - bot_username = bf_result["username"] - display_name = bf_result["display_name"] - - self.logger.info( - "BotFather created @%s for branch @%s, registering...", - bot_username, branch_name, - ) - - # Step 2: Register via bot_factory (validate, write config, registry, systemd) - result = create_bot( - bot_id=branch_name, - bot_token=bot_token, - branch_name=branch_name, - work_dir=branch_path, - bot_name=display_name, - allowed_user_ids=self.allowed_user_ids, - ) - - if not result: - self.send_message( - chat_id, - f"Bot @{bot_username} was created in BotFather but registration failed.\n" - f"Token: (check system logs)\n" - "Run /create chat again or register manually.", - ) - return - - auto_started = result.get("auto_started", False) - status_line = "Bot is running!" if auto_started else ( - f"Start it with:\nsystemctl --user start telegram-bot@{branch_name}" - ) - - self.send_message( - chat_id, - f"Bot created for @{branch_name}!\n\n" - f"Username: @{bot_username}\n" - f"Display name: {display_name}\n" - f"Bot ID: {branch_name}\n" - f"Work dir: {branch_path}\n" - f"Service: telegram-bot@{branch_name}\n\n" - f"{status_line}", - ) - - self.logger.info( - "/create: bot @%s created automatically for branch @%s (started=%s)", - bot_username, branch_name, auto_started, - ) - - def _handle_create_token(self, chat_id: int, text: str) -> None: - """ - Handle token paste — step 2: validate token and create bot. - - Args: - chat_id: Telegram chat ID - text: The token text pasted by the user - """ - state = self._create_state.get(chat_id) - if not state: - return - - # Check state TTL - if time.time() - state.get("started_at", 0) > self._create_state_ttl: - del self._create_state[chat_id] - self.send_message(chat_id, "Create session expired. Start again with /create chat .") - return - - branch_name = state["branch_name"] - branch_path = state["branch_path"] - bot_token = text.strip() - - # Basic token format check - if ":" not in bot_token or len(bot_token) < 20: - self.send_message( - chat_id, - "That doesn't look like a valid bot token.\n" - "Format: 123456789:ABCdefGHIjklMNO_pqr\n\n" - "Paste the token from @BotFather, or /cancel to abort.", - ) - return - - # Clean up state before the potentially slow API calls - del self._create_state[chat_id] - - self.send_message(chat_id, f"Validating token and creating @{branch_name} bot...") - - # Validate the token via Telegram getMe - bot_info = validate_token(bot_token) - if not bot_info: - self.send_message( - chat_id, - "Token validation failed. The token may be invalid or expired.\n" - "Get a fresh token from @BotFather and try /create chat again.", - ) - return - - bot_username = bot_info.get("username", "unknown") - - # Create the bot via bot_factory - result = create_bot( - bot_id=branch_name, - bot_token=bot_token, - branch_name=branch_name, - work_dir=branch_path, - allowed_user_ids=self.allowed_user_ids, - ) - - if not result: - self.send_message( - chat_id, - f"Bot creation failed for @{branch_name}. Check system logs.", - ) - return - - self.send_message( - chat_id, - f"Bot created for @{branch_name}!\n\n" - f"Username: @{bot_username}\n" - f"Bot ID: {branch_name}\n" - f"Work dir: {branch_path}\n" - f"Service: telegram-bot@{branch_name}\n\n" - f"Start it with:\n" - f"systemctl --user start telegram-bot@{branch_name}", - ) - - self.logger.info( - "/create: bot @%s created for branch @%s", - bot_username, branch_name, - ) - - def _build_registry_status(self) -> str: - """ - Build registry info string for /status display. - - Returns: - Formatted string showing registered bots, or empty string if none. - """ - try: - bots = registry_list_bots() - except Exception: - return "" - - if not bots: - return "Registered Bots: none" - - lines = [f"Registered Bots: {len(bots)}"] - for bot in bots: - bot_id = bot.get("bot_id", "?") - username = bot.get("username", "?") - status = bot.get("status", "?") - branch = bot.get("branch_name") or "base" - lines.append(f" {bot_id} (@{username}) - {branch} - {status}") - - return "\n".join(lines) - - # ============================================= - # TEXT CHUNKING - # ============================================= - - def chunk_text(self, text: str, limit: int = TELEGRAM_CHAR_LIMIT) -> list[str]: - """ - Split text into chunks for Telegram's message character limit. - - Uses smart breaking: tries sentence boundaries, then paragraphs, - then newlines, then spaces, and finally hard breaks. - - Args: - text: The full text to chunk - limit: Maximum characters per chunk (default 4096) - - Returns: - List of text chunks, each within the limit - """ - if len(text) <= limit: - return [text] - - chunks: list[str] = [] - remaining = text - - while remaining: - if len(remaining) <= limit: - chunks.append(remaining) - break - - chunk = remaining[:limit] - - # Try to break at sentence boundary - best_break = -1 - for i in range(len(chunk) - 1, max(0, len(chunk) - 500), -1): - if chunk[i] in ".!?" and ( - i + 1 >= len(chunk) or chunk[i + 1] in " \n" - ): - best_break = i + 1 - break - - # Try double newline - if best_break == -1: - newline_pos = chunk.rfind("\n\n") - if newline_pos > limit // 2: - best_break = newline_pos + 2 - - # Try single newline - if best_break == -1: - newline_pos = chunk.rfind("\n") - if newline_pos > limit // 2: - best_break = newline_pos + 1 - - # Try space - if best_break == -1: - space_pos = chunk.rfind(" ") - if space_pos > limit // 2: - best_break = space_pos + 1 - - # Hard break - if best_break == -1: - best_break = limit - - chunks.append(remaining[:best_break].rstrip()) - remaining = remaining[best_break:].lstrip() - - return chunks - - # ============================================= - # SECURITY - # ============================================= - - def is_user_allowed(self, user_id: int) -> bool: - """ - Check if a user ID is in the allowlist. - - Args: - user_id: Telegram user ID - - Returns: - True if allowed (or allowlist empty) - """ - if not self.allowed_user_ids: - return True - return user_id in self.allowed_user_ids - - def check_rate_limit(self, user_id: int) -> bool: - """ - Check if user is within rate limits using a sliding window. - - Args: - user_id: Telegram user ID - - Returns: - True if within limits, False if rate limited - """ - current_time = time.time() - - if user_id not in self._rate_limit_tracker: - self._rate_limit_tracker[user_id] = [] - - # Prune old timestamps - self._rate_limit_tracker[user_id] = [ - ts - for ts in self._rate_limit_tracker[user_id] - if current_time - ts < RATE_LIMIT_WINDOW - ] - - if len(self._rate_limit_tracker[user_id]) >= RATE_LIMIT_MESSAGES: - return False - - self._rate_limit_tracker[user_id].append(current_time) - return True - - # ============================================= - # TMUX SESSION MANAGEMENT - # ============================================= - - def ensure_tmux_session(self) -> bool: - """ - Ensure a tmux session is available for message injection. - - In shared-session mode: attaches to an existing tmux session (e.g., - Patrick's running Claude Code on PC). Falls back to own session if - the shared session is not found. - - In normal mode: creates telegram-{bot_id} session with Claude Code. - - Returns: - True if session is ready - """ - # Shared-session mode: attach to existing session if available - if self._shared_session_name: - try: - result = subprocess.run( - ["tmux", "has-session", "-t", self._shared_session_name], - capture_output=True, - ) - if result.returncode == 0: - self.session_name = self._shared_session_name - self._using_shared_session = True - self.logger.info( - "Shared session '%s' found — injecting into existing session", - self._shared_session_name, - ) - return True - else: - self._using_shared_session = False - self.session_name = f"telegram-{self.bot_id}" - self.logger.warning( - "Shared session '%s' not found — falling back to own session", - self._shared_session_name, - ) - except FileNotFoundError: - self._using_shared_session = False - self.session_name = f"telegram-{self.bot_id}" - - if self._tmux_session_exists(): - return True - - # Validate work_dir exists — tmux silently falls back to HOME on bad paths - if not self.work_dir.is_dir(): - self.logger.error( - "work_dir does not exist: %s — refusing to create tmux session", self.work_dir - ) - return False - - self.logger.info( - "Creating tmux session '%s' at %s", self.session_name, self.work_dir - ) - - try: - # env -u CLAUDECODE prevents "cannot run inside another Claude" error - env = os.environ.copy() - env.pop("CLAUDECODE", None) - - subprocess.run( - [ - "tmux", "new-session", "-d", - "-s", self.session_name, - "-c", str(self.work_dir), - ], - check=True, - capture_output=True, - env=env, - ) - - # Set AIPASS_BOT_ID environment variable in the tmux session - subprocess.run( - [ - "tmux", "send-keys", "-t", self.session_name, - f"export AIPASS_BOT_ID={self.bot_id}", "Enter", - ], - capture_output=True, - ) - time.sleep(0.3) - - # Launch Claude - claude_cmd = f"{CLAUDE_BIN} --permission-mode bypassPermissions" - subprocess.run( - [ - "tmux", "send-keys", "-t", self.session_name, - claude_cmd, "Enter", - ], - capture_output=True, - ) - - self.logger.info( - "tmux session created, waiting 5s for Claude to initialize..." - ) - time.sleep(5) - - # Hook: post-creation - self.on_session_create(self.session_name, self.work_dir) - - return True - - except subprocess.CalledProcessError as e: - self.logger.error( - "Failed to create tmux session: %s", - e.stderr.decode() if e.stderr else str(e), - ) - return False - except FileNotFoundError: - self.logger.error("tmux not found - is it installed?") - return False - - def inject_message(self, text: str) -> bool: - """ - Inject a message into the tmux session via send-keys. - - Uses -l flag for literal text (no shell interpretation), - followed by Enter to submit. - - Args: - text: The message text to inject - - Returns: - True if injection succeeded - """ - try: - subprocess.run( - ["tmux", "send-keys", "-t", self.session_name, "-l", text], - check=True, - capture_output=True, - ) - time.sleep(SEND_KEYS_DELAY) - subprocess.run( - ["tmux", "send-keys", "-t", self.session_name, "Enter"], - check=True, - capture_output=True, - ) - self.logger.info("Message injected into tmux session") - return True - except subprocess.CalledProcessError as e: - self.logger.error( - "Failed to inject message: %s", - e.stderr.decode() if e.stderr else str(e), - ) - return False - - def _tmux_session_exists(self) -> bool: - """Check if the tmux session exists.""" - try: - result = subprocess.run( - ["tmux", "has-session", "-t", self.session_name], - capture_output=True, - ) - return result.returncode == 0 - except FileNotFoundError: - return False - - def _kill_tmux_session(self) -> bool: - """Kill the tmux session. Protects shared sessions from being killed.""" - # Shared-session protection: never kill a session we don't own - if self._using_shared_session: - self.logger.info( - "Shared session '%s' — detaching instead of killing", - self.session_name, - ) - self._using_shared_session = False - self.session_name = f"telegram-{self.bot_id}" - return True - - if not self._tmux_session_exists(): - self.logger.info( - "tmux session '%s' not running, nothing to kill", self.session_name - ) - return True - - try: - subprocess.run( - ["tmux", "kill-session", "-t", self.session_name], - check=True, - capture_output=True, - ) - self.logger.info("Killed tmux session '%s'", self.session_name) - return True - except subprocess.CalledProcessError as e: - self.logger.error( - "Failed to kill tmux session '%s': %s", - self.session_name, - e.stderr.decode() if e.stderr else str(e), - ) - return False - - # ============================================= - # PENDING FILE MANAGEMENT - # ============================================= - - def write_pending_file( - self, chat_id: int, message_id: int, processing_message_id: Optional[int] = None - ) -> bool: - """ - Write the pending file for Stop hook coordination. - - Args: - chat_id: Telegram chat ID - message_id: Original message's Telegram message ID - processing_message_id: ID of the "Processing..." message to edit - - Returns: - True if written successfully - """ - PENDING_DIR.mkdir(parents=True, exist_ok=True) - - transcript_line_after = self._get_transcript_line_count() - - pending_data = { - "chat_id": chat_id, - "message_id": message_id, - "bot_token": self.bot_token, - "bot_id": self.bot_id, - "work_dir": str(self.work_dir), - "session_name": self.session_name, - "processing_message_id": processing_message_id, - "timestamp": time.time(), - "transcript_line_after": transcript_line_after, - } - - try: - self.pending_file.write_text( - json.dumps(pending_data, indent=2), - encoding="utf-8", - ) - self.logger.info("Pending file written for message %d", message_id) - return True - except OSError as e: - self.logger.error("Failed to write pending file: %s", e) - return False - - def clean_stale_pending(self) -> None: - """Remove stale pending file if older than PENDING_TTL and tmux session dead.""" - if not self.pending_file.exists(): - return - try: - age = time.time() - self.pending_file.stat().st_mtime - if age > PENDING_TTL and not self._tmux_session_exists(): - self.pending_file.unlink() - self.logger.info("Cleaned stale pending file (%.0fs old)", age) - except OSError as e: - self.logger.warning("Failed to clean stale pending file: %s", e) - - def _get_transcript_line_count(self) -> int: - """ - Count lines in the Claude JSONL transcript for Layer 3 position tracking. - - Returns: - Line count of the JSONL transcript, or 0 if unavailable - """ - slug = str(self.work_dir).replace("/", "-") - # Look for transcript files matching the session pattern - # Claude Code stores transcripts under ~/.claude/projects/ - claude_home = Path(os.environ.get("CLAUDE_HOME", Path.home() / ".claude")) - projects_dir = claude_home / "projects" / slug - if not projects_dir.exists(): - return 0 - - # Find the most recent JSONL transcript - jsonl_files = sorted(projects_dir.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True) - if not jsonl_files: - return 0 - - try: - text = jsonl_files[0].read_text(encoding="utf-8").strip() - return len(text.split("\n")) if text else 0 - except OSError as e: - self.logger.warning("Could not read transcript for line count: %s", e) - return 0 - - # ============================================= - # HEARTBEAT THREAD - # ============================================= - - def _start_heartbeat(self, chat_id: int, processing_msg_id: int) -> None: - """ - Start a background thread that updates the "Processing..." message - with elapsed time. - - Args: - chat_id: Chat ID where the processing message was sent - processing_msg_id: Message ID of the "Processing..." message - """ - self._stop_heartbeat() # Ensure no stale thread - self._heartbeat_stop.clear() - - def _heartbeat_loop(): - start = time.time() - while not self._heartbeat_stop.is_set(): - self._heartbeat_stop.wait(HEARTBEAT_INTERVAL) - if self._heartbeat_stop.is_set(): - break - - # Only update if pending file still exists and tmux alive - if not self.pending_file.exists(): - break - if not self._tmux_session_exists(): - break - - elapsed = time.time() - start - elapsed_str = self._format_elapsed(elapsed) - self.edit_message( - chat_id, processing_msg_id, f"Processing... ({elapsed_str})" - ) - - self._heartbeat_thread = threading.Thread( - target=_heartbeat_loop, daemon=True, name=f"heartbeat-{self.bot_id}" - ) - self._heartbeat_thread.start() - - def _stop_heartbeat(self) -> None: - """Signal the heartbeat thread to stop and wait for it.""" - self._heartbeat_stop.set() - if self._heartbeat_thread is not None and self._heartbeat_thread.is_alive(): - self._heartbeat_thread.join(timeout=5) - self._heartbeat_thread = None - - @staticmethod - def _format_elapsed(seconds: float) -> str: - """ - Format elapsed seconds as human-readable string. - - Args: - seconds: Elapsed time in seconds - - Returns: - Formatted string like "30s", "1m 0s", "2m 30s" - """ - total = int(seconds) - if total < 60: - return f"{total}s" - minutes, secs = divmod(total, 60) - return f"{minutes}m {secs}s" - - # ============================================= - # OVERRIDABLE HOOKS - # ============================================= - - def on_message(self, text: str) -> str: - """ - Hook: pre-process message text before tmux injection. - - Override in subclasses to modify the prompt sent to Claude. - - Args: - text: Raw message text - - Returns: - Processed text to inject into tmux - """ - return text - - def on_response(self, text: str) -> str: - """ - Hook: post-process response text before sending to Telegram. - - Override in subclasses to modify Claude's response. - - Args: - text: Raw response text from Claude - - Returns: - Processed text to send to Telegram - """ - return text - - def on_session_create(self, session_name: str, work_dir: Path) -> None: - """ - Hook: called after a new tmux session is created. - - Override in subclasses to perform post-creation setup - (e.g., injecting "hi" to trigger startup protocol). - - Args: - session_name: The tmux session name that was created - work_dir: The working directory of the session - """ - pass - - def get_custom_commands(self) -> dict: - """ - Hook: return additional bot-specific commands. - - Override in subclasses to add custom commands to /help and /start. - Base implementation includes /create and /cancel for bot management. - - Returns: - Dict of commands in telegram_standards format - """ - return { - "create": { - "description": "Create a new branch bot: /create chat ", - "menu_text": "Create branch bot", - }, - "cancel": { - "description": "Cancel active /create flow", - "menu_text": "Cancel", - }, - } - - # ============================================= - # LOCK FILE MANAGEMENT - # ============================================= - - def _create_lock(self) -> None: - """Write PID to lock file.""" - self._lock_file.parent.mkdir(parents=True, exist_ok=True) - try: - self._lock_file.write_text( - json.dumps({ - "pid": os.getpid(), - "started": datetime.now().isoformat(), - "session": self.session_name, - "bot_id": self.bot_id, - }), - encoding="utf-8", - ) - self.logger.info("Lock file created: %s", self._lock_file) - except OSError as e: - self.logger.error("Failed to create lock file: %s", e) - - def _remove_lock(self) -> None: - """Delete the lock file.""" - try: - if self._lock_file.exists(): - self._lock_file.unlink() - self.logger.info("Lock file removed") - except OSError as e: - self.logger.error("Failed to remove lock file: %s", e) - - def _check_lock(self) -> bool: - """ - Check if another instance of this bot is running. - - Verifies both PID liveness AND that the process is actually this bot. - Handles PID reuse: if the PID is alive but belongs to a different - process, the lock is treated as stale and cleaned. - - Returns: - True if another live instance holds the lock, False otherwise - """ - if not self._lock_file.exists(): - return False - - try: - lock_data = json.loads( - self._lock_file.read_text(encoding="utf-8") - ) - pid = lock_data.get("pid", 0) - - if pid: - try: - os.kill(pid, 0) # Signal 0 = check existence - except OSError: - self.logger.info("Cleaning stale lock (PID %d is dead)", pid) - self._lock_file.unlink(missing_ok=True) - return False - - # PID is alive — verify it's actually this bot (not PID reuse) - try: - cmdline = Path(f"/proc/{pid}/cmdline").read_bytes() - cmd_str = cmdline.decode("utf-8", errors="replace").replace("\x00", " ") - if f"--bot-id {self.bot_id}" not in cmd_str: - self.logger.info( - "Cleaning stale lock (PID %d is alive but not bot-%s)", - pid, self.bot_id, - ) - self._lock_file.unlink(missing_ok=True) - return False - except OSError: - pass # /proc not available, trust PID check - - return True # PID alive and belongs to this bot - - except (json.JSONDecodeError, OSError): - self._lock_file.unlink(missing_ok=True) - - return False - - # ============================================= - # SIGNAL HANDLING - # ============================================= - - def _shutdown_handler(self, signum, _frame) -> None: - """Handle SIGTERM/SIGINT for clean shutdown.""" - sig_name = ( - signal.Signals(signum).name if hasattr(signal, "Signals") else str(signum) - ) - self.logger.info("Received %s, shutting down...", sig_name) - self.state["running"] = False - - def _cleanup(self) -> None: - """Clean up resources on exit.""" - if self._log_streamer is not None: - self._log_streamer.stop() - self._log_streamer = None - self._stop_heartbeat() - self._remove_lock() - self.logger.info("Bot stopped") - - # ============================================= - # OFFSET PERSISTENCE - # ============================================= - - def _load_offset(self) -> int: - """Load the last processed update offset from disk.""" - if not self._offset_file.exists(): - return 0 - try: - with open(self._offset_file, "r", encoding="utf-8") as f: - data = json.load(f) - return data.get("offset", 0) - except (json.JSONDecodeError, OSError): - return 0 - - def _save_offset(self, offset: int) -> None: - """Persist the current update offset to disk.""" - self._offset_file.parent.mkdir(parents=True, exist_ok=True) - try: - with open(self._offset_file, "w", encoding="utf-8") as f: - json.dump( - {"offset": offset, "updated": datetime.now().isoformat()}, f - ) - except OSError as e: - self.logger.error("Failed to save offset: %s", e) - - -# ============================================= -# CLI ENTRY POINT -# ============================================= - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="AIPass Telegram Bot") - parser.add_argument("--bot-id", required=True, help="Bot identifier") - parser.add_argument("--config", help="Path to bot config JSON") - args = parser.parse_args() - - # Load config from data dir telegram_bots/{bot_id}.json or --config path - config_path = ( - Path(args.config) - if args.config - else _DATA_DIR / "telegram_bots" / f"{args.bot_id}.json" - ) - - with open(config_path, "r", encoding="utf-8") as f: - config = json.load(f) - - bot = BaseBot( - bot_id=args.bot_id, - bot_token=config["bot_token"], - work_dir=Path(config.get("work_dir", str(Path.cwd()))), - bot_name=config.get("bot_name", "AIPass Bot"), - allowed_user_ids=config.get("allowed_user_ids", []), - branch_name=config.get("branch_name"), - shared_session=config.get("shared_session"), - ) - sys.exit(bot.run()) diff --git a/src/aipass/api/apps/handlers/telegram/bot_factory.py b/src/aipass/api/apps/handlers/telegram/bot_factory.py deleted file mode 100644 index 9b472fc3..00000000 --- a/src/aipass/api/apps/handlers/telegram/bot_factory.py +++ /dev/null @@ -1,555 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: bot_factory.py - Bot creation and deletion factory -# Date: 2026-02-24 -# Version: 1.2.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.2.0 (2026-02-25): Fix: create_bot always uses registry path when branch_name provided -# - v1.1.0 (2026-02-24): Auto-start bot process after create_bot via Popen fire-and-forget -# - v1.0.0 (2026-02-24): Initial - bot lifecycle management (create, delete, validate, systemd) -# -# CODE STANDARDS: -# - Pure functions with proper error handling (graceful - never raise) -# - Uses Prax system_logger (FPLAN-0382 migration) -# - Stdlib only (urllib for HTTP, subprocess for systemd) -# ============================================= - -""" -Bot Creation and Deletion Factory - -Manages the full lifecycle of Telegram bots in the multi-bot architecture: -- Validate bot tokens via Telegram getMe API -- Validate branch existence against BRANCH_REGISTRY.json -- Write per-bot config files to ~/.aipass/telegram_bots/{bot_id}.json -- Register/deregister bots in the central bot registry -- Set BotFather commands via setMyCommands API -- Enable/disable/stop systemd user services - -All HTTP calls use urllib (stdlib). No external dependencies. -""" - -# Infrastructure -import sys -from pathlib import Path - -# Standard library -import json -import os -import shutil -import subprocess -from datetime import datetime, timezone -from typing import Optional -from urllib.request import urlopen, Request -from urllib.error import URLError, HTTPError - -# Logging (Prax system_logger — FPLAN-0382) -from aipass.prax.apps.modules.logger import system_logger as logger - -# Internal imports -from aipass.api.apps.handlers.telegram.bot_registry import ( - ensure_registry, register_bot, deregister_bot, get_bot, get_bot_by_branch -) - -# ============================================= -# CONSTANTS -# ============================================= - -TELEGRAM_API = "https://api.telegram.org/bot{token}" - -def _aipass_data_dir() -> Path: - """User data directory for AIPass runtime files.""" - env = os.environ.get("AIPASS_DATA_DIR") - if env: - return Path(env) - return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "aipass" - -def _find_repo_root() -> Path: - """Walk up from this file to find AIPASS_REGISTRY.json (repo root).""" - current = Path(__file__).resolve().parent - for parent in [current] + list(current.parents): - if (parent / "AIPASS_REGISTRY.json").exists(): - return parent - return Path.cwd() - -_DATA_DIR = _aipass_data_dir() -BOT_CONFIG_DIR = _DATA_DIR / "telegram_bots" -BRANCH_REGISTRY = _find_repo_root() / "AIPASS_REGISTRY.json" -SYSTEMD_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "systemd" / "user" - -# Default commands set on every new bot via BotFather -DEFAULT_BOT_COMMANDS = [ - {"command": "start", "description": "Start the bot"}, - {"command": "help", "description": "Show available commands"}, - {"command": "status", "description": "Show session status"}, - {"command": "new", "description": "Start a fresh session"}, -] - -# ============================================= -# TELEGRAM API HELPERS -# ============================================= - - -def validate_token(bot_token: str) -> Optional[dict]: - """ - Validate a bot token via the Telegram getMe API call. - - Args: - bot_token: Telegram bot token string (e.g., "123456:ABC-DEF..."). - - Returns: - Bot info dict with keys like "id", "username", "first_name" on success. - None if the token is invalid or the API is unreachable. - """ - url = f"{TELEGRAM_API.format(token=bot_token)}/getMe" - - try: - req = Request(url, method="GET") - with urlopen(req, timeout=15) as resp: - result = json.loads(resp.read().decode("utf-8")) - - if result.get("ok") and result.get("result"): - bot_info = result["result"] - logger.info("Token validated: @%s (id=%s)", bot_info.get("username"), bot_info.get("id")) - return bot_info - - logger.warning("Token validation failed: API returned ok=false") - return None - - except HTTPError as e: - logger.warning("Token validation HTTP error %d: %s", e.code, e.reason) - return None - except URLError as e: - logger.warning("Token validation network error: %s", e) - return None - except Exception as e: - logger.warning("Token validation unexpected error: %s", e) - return None - - -def validate_branch(branch_name: str) -> Optional[dict]: - """ - Check that a branch exists in BRANCH_REGISTRY.json. - - Args: - branch_name: Branch name to look up (case-insensitive, matches email field). - - Returns: - Branch info dict from the registry on success, None if not found. - """ - try: - if not BRANCH_REGISTRY.exists(): - logger.warning("Branch registry not found: %s", BRANCH_REGISTRY) - return None - - with open(BRANCH_REGISTRY, "r", encoding="utf-8") as f: - registry = json.load(f) - - branches = registry.get("branches", []) - target = branch_name.lower() - - for branch_entry in branches: - clean_email = branch_entry.get("email", "").replace("@", "").lower() - if clean_email == target: - logger.info("Branch validated: %s -> %s", branch_name, branch_entry.get("path")) - return branch_entry - - logger.warning("Branch '%s' not found in registry", branch_name) - return None - - except (json.JSONDecodeError, OSError) as e: - logger.warning("Failed to read branch registry: %s", e) - return None - - -def set_bot_commands(bot_token: str, commands: list[dict]) -> bool: - """ - Set BotFather commands via the Telegram setMyCommands API. - - Args: - bot_token: Telegram bot token. - commands: List of command dicts, each with "command" and "description" keys. - - Returns: - True if commands were set successfully, False otherwise. - """ - url = f"{TELEGRAM_API.format(token=bot_token)}/setMyCommands" - - payload = {"commands": commands} - data = json.dumps(payload).encode("utf-8") - req = Request(url, data=data, headers={"Content-Type": "application/json"}) - - try: - with urlopen(req, timeout=15) as resp: - result = json.loads(resp.read().decode("utf-8")) - if result.get("ok"): - logger.info("Bot commands set successfully (%d commands)", len(commands)) - return True - logger.warning("setMyCommands failed: %s", result.get("description")) - return False - - except (HTTPError, URLError) as e: - logger.warning("Failed to set bot commands: %s", e) - return False - except Exception as e: - logger.warning("Unexpected error setting bot commands: %s", e) - return False - - -# ============================================= -# SYSTEMD SERVICE MANAGEMENT -# ============================================= - - -def enable_service(bot_id: str) -> bool: - """ - Enable the systemd user service for a bot (does not start it). - - Runs: systemctl --user enable telegram-bot@{bot_id} - - Args: - bot_id: Bot identifier used in the service template. - - Returns: - True if the service was enabled successfully, False otherwise. - """ - SERVICE_NAME = f"telegram-bot@{bot_id}" - try: - result = subprocess.run( - ["systemctl", "--user", "enable", SERVICE_NAME], - capture_output=True, - text=True, - timeout=10, - ) - if result.returncode == 0: - logger.info("Enabled systemd service: %s", SERVICE_NAME) - return True - - logger.warning("Failed to enable service %s: %s", SERVICE_NAME, result.stderr.strip()) - return False - - except subprocess.TimeoutExpired: - logger.warning("Timeout enabling service: %s", SERVICE_NAME) - return False - except OSError as e: - logger.warning("Error enabling service %s: %s", SERVICE_NAME, e) - return False - - -def disable_service(bot_id: str) -> bool: - """ - Disable the systemd user service for a bot. - - Runs: systemctl --user disable telegram-bot@{bot_id} - - Args: - bot_id: Bot identifier used in the service template. - - Returns: - True if the service was disabled successfully, False otherwise. - """ - SERVICE_NAME = f"telegram-bot@{bot_id}" - try: - result = subprocess.run( - ["systemctl", "--user", "disable", SERVICE_NAME], - capture_output=True, - text=True, - timeout=10, - ) - if result.returncode == 0: - logger.info("Disabled systemd service: %s", SERVICE_NAME) - return True - - logger.warning("Failed to disable service %s: %s", SERVICE_NAME, result.stderr.strip()) - return False - - except subprocess.TimeoutExpired: - logger.warning("Timeout disabling service: %s", SERVICE_NAME) - return False - except OSError as e: - logger.warning("Error disabling service %s: %s", SERVICE_NAME, e) - return False - - -def start_bot_process(bot_id: str) -> bool: - """ - Launch the bot process via subprocess.Popen (fire-and-forget). - - Starts base_bot.py --bot-id {bot_id} as a detached process. - This is called after create_bot() to auto-start the new bot. - - Args: - bot_id: Bot identifier to start. - - Returns: - True if the process was launched successfully, False otherwise. - """ - BASE_BOT_PATH = Path(__file__).resolve().parent / "base_bot.py" - PYTHON = shutil.which("python3") or sys.executable - - try: - proc = subprocess.Popen( - [PYTHON, str(BASE_BOT_PATH), "--bot-id", bot_id], - start_new_session=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - logger.info("Started bot process: bot_id=%s, pid=%d", bot_id, proc.pid) - return True - - except OSError as e: - logger.warning("Failed to start bot process for '%s': %s", bot_id, e) - return False - - -def stop_service(bot_id: str) -> bool: - """ - Stop the systemd user service for a bot. - - Runs: systemctl --user stop telegram-bot@{bot_id} - - Args: - bot_id: Bot identifier used in the service template. - - Returns: - True if the service was stopped successfully, False otherwise. - """ - SERVICE_NAME = f"telegram-bot@{bot_id}" - try: - result = subprocess.run( - ["systemctl", "--user", "stop", SERVICE_NAME], - capture_output=True, - text=True, - timeout=10, - ) - if result.returncode == 0: - logger.info("Stopped systemd service: %s", SERVICE_NAME) - return True - - logger.warning("Failed to stop service %s: %s", SERVICE_NAME, result.stderr.strip()) - return False - - except subprocess.TimeoutExpired: - logger.warning("Timeout stopping service: %s", SERVICE_NAME) - return False - except OSError as e: - logger.warning("Error stopping service %s: %s", SERVICE_NAME, e) - return False - - -# ============================================= -# BOT LIFECYCLE -# ============================================= - - -def create_bot( - bot_id: str, - bot_token: str, - branch_name: Optional[str] = None, - work_dir: Optional[str] = None, - bot_name: Optional[str] = None, - allowed_user_ids: Optional[list[int]] = None, -) -> Optional[dict]: - """ - Create a new bot: validate, write config, register, setup systemd. - - Steps: - 1. Validate token via getMe - 2. If branch_name provided: validate branch exists in BRANCH_REGISTRY.json - 3. Check bot_id not already registered - 4. Write config file: ~/.aipass/telegram_bots/{bot_id}.json - 5. Register in bot registry - 6. Set BotFather commands via setMyCommands API - 7. Enable systemd service (don't start - leave that to caller) - - Args: - bot_id: Unique identifier for this bot (e.g., "dev_central", "base"). - bot_token: Telegram bot token from BotFather. - branch_name: AIPass branch name to associate, or None for base bot. - work_dir: Working directory for Claude sessions. Auto-resolved from branch if None. - bot_name: Human-readable bot name. Auto-generated if None. - allowed_user_ids: List of Telegram user IDs allowed to use this bot. - - Returns: - Bot info dict on success, None on any failure. - """ - # Step 1: Validate token - bot_info = validate_token(bot_token) - if not bot_info: - logger.warning("create_bot failed: invalid token for bot_id '%s'", bot_id) - return None - - BOT_USERNAME = bot_info.get("username", "unknown") - - # Step 2: Validate branch if provided - RESOLVED_WORK_DIR = str(Path.cwd()) if work_dir is None else str(work_dir) - if branch_name: - branch_info = validate_branch(branch_name) - if not branch_info: - logger.warning("create_bot failed: branch '%s' not found", branch_name) - return None - # Always use registry path as source of truth when branch_name is provided - REGISTRY_PATH = branch_info.get("path", "") - if REGISTRY_PATH: - if work_dir and str(work_dir) != REGISTRY_PATH: - logger.warning( - "create_bot: explicit work_dir '%s' differs from registry path '%s' — using registry", - work_dir, REGISTRY_PATH, - ) - RESOLVED_WORK_DIR = REGISTRY_PATH - - # Step 3: Check not already registered - existing = get_bot(bot_id) - if existing: - logger.warning("create_bot failed: bot_id '%s' already registered", bot_id) - return None - - # Also check no other bot owns this branch - if branch_name: - branch_bot = get_bot_by_branch(branch_name) - if branch_bot: - logger.warning( - "create_bot failed: branch '%s' already has bot '%s'", - branch_name, branch_bot.get("bot_id"), - ) - return None - - # Step 4: Write config file - ensure_registry() - BOT_CONFIG_DIR.mkdir(parents=True, exist_ok=True) - - RESOLVED_BOT_NAME = bot_name or f"AIPass {bot_id.replace('_', ' ').title()} Bot" - CONFIG_PATH = BOT_CONFIG_DIR / f"{bot_id}.json" - - config_data = { - "bot_id": bot_id, - "bot_token": bot_token, - "bot_name": RESOLVED_BOT_NAME, - "branch_name": branch_name, - "work_dir": RESOLVED_WORK_DIR, - "allowed_user_ids": allowed_user_ids or [], - "created_at": datetime.now(timezone.utc).isoformat(), - } - - try: - CONFIG_PATH.write_text( - json.dumps(config_data, indent=2), - encoding="utf-8", - ) - logger.info("Wrote bot config: %s", CONFIG_PATH) - except OSError as e: - logger.warning("Failed to write bot config: %s", e) - return None - - # Step 5: Register in bot registry - registered = register_bot( - bot_id=bot_id, - username=BOT_USERNAME, - branch_name=branch_name, - work_dir=RESOLVED_WORK_DIR, - config_path=str(CONFIG_PATH), - ) - if not registered: - # Clean up config file on registration failure - CONFIG_PATH.unlink(missing_ok=True) - logger.warning("create_bot failed: registry registration failed for '%s'", bot_id) - return None - - # Step 6: Set BotFather commands - set_bot_commands(bot_token, DEFAULT_BOT_COMMANDS) - - # Step 7: Enable systemd service - enable_service(bot_id) - - # Step 8: Auto-start the bot process - started = start_bot_process(bot_id) - - logger.info( - "Bot created: %s (@%s, branch=%s, work_dir=%s, started=%s)", - bot_id, BOT_USERNAME, branch_name, RESOLVED_WORK_DIR, started, - ) - - return { - "bot_id": bot_id, - "username": BOT_USERNAME, - "bot_name": RESOLVED_BOT_NAME, - "branch_name": branch_name, - "work_dir": RESOLVED_WORK_DIR, - "config_path": str(CONFIG_PATH), - "service_name": f"telegram-bot@{bot_id}", - "auto_started": started, - } - - -def delete_bot(bot_id: str, kill_tmux: bool = True) -> bool: - """ - Delete a bot: stop service, kill tmux, remove config, deregister. - - Steps: - 1. Stop systemd service - 2. Kill tmux session if exists and kill_tmux is True - 3. Remove config file - 4. Clean up pending file (both v1 and v2 naming) - 5. Deregister from registry - - Args: - bot_id: Bot identifier to delete. - kill_tmux: Whether to kill the associated tmux session (default True). - - Returns: - True if the bot was fully cleaned up, False on any failure. - """ - bot = get_bot(bot_id) - if not bot: - logger.warning("delete_bot failed: bot '%s' not found in registry", bot_id) - return False - - # Step 1: Stop systemd service - stop_service(bot_id) - disable_service(bot_id) - - # Step 2: Kill tmux session if requested - if kill_tmux: - TMUX_SESSION_NAME = f"telegram-{bot_id}" - try: - subprocess.run( - ["tmux", "kill-session", "-t", TMUX_SESSION_NAME], - capture_output=True, - text=True, - timeout=5, - ) - logger.info("Killed tmux session: %s", TMUX_SESSION_NAME) - except (subprocess.TimeoutExpired, OSError) as e: - logger.info("tmux kill-session for '%s' skipped (may not exist): %s", TMUX_SESSION_NAME, e) - - # Step 3: Remove config file - CONFIG_PATH = bot.get("config_path", "") - if CONFIG_PATH: - try: - Path(CONFIG_PATH).unlink(missing_ok=True) - logger.info("Removed config file: %s", CONFIG_PATH) - except OSError as e: - logger.warning("Failed to remove config file: %s", e) - - # Step 4: Clean up pending files (both v1 and v2 naming) - PENDING_DIR = _DATA_DIR / "telegram_pending" - BRANCH_NAME = bot.get("branch_name", bot_id) - - # v2 naming: bot-{bot_id}.json - PENDING_V2 = PENDING_DIR / f"bot-{bot_id}.json" - PENDING_V2.unlink(missing_ok=True) - - # v1 naming: telegram-{branch_name}.json - if BRANCH_NAME: - PENDING_V1 = PENDING_DIR / f"telegram-{BRANCH_NAME}.json" - PENDING_V1.unlink(missing_ok=True) - - # Step 5: Deregister from registry - if not deregister_bot(bot_id): - logger.warning("delete_bot: deregistration failed for '%s'", bot_id) - return False - - logger.info("Bot deleted: %s", bot_id) - return True diff --git a/src/aipass/api/apps/handlers/telegram/bot_operations.py b/src/aipass/api/apps/handlers/telegram/bot_operations.py deleted file mode 100644 index dbc39a18..00000000 --- a/src/aipass/api/apps/handlers/telegram/bot_operations.py +++ /dev/null @@ -1,240 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: bot_operations.py - Bot operation handlers for multi-bot module -# Date: 2026-02-24 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-24): Initial - start, stop, status, list operations for multi-bot system -# -# CODE STANDARDS: -# - Pure functions with proper error handling (graceful - never raise) -# - No Prax imports (handler tier 3) -# - Stdlib only (subprocess for systemd) -# - Returns values for caller to log/display - no handler-level logging -# ============================================= - -""" -Bot Operation Handlers for Multi-Bot Architecture - -Implementation logic for bot lifecycle operations: -- start_bot: load config and run polling loop -- stop_bot: stop systemd service -- get_status: query registry for bot status -- get_all_bots: list all registered bots -- format_bot_details: format a bot entry for display - -Called by the telegram_bot module (thin orchestration layer). -All functions return values - the module layer handles logging and display. -""" - -# Infrastructure -import sys -from pathlib import Path - -# Standard library -import subprocess - -# Internal handler imports -from aipass.api.apps.handlers.telegram.base_bot import BaseBot -from aipass.api.apps.handlers.telegram.branch_plugin import BranchPlugin -from aipass.api.apps.handlers.telegram.bot_registry import list_bots, get_bot -from aipass.api.apps.handlers.telegram.config import load_bot_config - -# ============================================= -# BOT OPERATIONS -# ============================================= - - -def start_bot(bot_id: str) -> int | None: - """ - Load config and start a bot's polling loop. - - Loads config from ~/.aipass/telegram_bots/{bot_id}.json. - If config has "branch_name", creates a BranchPlugin, else a BaseBot. - Calls bot.run() which blocks until terminated. - - Args: - bot_id: Bot identifier to start - - Returns: - Bot exit code, or None if config loading failed. - """ - config = load_bot_config(bot_id) - if not config: - return None - - bot_token = config.get("bot_token") - if not bot_token: - return None - - work_dir = Path(config.get("work_dir", str(Path.cwd()))) - bot_name = config.get("bot_name", f"AIPass {bot_id} Bot") - allowed_user_ids = config.get("allowed_user_ids", []) - branch_name = config.get("branch_name") - - if branch_name: - bot = BranchPlugin( - branch_name=branch_name, - bot_id=bot_id, - bot_token=bot_token, - work_dir=work_dir, - bot_name=bot_name, - allowed_user_ids=allowed_user_ids, - ) - else: - bot = BaseBot( - bot_id=bot_id, - bot_token=bot_token, - work_dir=work_dir, - bot_name=bot_name, - allowed_user_ids=allowed_user_ids, - ) - - return bot.run() - - -def stop_bot(bot_id: str) -> tuple[bool, str]: - """ - Stop a bot's systemd service. - - Args: - bot_id: Bot identifier to stop - - Returns: - Tuple of (success, message). - """ - service_name = f"telegram-bot@{bot_id}" - - try: - result = subprocess.run( - ["systemctl", "--user", "stop", service_name], - capture_output=True, - text=True, - timeout=10, - ) - - if result.returncode == 0: - return True, f"Stopped {service_name}" - - return False, f"Failed to stop {service_name}: {result.stderr.strip()}" - - except subprocess.TimeoutExpired: - return False, f"Timeout stopping {service_name}" - except OSError as e: - return False, f"Error stopping {service_name}: {e}" - - -def get_status(bot_id: str | None = None) -> list[dict]: - """ - Get bot status entries. If no bot_id, returns all bots. - - Args: - bot_id: Specific bot to check, or None for all bots - - Returns: - List of bot entry dicts. Empty list if not found. - """ - if bot_id: - bot = get_bot(bot_id) - return [bot] if bot else [] - - return list_bots() - - -def get_all_bots() -> list[dict]: - """ - Get all registered bots. - - Returns: - List of bot entry dicts. - """ - return list_bots() - - -def format_bot_details(bot: dict) -> list[str]: - """ - Format a single bot entry into display lines. - - Args: - bot: Bot entry dict from registry. - - Returns: - List of formatted strings for display. - """ - bot_id = bot.get("bot_id", "?") - username = bot.get("username", "?") - branch = bot.get("branch_name") or "none (base bot)" - work_dir = bot.get("work_dir", "?") - status = bot.get("status", "?") - service = bot.get("service_name", f"telegram-bot@{bot_id}") - - return [ - f"Bot ID: {bot_id}", - f"Username: @{username}", - f"Branch: {branch}", - f"Work Dir: {work_dir}", - f"Status: {status}", - f"Service: {service}", - ] - - -def format_bot_table(bots: list[dict]) -> list[str]: - """ - Format a list of bots into table rows. - - Args: - bots: List of bot entry dicts. - - Returns: - List of formatted strings (header + separator + rows + total). - """ - lines = [] - lines.append(f" {'Bot ID':<18} {'Branch':<16} {'Username':<24} {'Status':<10}") - lines.append(f" {'---' * 6:<18} {'---' * 5:<16} {'---' * 8:<24} {'---' * 3:<10}") - - for bot in bots: - bot_id = bot.get("bot_id", "?") - branch = bot.get("branch_name") or "-" - username = f"@{bot.get('username', '?')}" - status = bot.get("status", "?") - lines.append(f" {bot_id:<18} {branch:<16} {username:<24} {status}") - - lines.append(f" Total: {len(bots)} bot(s)") - return lines - - -def parse_create_args(args: list) -> dict | None: - """ - Parse create command arguments. - - Args: - args: Arguments after 'create' (bot_id, token, [--branch name], [--work-dir path]) - - Returns: - Dict with parsed values, or None if args are insufficient. - """ - if len(args) < 2: - return None - - result = { - "bot_id": args[0], - "bot_token": args[1], - "branch_name": None, - "work_dir": None, - } - - i = 2 - while i < len(args): - if args[i] == "--branch" and i + 1 < len(args): - result["branch_name"] = args[i + 1] - i += 2 - elif args[i] == "--work-dir" and i + 1 < len(args): - result["work_dir"] = args[i + 1] - i += 2 - else: - i += 1 - - return result diff --git a/src/aipass/api/apps/handlers/telegram/bot_registry.py b/src/aipass/api/apps/handlers/telegram/bot_registry.py deleted file mode 100644 index 6ba9532a..00000000 --- a/src/aipass/api/apps/handlers/telegram/bot_registry.py +++ /dev/null @@ -1,371 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: bot_registry.py - Bot registry management for multi-bot architecture -# Date: 2026-02-24 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-24): Initial - registry CRUD with fcntl locking for multi-bot management -# -# CODE STANDARDS: -# - Pure functions with proper error handling (graceful - never raise) -# - Uses Prax system_logger (FPLAN-0382 migration) -# - Thread-safe via fcntl.flock (shared read, exclusive write) -# ============================================= - -""" -Bot Registry Management for Multi-Bot Architecture - -Manages a central registry of all Telegram bots in the AIPass ecosystem. -Each bot entry tracks its ID, branch association, working directory, -config path, systemd service name, and status. - -Registry location: ~/.aipass/telegram_bots/_registry.json - -Thread-safe via fcntl.flock file locking (same pattern as session_store.py). -All public functions return None/False on errors - never raise exceptions. -""" - -# Infrastructure -import sys -from pathlib import Path - -# Standard library -import fcntl -import json -import os -from datetime import datetime, timezone -from typing import Optional - -# Logging (Prax system_logger — FPLAN-0382) -from aipass.prax.apps.modules.logger import system_logger as logger - -# ============================================= -# CONSTANTS -# ============================================= - -def _aipass_data_dir() -> Path: - """User data directory for AIPass runtime files.""" - env = os.environ.get("AIPASS_DATA_DIR") - if env: - return Path(env) - return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "aipass" - -REGISTRY_DIR = _aipass_data_dir() / "telegram_bots" -REGISTRY_FILE = REGISTRY_DIR / "_registry.json" - -# ============================================= -# EMPTY REGISTRY TEMPLATE -# ============================================= - - -def _empty_registry() -> dict: - """Return a fresh empty registry structure.""" - return { - "bots": {}, - "metadata": { - "version": "1.0.0", - "last_updated": datetime.now(timezone.utc).isoformat(), - }, - } - - -def _now_iso() -> str: - """Return current UTC timestamp in ISO format.""" - return datetime.now(timezone.utc).isoformat() - - -# ============================================= -# REGISTRY LIFECYCLE -# ============================================= - - -def ensure_registry() -> None: - """ - Create registry directory and file if they don't exist. - - Safe to call multiple times - only creates what is missing. - """ - try: - REGISTRY_DIR.mkdir(parents=True, exist_ok=True) - if not REGISTRY_FILE.exists(): - data = _empty_registry() - with open(REGISTRY_FILE, "w", encoding="utf-8") as f: - fcntl.flock(f.fileno(), fcntl.LOCK_EX) - try: - json.dump(data, f, indent=2) - finally: - fcntl.flock(f.fileno(), fcntl.LOCK_UN) - logger.info("Created new bot registry at %s", REGISTRY_FILE) - except OSError as e: - logger.warning("Failed to ensure registry: %s", e) - - -# ============================================= -# READ / WRITE WITH LOCKING -# ============================================= - - -def load_registry() -> dict: - """ - Load registry with fcntl shared lock. - - Returns: - Registry dict. Returns empty structure if file missing or corrupt. - """ - if not REGISTRY_FILE.exists(): - return _empty_registry() - - try: - with open(REGISTRY_FILE, "r", encoding="utf-8") as f: - fcntl.flock(f.fileno(), fcntl.LOCK_SH) - try: - data = json.load(f) - finally: - fcntl.flock(f.fileno(), fcntl.LOCK_UN) - - if not isinstance(data, dict) or "bots" not in data: - logger.warning("Registry file has unexpected structure, returning empty") - return _empty_registry() - - return data - - except (json.JSONDecodeError, OSError) as e: - logger.warning("Failed to load registry: %s", e) - return _empty_registry() - - -def save_registry(data: dict) -> bool: - """ - Save registry with fcntl exclusive lock. - - Args: - data: Full registry dict to write. - - Returns: - True if saved successfully, False on error. - """ - try: - REGISTRY_DIR.mkdir(parents=True, exist_ok=True) - - # Update metadata timestamp - if "metadata" not in data: - data["metadata"] = {} - data["metadata"]["last_updated"] = _now_iso() - - with open(REGISTRY_FILE, "w", encoding="utf-8") as f: - fcntl.flock(f.fileno(), fcntl.LOCK_EX) - try: - json.dump(data, f, indent=2) - finally: - fcntl.flock(f.fileno(), fcntl.LOCK_UN) - - return True - - except OSError as e: - logger.warning("Failed to save registry: %s", e) - return False - - -# ============================================= -# CRUD OPERATIONS -# ============================================= - - -def get_bot(bot_id: str) -> Optional[dict]: - """ - Get a single bot entry by bot_id. - - Args: - bot_id: Unique bot identifier. - - Returns: - Bot entry dict or None if not found. - """ - registry = load_registry() - return registry.get("bots", {}).get(bot_id) - - -def list_bots(status: Optional[str] = None) -> list[dict]: - """ - List all bots, optionally filtered by status. - - Args: - status: Filter by status (e.g., "active", "inactive"). None returns all. - - Returns: - List of bot entry dicts. - """ - registry = load_registry() - bots = list(registry.get("bots", {}).values()) - - if status is not None: - bots = [b for b in bots if b.get("status") == status] - - return bots - - -def register_bot( - bot_id: str, - username: str, - branch_name: Optional[str], - work_dir: str, - config_path: str, - bot_token_ref: Optional[str] = None, -) -> bool: - """ - Register a new bot in the registry. - - Args: - bot_id: Unique bot identifier. - username: Telegram bot username (e.g., "aipass_dev_central_bot"). - branch_name: AIPass branch name, or None for the base bot. - work_dir: Working directory for Claude sessions. - config_path: Path to the bot's config JSON file. - bot_token_ref: Optional env var name or reference for the token. - - Returns: - True on success, False if bot_id already exists or on error. - """ - registry = load_registry() - bots = registry.get("bots", {}) - - if bot_id in bots: - logger.warning("Bot '%s' already registered", bot_id) - return False - - now = _now_iso() - entry = { - "bot_id": bot_id, - "username": username, - "branch_name": branch_name, - "work_dir": str(work_dir), - "config_path": str(config_path), - "service_name": f"telegram-bot@{bot_id}", - "status": "active", - "created_at": now, - "updated_at": now, - } - - if bot_token_ref: - entry["bot_token_env"] = bot_token_ref - - bots[bot_id] = entry - registry["bots"] = bots - - if not save_registry(registry): - return False - - logger.info("Registered bot '%s' (branch=%s, work_dir=%s)", bot_id, branch_name, work_dir) - return True - - -def update_bot(bot_id: str, **kwargs) -> bool: - """ - Update specific fields of a bot entry. - - Auto-updates the updated_at timestamp. - - Args: - bot_id: Bot identifier to update. - **kwargs: Fields to update (e.g., status="inactive", username="new_name"). - - Returns: - True on success, False if bot not found or on error. - """ - registry = load_registry() - bots = registry.get("bots", {}) - - if bot_id not in bots: - logger.warning("Cannot update bot '%s': not found", bot_id) - return False - - for key, value in kwargs.items(): - bots[bot_id][key] = value - - bots[bot_id]["updated_at"] = _now_iso() - registry["bots"] = bots - - if not save_registry(registry): - return False - - logger.info("Updated bot '%s': %s", bot_id, list(kwargs.keys())) - return True - - -def deregister_bot(bot_id: str) -> bool: - """ - Remove a bot from the registry. - - Args: - bot_id: Bot identifier to remove. - - Returns: - True on success, False if bot not found or on error. - """ - registry = load_registry() - bots = registry.get("bots", {}) - - if bot_id not in bots: - logger.warning("Cannot deregister bot '%s': not found", bot_id) - return False - - del bots[bot_id] - registry["bots"] = bots - - if not save_registry(registry): - return False - - logger.info("Deregistered bot '%s'", bot_id) - return True - - -# ============================================= -# LOOKUP HELPERS -# ============================================= - - -def get_bot_by_branch(branch_name: str) -> Optional[dict]: - """ - Find a bot by its branch_name. - - Args: - branch_name: AIPass branch name (e.g., "dev_central"). - - Returns: - Bot entry dict or None if not found. - """ - registry = load_registry() - for bot in registry.get("bots", {}).values(): - if bot.get("branch_name") == branch_name: - return bot - return None - - -def get_bot_by_work_dir(work_dir) -> Optional[dict]: - """ - Find a bot whose work_dir matches the given path. - - Used by the response router to match CWD to a bot. - - Args: - work_dir: Path (str or Path) to match against bot work_dir fields. - - Returns: - Bot entry dict or None if not found. - """ - target = str(Path(work_dir).resolve()) - registry = load_registry() - - for bot in registry.get("bots", {}).values(): - bot_dir = bot.get("work_dir", "") - if bot_dir: - try: - if str(Path(bot_dir).resolve()) == target: - return bot - except (ValueError, OSError): - continue - - return None diff --git a/src/aipass/api/apps/handlers/telegram/botfather_client.py b/src/aipass/api/apps/handlers/telegram/botfather_client.py deleted file mode 100644 index b11b7dee..00000000 --- a/src/aipass/api/apps/handlers/telegram/botfather_client.py +++ /dev/null @@ -1,539 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: botfather_client.py - Telethon-based BotFather automation client -# Date: 2026-02-24 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-24): Initial - automated bot creation via BotFather using Telethon -# -# CODE STANDARDS: -# - Telethon for Telegram user-account interaction with @BotFather -# - Graceful error handling (never raise - return None on failure) -# - Sync wrapper for async Telethon calls (callable from stdlib code) -# - Uses Prax system_logger (FPLAN-0382 migration) -# ============================================= - -""" -BotFather Automation Client - -Automates Telegram bot creation by driving a conversation with @BotFather -using Telethon (user-account MTProto client). This replaces the manual -"go to BotFather, create a bot, paste the token" workflow. - -Flow: - 1. Connect to Telegram as Patrick's user account (pre-authenticated session) - 2. Send /newbot to @BotFather - 3. Provide display name and username - 4. Parse the bot token from BotFather's success response - 5. Return token + metadata for bot_factory.py to complete registration - -Requirements: - - Telethon 1.42.0+ installed in .venv - - One-time manual phone auth to create .telethon.session file - - API credentials in ~/.aipass/telegram_bots/.telethon_config.json -""" - -# Infrastructure -import sys -from pathlib import Path - -# Standard library -import asyncio -import json -import os -import re -import time -from typing import Any, Optional - -# Logging (Prax system_logger — FPLAN-0382) -from aipass.prax.apps.modules.logger import system_logger as logger - -# Third party (Telethon) — runtime-imported in methods to avoid Pyright issues -TELETHON_AVAILABLE = False -try: - import telethon as _telethon_check # noqa: F401 - TELETHON_AVAILABLE = True - del _telethon_check -except ImportError: - pass - -# ============================================= -# CONSTANTS -# ============================================= - -def _aipass_data_dir() -> Path: - """User data directory for AIPass runtime files.""" - env = os.environ.get("AIPASS_DATA_DIR") - if env: - return Path(env) - return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "aipass" - -BOT_CONFIG_DIR = _aipass_data_dir() / "telegram_bots" -TELETHON_CONFIG_PATH = BOT_CONFIG_DIR / ".telethon_config.json" -SESSION_PATH = BOT_CONFIG_DIR / ".telethon" # Telethon appends .session automatically - -BOTFATHER_USERNAME = "BotFather" -BOT_TOKEN_PATTERN = re.compile(r"\d+:[A-Za-z0-9_-]+") - -# Timeouts -MESSAGE_TIMEOUT = 30 # seconds to wait for BotFather response -MAX_USERNAME_ATTEMPTS = 3 - - -# ============================================= -# CONFIG LOADER -# ============================================= - - -def _load_telethon_config() -> Optional[dict]: - """ - Load Telethon API credentials from .telethon_config.json. - - Expected format: - {"api_id": 12345, "api_hash": "abc123..."} - - Returns: - Dict with "api_id" (int) and "api_hash" (str), or None on failure. - """ - try: - if not TELETHON_CONFIG_PATH.exists(): - logger.warning("Telethon config not found: %s", TELETHON_CONFIG_PATH) - return None - - with open(TELETHON_CONFIG_PATH, "r", encoding="utf-8") as f: - config = json.load(f) - - api_id = config.get("api_id") - api_hash = config.get("api_hash") - - if not api_id or not api_hash: - logger.warning("Telethon config missing api_id or api_hash") - return None - - # Ensure api_id is an integer - config["api_id"] = int(api_id) - config["api_hash"] = str(api_hash) - - logger.info("Telethon config loaded successfully") - return config - - except (json.JSONDecodeError, ValueError, OSError) as e: - logger.warning("Failed to load Telethon config: %s", e) - return None - - -# ============================================= -# SETUP CHECK -# ============================================= - - -def check_telethon_setup() -> tuple[bool, str]: - """ - Check whether Telethon is ready for BotFather automation. - - Verifies: - 1. Telethon library is importable - 2. .telethon_config.json exists with valid credentials - 3. .telethon.session exists (phone auth already completed) - - Returns: - (True, "ready") if everything is in place. - (False, "reason") with a human-readable explanation of what is missing. - """ - if not TELETHON_AVAILABLE: - return (False, "Telethon library not installed. Run: pip install telethon") - - if not TELETHON_CONFIG_PATH.exists(): - return (False, f"Telethon config not found at {TELETHON_CONFIG_PATH}") - - config = _load_telethon_config() - if config is None: - return (False, "Telethon config is invalid (missing api_id or api_hash)") - - # Telethon creates session files with .session extension - session_file = Path(str(SESSION_PATH) + ".session") - if not session_file.exists(): - return (False, f"Telethon session not found at {session_file}. Run one-time phone auth first.") - - return (True, "ready") - - -# ============================================= -# HELPER FUNCTIONS -# ============================================= - - -def _format_display_name(branch_name: str) -> str: - """ - Convert a branch_name to a BotFather display name. - - Examples: - "dev_central" -> "AIPass Dev Central" - "flow" -> "AIPass Flow" - "vera" -> "AIPass Vera" - - Args: - branch_name: AIPass branch name (snake_case). - - Returns: - Display name string. - """ - title = branch_name.replace("_", " ").title() - return f"AIPass {title}" - - -def _format_username(branch_name: str, suffix: int = 0) -> str: - """ - Generate a BotFather username from a branch name. - - Examples: - ("dev_central", 0) -> "aipass_dev_central_bot" - ("dev_central", 1) -> "aipass_dev_central_1_bot" - ("dev_central", 2) -> "aipass_dev_central_2_bot" - - Args: - branch_name: AIPass branch name (snake_case). - suffix: Numeric suffix for retries (0 = no suffix). - - Returns: - Username string ending in _bot. - """ - if suffix == 0: - return f"aipass_{branch_name}_bot" - return f"aipass_{branch_name}_{suffix}_bot" - - -# ============================================= -# BOTFATHER CLIENT -# ============================================= - - -class BotFatherClient: - """ - Telethon-based client that automates bot creation via @BotFather. - - Uses Patrick's authenticated user session to send commands to BotFather - and parse the resulting bot token. - - Usage: - client = BotFatherClient(api_id=12345, api_hash="abc...") - result = await client.create_bot("dev_central") - # result = {"token": "123:ABC", "username": "aipass_dev_central_bot", "display_name": "AIPass Dev Central"} - """ - - def __init__(self, api_id: int, api_hash: str) -> None: - self._api_id = api_id - self._api_hash = api_hash - self._client: Any = None - - async def connect(self) -> bool: - """ - Connect to Telegram using the existing session file. - - The session file must already exist from a prior manual phone auth. - This method will NOT prompt for phone/code input. - - Returns: - True if connected and authorized, False otherwise. - """ - try: - from telethon import TelegramClient as _TelegramClient - - self._client = _TelegramClient( - str(SESSION_PATH), - self._api_id, - self._api_hash, - ) - await self._client.connect() - - if not await self._client.is_user_authorized(): - logger.warning("Telethon session exists but is not authorized. Re-run phone auth.") - await self._client.disconnect() - self._client = None - return False - - me = await self._client.get_me() - if me: - logger.info( - "Connected to Telegram as: %s (id=%s)", - getattr(me, "first_name", "?"), - getattr(me, "id", "?"), - ) - else: - logger.info("Connected to Telegram (could not resolve self)") - - return True - - except Exception as e: - logger.warning("Failed to connect to Telegram: %s", e) - self._client = None - return False - - async def disconnect(self) -> None: - """Disconnect from Telegram gracefully.""" - if self._client: - try: - await self._client.disconnect() - logger.info("Disconnected from Telegram") - except Exception as e: - logger.warning("Error during disconnect: %s", e) - finally: - self._client = None - - async def _send_and_wait(self, entity: Any, message: str) -> Optional[str]: - """ - Send a message to BotFather and wait for a response. - - Handles FloodWaitError by sleeping for the required duration and retrying once. - - Args: - entity: The BotFather entity to send to. - message: The text message to send. - - Returns: - BotFather's response text, or None on timeout/error. - """ - if not self._client: - logger.warning("_send_and_wait called without active client") - return None - - from telethon.errors import FloodWaitError as _FloodWaitError - from telethon.errors import RPCError as _RPCError - - try: - await self._client.send_message(entity, message) - logger.info("Sent to BotFather: %s", message) - except _FloodWaitError as e: - wait_seconds = e.seconds - logger.warning("FloodWaitError: waiting %d seconds before retry", wait_seconds) - await asyncio.sleep(wait_seconds) - try: - await self._client.send_message(entity, message) - logger.info("Sent to BotFather (after flood wait): %s", message) - except Exception as retry_err: - logger.warning("Failed to send after flood wait: %s", retry_err) - return None - except _RPCError as e: - logger.warning("RPC error sending to BotFather: %s", e) - return None - except Exception as e: - logger.warning("Unexpected error sending to BotFather: %s", e) - return None - - # Wait for BotFather's response - deadline = time.monotonic() + MESSAGE_TIMEOUT - # Brief pause to let BotFather process - await asyncio.sleep(1.5) - - try: - while time.monotonic() < deadline: - # Get the most recent messages from BotFather - messages = await self._client.get_messages(entity, limit=1) - if not messages: - await asyncio.sleep(1.0) - continue - # get_messages returns a list-like object - msg_list = list(messages) - if msg_list: - latest = msg_list[0] - # Check that this message is FROM BotFather (not our own) - if getattr(latest, "out", True) is False and getattr(latest, "text", None): - response_text: str = latest.text - logger.info("BotFather response received (%d chars)", len(response_text)) - return response_text - - # Poll interval - await asyncio.sleep(1.0) - - logger.warning("Timeout waiting for BotFather response (after %ds)", MESSAGE_TIMEOUT) - return None - - except Exception as e: - logger.warning("Error reading BotFather response: %s", e) - return None - - async def create_bot(self, branch_name: str) -> Optional[dict]: - """ - Create a new Telegram bot via @BotFather conversation. - - Conversation flow: - 1. /newbot - 2. Display name (e.g., "AIPass Dev Central") - 3. Username (e.g., "aipass_dev_central_bot") - 4. Parse token from success response - - If the username is taken, retries with numeric suffixes up to MAX_USERNAME_ATTEMPTS. - - Args: - branch_name: AIPass branch name (e.g., "dev_central", "flow"). - - Returns: - Dict with "token", "username", "display_name" on success. - None on any failure. - """ - if not self._client: - logger.warning("create_bot called without active connection") - return None - - display_name = _format_display_name(branch_name) - - # Resolve BotFather entity - try: - botfather = await self._client.get_entity(BOTFATHER_USERNAME) - logger.info("Resolved BotFather entity: %s", getattr(botfather, "id", "?")) - except Exception as e: - logger.warning("Failed to resolve @BotFather entity: %s", e) - return None - - # Step 1: Send /newbot - response = await self._send_and_wait(botfather, "/newbot") - if not response: - logger.warning("BotFather did not respond to /newbot") - return None - - # BotFather should ask for a name - if "name" not in response.lower(): - logger.warning("Unexpected BotFather response to /newbot: %s", response[:200]) - return None - - logger.info("BotFather asked for bot name") - - # Step 2: Send display name - response = await self._send_and_wait(botfather, display_name) - if not response: - logger.warning("BotFather did not respond to display name") - return None - - # BotFather should ask for a username - if "username" not in response.lower(): - logger.warning("Unexpected BotFather response to display name: %s", response[:200]) - return None - - logger.info("BotFather asked for username") - - # Step 3: Try usernames with incrementing suffix - for attempt in range(MAX_USERNAME_ATTEMPTS): - username = _format_username(branch_name, suffix=attempt) - logger.info("Trying username: %s (attempt %d/%d)", username, attempt + 1, MAX_USERNAME_ATTEMPTS) - - response = await self._send_and_wait(botfather, username) - if not response: - logger.warning("BotFather did not respond to username '%s'", username) - return None - - # Check if the username was accepted (token in response) - token_match = BOT_TOKEN_PATTERN.search(response) - if token_match: - token = token_match.group() - logger.info( - "Bot created successfully: @%s (token: %s...%s)", - username, token[:8], token[-4:], - ) - return { - "token": token, - "username": username, - "display_name": display_name, - } - - # Username taken - BotFather says "Sorry" or mentions "already" - if "sorry" in response.lower() or "already" in response.lower() or "taken" in response.lower(): - logger.info("Username '%s' is taken, trying next", username) - # If not the last attempt, BotFather is still waiting for a username - # so we can send another one directly without restarting /newbot - continue - - # Unexpected response - logger.warning( - "Unexpected BotFather response for username '%s': %s", - username, response[:200], - ) - return None - - logger.warning( - "All %d username attempts exhausted for branch '%s'", - MAX_USERNAME_ATTEMPTS, branch_name, - ) - return None - - -# ============================================= -# SYNCHRONOUS WRAPPER -# ============================================= - - -def create_bot_via_botfather(branch_name: str) -> Optional[dict]: - """ - Synchronous wrapper to create a Telegram bot via BotFather automation. - - This is the main entry point for stdlib-based callers (e.g., base_bot.py). - Loads config, connects via Telethon, drives the BotFather conversation, - and returns the result. - - Args: - branch_name: AIPass branch name (e.g., "dev_central"). - - Returns: - Dict with "token", "username", "display_name" on success. - None on any failure (config missing, connection failed, BotFather error, etc.). - """ - # Pre-flight checks - ready, reason = check_telethon_setup() - if not ready: - logger.warning("Telethon setup check failed: %s", reason) - return None - - # Load config - config = _load_telethon_config() - if config is None: - logger.warning("Cannot create bot: Telethon config not loaded") - return None - - api_id = config["api_id"] - api_hash = config["api_hash"] - - # Run the async flow - client = BotFatherClient(api_id, api_hash) - result = None - - async def _run() -> Optional[dict]: - connected = await client.connect() - if not connected: - return None - try: - return await client.create_bot(branch_name) - finally: - await client.disconnect() - - try: - # Handle the case where an event loop is already running - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = None - - if loop and loop.is_running(): - # We're inside an existing event loop (unlikely for our stdlib callers, - # but handle gracefully). Create a new loop in a thread. - import concurrent.futures - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: - future = pool.submit(asyncio.run, _run()) - result = future.result(timeout=120) - else: - result = asyncio.run(_run()) - - except Exception as e: - logger.warning("create_bot_via_botfather failed: %s", e) - return None - - if result: - logger.info( - "Bot created via BotFather: @%s for branch '%s'", - result.get("username"), branch_name, - ) - else: - logger.warning("Bot creation via BotFather failed for branch '%s'", branch_name) - - return result diff --git a/src/aipass/api/apps/handlers/telegram/branch_plugin.py b/src/aipass/api/apps/handlers/telegram/branch_plugin.py deleted file mode 100644 index 6758c2b8..00000000 --- a/src/aipass/api/apps/handlers/telegram/branch_plugin.py +++ /dev/null @@ -1,170 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: branch_plugin.py - BranchPlugin extends BaseBot for per-branch bots -# Date: 2026-02-24 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-24): Initial - BranchPlugin with message prefixing and startup injection -# -# CODE STANDARDS: -# - Inherits Prax get_direct_logger() from BaseBot (FPLAN-0382 migration) -# - Extends BaseBot via hook overrides -# - No duplicated logic - all core behavior lives in BaseBot -# ============================================= - -""" -BranchPlugin - Per-branch Telegram bot extending BaseBot. - -Each AIPass branch gets its own dedicated Telegram bot. BranchPlugin overrides -BaseBot's hooks to: - - Prefix incoming messages with "Patrick via Telegram: " - - Prefix outgoing responses with "@branch_name" - - Inject "hi" on session creation to trigger the branch startup protocol - -Usage: - bot = BranchPlugin( - branch_name="dev_central", - bot_id="dev_central", - bot_token="123:ABC", - work_dir=Path.cwd(), - bot_name="AIPass Dev Central Bot", - allowed_user_ids=[7235222625], - ) - sys.exit(bot.run()) -""" - -# Infrastructure -import os -import sys -from pathlib import Path - -# Standard library -import argparse -import json -import time - -# Sibling import -from aipass.api.apps.handlers.telegram.base_bot import BaseBot - - -# ============================================= -# BranchPlugin CLASS -# ============================================= - -class BranchPlugin(BaseBot): - """ - Per-branch Telegram bot that extends BaseBot with branch-specific behavior. - - Overrides BaseBot hooks to prefix messages, tag responses, and trigger - the AIPass startup protocol when a new tmux session is created. - """ - - def __init__(self, branch_name: str, **kwargs) -> None: - """ - Initialize BranchPlugin. - - Args: - branch_name: AIPass branch name (e.g., "dev_central", "seed") - **kwargs: All BaseBot constructor arguments (bot_id, bot_token, etc.) - """ - self.branch_name = branch_name - super().__init__(**kwargs) - - # ============================================= - # HOOK OVERRIDES - # ============================================= - - def on_message(self, text: str) -> str: - """ - Prefix incoming messages with sender attribution. - - Args: - text: Raw message text from Telegram - - Returns: - Prefixed text for Claude: "Patrick via Telegram: {text}" - """ - return f"Patrick via Telegram: {text}" - - def on_response(self, text: str) -> str: - """ - Prefix outgoing responses with branch tag. - - Args: - text: Raw response text from Claude - - Returns: - Tagged text: "@{branch_name}\n{text}" - """ - return f"@{self.branch_name}\n{text}" - - def on_session_create(self, session_name: str, work_dir: Path) -> None: - """ - Inject "hi" after tmux session creation to trigger startup protocol. - - Waits 2 seconds for Claude to fully initialize, then injects "hi" - which triggers the AIPass startup sequence (reading memories, etc.). - - Args: - session_name: The tmux session name that was created - work_dir: The working directory of the session - """ - self.logger.info( - "Branch session created for @%s, injecting startup greeting", - self.branch_name, - ) - time.sleep(2) - self.inject_message("hi") - - -# ============================================= -# CLI ENTRY POINT -# ============================================= - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="AIPass Telegram Branch Bot") - parser.add_argument("--bot-id", required=True, help="Bot identifier") - parser.add_argument("--config", help="Path to bot config JSON") - args = parser.parse_args() - - # Load config from data dir telegram_bots/{bot_id}.json or --config path - _env_data = os.environ.get("AIPASS_DATA_DIR") - _data_dir = Path(_env_data) if _env_data else ( - Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "aipass" - ) - config_path = ( - Path(args.config) - if args.config - else _data_dir / "telegram_bots" / f"{args.bot_id}.json" - ) - - with open(config_path, "r", encoding="utf-8") as f: - config = json.load(f) - - # If config has "branch_name", create BranchPlugin; otherwise BaseBot - shared_session = config.get("shared_session") - - if config.get("branch_name"): - bot = BranchPlugin( - branch_name=config["branch_name"], - bot_id=args.bot_id, - bot_token=config["bot_token"], - work_dir=Path(config["work_dir"]), - bot_name=config.get("bot_name", f"AIPass {config['branch_name']} Bot"), - allowed_user_ids=config.get("allowed_user_ids", []), - shared_session=shared_session, - ) - else: - bot = BaseBot( - bot_id=args.bot_id, - bot_token=config["bot_token"], - work_dir=Path(config.get("work_dir", str(Path.cwd()))), - bot_name=config.get("bot_name", "AIPass Bot"), - allowed_user_ids=config.get("allowed_user_ids", []), - shared_session=shared_session, - ) - - sys.exit(bot.run()) diff --git a/src/aipass/api/apps/handlers/telegram/config.py b/src/aipass/api/apps/handlers/telegram/config.py deleted file mode 100644 index 4544fea6..00000000 --- a/src/aipass/api/apps/handlers/telegram/config.py +++ /dev/null @@ -1,267 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: config.py - Telegram Configuration Handler -# Date: 2026-02-03 -# Version: 1.2.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.2.0 (2026-02-24): Add multi-bot config: load_bot_config, list_bot_configs, validate_bot_config -# - v1.1.0 (2026-02-03): Add allowed_user_ids loading for user allowlist -# - v1.0.0 (2026-02-03): Initial config loader for Telegram bridge -# -# CODE STANDARDS: -# - Pure functions with proper error raising -# - No Prax imports (handler tier 3) -# ============================================= - -""" -Telegram Configuration Handler - -Manages Telegram bot configuration: -- Load bot token from config file (legacy single-bot: ~/.aipass/telegram_config.json) -- Load bot username -- Load allowed user IDs for access control -- Load per-bot configs (multi-bot: ~/.aipass/telegram_bots/{bot_id}.json) -- List and validate bot configs -""" - -# Infrastructure -import os -import sys -from pathlib import Path - -# Standard library -import json -from typing import Optional, List - -# ============================================= -# CONSTANTS -# ============================================= - -def _aipass_data_dir() -> Path: - """User data directory for AIPass runtime files.""" - env = os.environ.get("AIPASS_DATA_DIR") - if env: - return Path(env) - return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "aipass" - -_DATA_DIR = _aipass_data_dir() -CONFIG_PATH = _DATA_DIR / "telegram_config.json" -BOT_CONFIG_DIR = _DATA_DIR / "telegram_bots" - -REQUIRED_BOT_FIELDS = ("bot_id", "bot_token") - -# ============================================= -# CONFIGURATION LOADING -# ============================================= - -def load_telegram_config() -> Optional[dict]: - """ - Load Telegram configuration from config file. - - Config file location: ~/.aipass/telegram_config.json - Expected structure: - { - "telegram_bot_token": "...", - "telegram_bot_username": "aipass_bridge_bot", - "allowed_user_ids": [] - } - - Returns: - Configuration dict or None if load fails - """ - try: - if not CONFIG_PATH.exists(): - return None - - with open(CONFIG_PATH, 'r', encoding='utf-8') as f: - config = json.load(f) - - return config - - except json.JSONDecodeError: - return None - except Exception: - return None - - -def get_bot_token() -> Optional[str]: - """ - Get Telegram bot token from config. - - Returns: - Bot token string or None if not found - """ - config = load_telegram_config() - if not config: - return None - - token = config.get("telegram_bot_token") - if not token: - return None - - return token - - -def get_bot_username() -> Optional[str]: - """ - Get Telegram bot username from config. - - Returns: - Bot username string or None if not found - """ - config = load_telegram_config() - if not config: - return None - - username = config.get("telegram_bot_username") - if not username: - return None - - return username - - -def get_allowed_user_ids() -> List[int]: - """ - Get list of allowed Telegram user IDs from config. - - Returns: - List of allowed user IDs. Empty list means allow all (for testing). - """ - config = load_telegram_config() - if not config: - return [] - - allowed = config.get("allowed_user_ids", []) - if not isinstance(allowed, list): - return [] - - return [int(uid) for uid in allowed if isinstance(uid, (int, str))] - - -def validate_config() -> bool: - """ - Validate that Telegram configuration is complete. - - Returns: - True if config is valid, False otherwise - """ - config = load_telegram_config() - if not config: - return False - - if not config.get("telegram_bot_token"): - return False - - return True - - -# ============================================= -# MULTI-BOT CONFIGURATION (per-bot configs) -# ============================================= - - -def load_bot_config(bot_id: str) -> dict | None: - """ - Load per-bot config from ~/.aipass/telegram_bots/{bot_id}.json. - - Config format: - { - "bot_id": "dev_central", - "bot_token": "123:ABC...", - "bot_name": "AIPass Dev Central Bot", - "branch_name": "dev_central", // null for base bot - "work_dir": "/path/to/branch/work_dir", - "allowed_user_ids": [7235222625] - } - - Args: - bot_id: Bot identifier matching the config filename. - - Returns: - Config dict or None if not found/invalid. - """ - config_path = BOT_CONFIG_DIR / f"{bot_id}.json" - - try: - if not config_path.exists(): - return None - - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) - - if not isinstance(config, dict): - return None - - return config - - except json.JSONDecodeError: - return None - except OSError: - return None - - -def list_bot_configs() -> list[str]: - """ - List all bot config files (returns list of bot_ids). - - Scans ~/.aipass/telegram_bots/ for .json files, excluding - internal files that start with underscore (e.g., _registry.json). - - Returns: - List of bot_id strings derived from config filenames. - """ - if not BOT_CONFIG_DIR.exists(): - return [] - - bot_ids = [] - try: - for path in sorted(BOT_CONFIG_DIR.glob("*.json")): - # Skip internal files (e.g., _registry.json) - if path.stem.startswith("_"): - continue - bot_ids.append(path.stem) - except OSError: - return [] - - return bot_ids - - -def validate_bot_config(config: object) -> tuple[bool, str]: - """ - Validate a bot config dict. - - Checks for required fields and basic type correctness. - - Args: - config: Bot config dict to validate. - - Returns: - Tuple of (valid, error_message). error_message is empty on success. - """ - if not isinstance(config, dict): - return False, "Config must be a dict" - - # Check required fields - for field in REQUIRED_BOT_FIELDS: - if not config.get(field): - return False, f"Missing required field: {field}" - - # Type checks - bot_token = config.get("bot_token", "") - if not isinstance(bot_token, str) or ":" not in bot_token: - return False, "bot_token must be a string in format 'id:hash'" - - if "work_dir" in config and config["work_dir"] is not None: - work_dir = Path(config["work_dir"]) - if not work_dir.is_absolute(): - return False, "work_dir must be an absolute path" - - if "allowed_user_ids" in config: - allowed = config["allowed_user_ids"] - if not isinstance(allowed, list): - return False, "allowed_user_ids must be a list" - - return True, "" diff --git a/src/aipass/api/apps/handlers/telegram/file_handler.py b/src/aipass/api/apps/handlers/telegram/file_handler.py deleted file mode 100644 index 4ce003a1..00000000 --- a/src/aipass/api/apps/handlers/telegram/file_handler.py +++ /dev/null @@ -1,220 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: file_handler.py - Telegram File Upload Handler -# Date: 2026-02-10 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-10): Initial file handler - photo, document, file upload support (Phase 5 FPLAN-0312) -# -# CODE STANDARDS: -# - Pure functions with proper error raising -# - No Prax imports (handler tier 3) -# ============================================= - -""" -Telegram File Upload Handler - -Handles file uploads from Telegram: -- Downloads files to temp directory -- Detects file type (text, image, pdf, binary) -- Builds Claude prompts with file content inline or path reference -- Cleans up temp files after processing -""" - -# Infrastructure -import sys -from pathlib import Path - -import uuid - - -# Constants -TEMP_DIR = Path('/tmp/telegram_uploads') -MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB -TEXT_CONTENT_LIMIT = 50000 - -# Text file extensions that can be read as UTF-8 -SUPPORTED_TEXT_EXTENSIONS = { - '.py', '.js', '.ts', '.java', '.go', '.rs', '.rb', '.php', '.c', '.cpp', - '.h', '.hpp', '.cs', '.swift', '.kt', - '.sh', '.bash', '.zsh', '.sql', '.html', '.css', '.scss', - '.json', '.yaml', '.yml', '.xml', '.toml', '.ini', '.cfg', - '.md', '.txt', '.rst', '.log', '.csv', '.env', '.gitignore', '.dockerfile', -} - -# Image file extensions -IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'} - -# Map file extensions to language names for code blocks -LANGUAGE_MAP = { - '.py': 'python', '.js': 'javascript', '.ts': 'typescript', '.java': 'java', - '.go': 'go', '.rs': 'rust', '.rb': 'ruby', '.php': 'php', '.c': 'c', - '.cpp': 'cpp', '.h': 'c', '.cs': 'csharp', '.swift': 'swift', '.kt': 'kotlin', - '.sh': 'bash', '.bash': 'bash', '.sql': 'sql', '.html': 'html', '.css': 'css', - '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.xml': 'xml', - '.toml': 'toml', '.md': 'markdown', -} - - -def _sanitize_filename(raw_filename: str) -> str: - """ - Sanitize a filename by removing path separators and dangerous characters. - - Args: - raw_filename: The original filename to sanitize - - Returns: - A safe filename string - """ - safe_name = Path(raw_filename).name - safe_name = "".join(c if c.isalnum() or c in '.-_' else '_' for c in safe_name) - return safe_name or str(uuid.uuid4()) - - -async def download_telegram_file(file_obj, filename: str | None = None) -> Path: - """ - Download a Telegram file to the temp directory. - - Args: - file_obj: Telegram File object (from get_file()) - filename: Optional original filename - - Returns: - Path to the downloaded file - - Raises: - ValueError: If file exceeds MAX_FILE_SIZE - """ - if file_obj.file_size and file_obj.file_size > MAX_FILE_SIZE: - raise ValueError( - f"File too large: {file_obj.file_size} bytes " - f"(max {MAX_FILE_SIZE // (1024 * 1024)}MB)" - ) - - TEMP_DIR.mkdir(parents=True, exist_ok=True) - - if filename: - safe_name = _sanitize_filename(filename) - else: - ext = '' - if file_obj.file_path: - ext = Path(file_obj.file_path).suffix - safe_name = f"{uuid.uuid4()}{ext}" - - dest = TEMP_DIR / safe_name - await file_obj.download_to_drive(dest) - print("[INFO]", "Downloaded file to %s (%s bytes)", dest, file_obj.file_size) - return dest - - -def detect_file_type(file_path: Path) -> str: - """ - Detect the type of a file based on extension and content. - - Args: - file_path: Path to the file - - Returns: - One of: 'text', 'image', 'pdf', 'binary' - """ - suffix = file_path.suffix.lower() - - if suffix in SUPPORTED_TEXT_EXTENSIONS: - return 'text' - - if suffix in IMAGE_EXTENSIONS: - return 'image' - - if suffix == '.pdf': - return 'pdf' - - # Unknown extension - try reading as UTF-8 - try: - with open(file_path, 'rb') as f: - sample = f.read(1024) - sample.decode('utf-8') - return 'text' - except (UnicodeDecodeError, OSError): - return 'binary' - - -def build_file_prompt( - file_path: Path, - file_type: str, - caption: str | None = None, - sender_name: str = 'Patrick' -) -> str: - """ - Build a Claude prompt that includes file content. - - Args: - file_path: Path to the downloaded file - file_type: One of 'text', 'image', 'pdf', 'binary' - caption: Optional caption from the Telegram message - sender_name: Name of the sender - - Returns: - Formatted prompt string for Claude - """ - FILE_NAME = file_path.name - - if file_type == 'text': - try: - FILE_CONTENT = file_path.read_text(encoding='utf-8', errors='ignore') - except OSError: - FILE_CONTENT = '[Error reading file]' - - if len(FILE_CONTENT) > TEXT_CONTENT_LIMIT: - FILE_CONTENT = FILE_CONTENT[:TEXT_CONTENT_LIMIT] + '\n[...truncated]' - - FILE_SUFFIX = file_path.suffix.lower() - FILE_LANGUAGE = LANGUAGE_MAP.get(FILE_SUFFIX, '') - - PROMPT_CAPTION = caption or 'Review this file' - return ( - f"{sender_name} via Telegram: {PROMPT_CAPTION}\n\n" - f"File: {FILE_NAME}\n\n" - f"```{FILE_LANGUAGE}\n{FILE_CONTENT}\n```" - ) - - elif file_type == 'image': - PROMPT_CAPTION = caption or 'What do you see in this image?' - return ( - f"{sender_name} via Telegram: {PROMPT_CAPTION}\n\n" - f"[Image attached at: {file_path}]\n" - f"Please use the Read tool to view the image file at the path above." - ) - - elif file_type == 'pdf': - PROMPT_CAPTION = caption or 'Review this document' - return ( - f"{sender_name} via Telegram: {PROMPT_CAPTION}\n\n" - f"[PDF document at: {file_path}]\n" - f"Please use the Read tool to view the PDF file at the path above." - ) - - else: # binary - try: - FILE_SIZE = file_path.stat().st_size - except OSError: - FILE_SIZE = 0 - PROMPT_CAPTION = caption or 'I sent a file' - return ( - f"{sender_name} via Telegram: {PROMPT_CAPTION}\n\n" - f"[File at: {file_path}] (binary, {FILE_SIZE} bytes)\n" - f"Note: This is a binary file that may not be directly readable." - ) - - -def cleanup_file(file_path: Path) -> None: - """ - Remove a temporary file. - - Args: - file_path: Path to the file to clean up - """ - file_path.unlink(missing_ok=True) - print("[INFO]", "Cleaned up temp file: %s", file_path) diff --git a/src/aipass/api/apps/handlers/telegram/log_streamer.py b/src/aipass/api/apps/handlers/telegram/log_streamer.py deleted file mode 100644 index abf1f365..00000000 --- a/src/aipass/api/apps/handlers/telegram/log_streamer.py +++ /dev/null @@ -1,263 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: log_streamer.py - Stream system logs to Telegram -# Date: 2026-02-26 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-26): Initial - daemon thread log tailing with batched Telegram delivery -# -# CODE STANDARDS: -# - Uses Prax get_direct_logger() — no event pipeline (FPLAN-0382 migration) -# - Silent failure on Telegram errors (log warning, never crash) -# - Handlers implement logic, modules orchestrate -# ============================================= - -""" -LogStreamer - Stream system log lines to a Telegram chat. - -v1.0.0 - -Runs as a background daemon thread, tailing log files for a specific branch -and batching new lines to send via the Telegram Bot API. Tracks file positions -to only deliver new content, handles file rotation, and discovers new log files -each cycle. - -Usage: - streamer = LogStreamer(bot_token="...", chat_id=123456, branch_name="api") - streamer.start() - # ... later ... - streamer.stop() -""" - -# Infrastructure -import sys -from pathlib import Path - -# Standard library -import json -import threading -from typing import Dict, List -from urllib.error import URLError -from urllib.request import Request, urlopen - -# Logging (Prax direct logger — FPLAN-0382, no event pipeline) -from aipass.prax.apps.modules.logger import get_direct_logger - -# ============================================= -# CONSTANTS -# ============================================= - -def _find_repo_root() -> Path: - """Walk up from this file to find AIPASS_REGISTRY.json (repo root).""" - current = Path(__file__).resolve().parent - for parent in [current] + list(current.parents): - if (parent / "AIPASS_REGISTRY.json").exists(): - return parent - return Path.cwd() - -SYSTEM_LOGS_DIR = _find_repo_root() / "logs" -BATCH_INTERVAL = 5.0 -TELEGRAM_MAX_LENGTH = 4000 - - -# ============================================= -# LOG STREAMER -# ============================================= - - -class LogStreamer: - """Stream system log lines for a branch to Telegram via batched sends.""" - - def __init__(self, bot_token: str, chat_id: int, branch_name: str) -> None: - self.bot_token = bot_token - self.chat_id = chat_id - self.branch_name = branch_name - - self._running = False - self._stop_event = threading.Event() - self._thread: threading.Thread | None = None - self.log_positions: Dict[str, int] = {} - - # Direct logger (no event pipeline — avoids recursion with log tailing) - self.logger = get_direct_logger() - - # Initialize positions to end of all existing log files - self._init_positions() - - # ----------------------------------------- - # POSITION TRACKING - # ----------------------------------------- - - def _get_log_files(self) -> List[Path]: - """Find all log files matching this branch's pattern.""" - if not SYSTEM_LOGS_DIR.exists(): - return [] - return sorted(SYSTEM_LOGS_DIR.glob(f"{self.branch_name}_*.log")) - - def _init_positions(self) -> None: - """Set initial positions to end of file so we only tail new lines.""" - for log_file in self._get_log_files(): - file_path = str(log_file) - try: - self.log_positions[file_path] = log_file.stat().st_size - except OSError: - self.log_positions[file_path] = 0 - self.logger.info( - "Initialized positions for %d log files (branch: %s)", - len(self.log_positions), self.branch_name - ) - - def _read_new_lines(self) -> List[str]: - """Read new lines from all tracked log files.""" - all_new_lines: List[str] = [] - - for log_file in self._get_log_files(): - file_path = str(log_file) - - try: - current_size = log_file.stat().st_size - except OSError: - continue - - last_pos = self.log_positions.get(file_path, 0) - - try: - # New file discovered mid-run: start from beginning - if file_path not in self.log_positions: - last_pos = 0 - self.logger.info("New log file discovered: %s", file_path) - - # File rotation: size shrank, reset to beginning - if current_size < last_pos: - self.logger.info("File rotation detected: %s", file_path) - last_pos = 0 - - # Read new content - if current_size > last_pos: - with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: - f.seek(last_pos) - new_content = f.read() - self.log_positions[file_path] = f.tell() - - lines = new_content.splitlines() - if lines: - all_new_lines.extend(lines) - else: - # Update position even when nothing new (handles new file registration) - self.log_positions[file_path] = current_size - except OSError as e: - self.logger.warning("Failed to process %s: %s", file_path, e) - continue - - return all_new_lines - - # ----------------------------------------- - # TELEGRAM DELIVERY - # ----------------------------------------- - - def _send_message(self, message: str) -> bool: - """Send a message to Telegram. Returns True on success.""" - url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage" - payload = json.dumps({ - "chat_id": self.chat_id, - "text": message, - "disable_notification": True - }).encode("utf-8") - req = Request(url, data=payload, headers={"Content-Type": "application/json"}) - - try: - with urlopen(req, timeout=10) as resp: - result = json.loads(resp.read()) - return result.get("ok", False) - except (URLError, Exception) as e: - self.logger.warning("Telegram send failed: %s", e) - return False - - def _send_batched(self, lines: List[str]) -> None: - """Split lines into messages respecting TELEGRAM_MAX_LENGTH, send each.""" - if not lines: - return - - batch: List[str] = [] - batch_len = 0 - - for line in lines: - # +1 for the newline separator between lines - line_len = len(line) + (1 if batch else 0) - - if batch_len + line_len > TELEGRAM_MAX_LENGTH and batch: - # Send current batch - message = "\n".join(batch) - self._send_message(message) - batch = [] - batch_len = 0 - - batch.append(line) - batch_len += line_len - - # Send remaining - if batch: - message = "\n".join(batch) - self._send_message(message) - - # ----------------------------------------- - # DAEMON THREAD - # ----------------------------------------- - - def _run(self) -> None: - """Main loop: read new lines, batch, send, sleep.""" - self.logger.info("Log streamer started for branch: %s", self.branch_name) - self.logger.info("Watching: %s/%s_*.log (chat_id=%s)", SYSTEM_LOGS_DIR, self.branch_name, self.chat_id) - - while self._running: - try: - new_lines = self._read_new_lines() - if new_lines: - self.logger.info("Found %d new log lines, sending to Telegram", len(new_lines)) - self._send_batched(new_lines) - except Exception as e: - self.logger.warning("Streamer cycle error: %s", e) - - # Interruptible sleep - self._stop_event.wait(BATCH_INTERVAL) - - self.logger.info("Log streamer stopped for branch: %s", self.branch_name) - - # ----------------------------------------- - # PUBLIC API - # ----------------------------------------- - - def start(self) -> None: - """Start the log streamer daemon thread.""" - if self._running: - self.logger.warning("Log streamer already running") - return - - self._running = True - self._stop_event.clear() - self._thread = threading.Thread( - target=self._run, - name=f"log-streamer-{self.branch_name}", - daemon=True - ) - self._thread.start() - self.logger.info("Daemon thread started: %s", self._thread.name) - - def stop(self) -> None: - """Stop the log streamer and wait for thread to finish.""" - if not self._running: - return - - self._running = False - self._stop_event.set() - - if self._thread is not None: - self._thread.join(timeout=BATCH_INTERVAL + 2) - if self._thread.is_alive(): - self.logger.warning("Daemon thread did not exit cleanly") - self._thread = None - - self.logger.info("Log streamer stopped") diff --git a/src/aipass/api/apps/handlers/telegram/notifier.py b/src/aipass/api/apps/handlers/telegram/notifier.py deleted file mode 100644 index a827a8ee..00000000 --- a/src/aipass/api/apps/handlers/telegram/notifier.py +++ /dev/null @@ -1,122 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: notifier.py - Telegram Push Notifications -# Date: 2026-02-17 -# Version: 1.1.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.1.0 (2026-02-18): DPLAN-005 - Add silent mode, CLI interface for cross-branch use, markdown support -# - v1.0.0 (2026-02-17): Initial implementation - reusable Telegram notification sender -# -# CODE STANDARDS: -# - Handlers implement logic, modules orchestrate -# - No cross-branch imports, no Prax logger -# ============================================= - -""" -Telegram notification sender for the scheduler bot. - -Can be used two ways: -1. Import (within API branch): send_telegram_notification("message") -2. CLI (cross-branch, no import guard): python3 notifier.py "message" - Flags: --silent (silent push), --markdown (Markdown parse mode) - -Reads bot token and chat_id from ~/.aipass/scheduler_config.json. -""" - -# Infrastructure -import os -import sys -from pathlib import Path - -# Standard library -import json -from urllib.request import Request, urlopen -from urllib.error import URLError - -# ============================================= -# CONSTANTS -# ============================================= - -def _aipass_data_dir() -> Path: - """User data directory for AIPass runtime files.""" - env = os.environ.get("AIPASS_DATA_DIR") - if env: - return Path(env) - return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "aipass" - -CONFIG_PATH = _aipass_data_dir() / "scheduler_config.json" - -# ============================================= -# PUBLIC API -# ============================================= - - -def send_telegram_notification( - message: str, - silent: bool = False, - parse_mode: str | None = None, -) -> bool: - """ - Send a message to Telegram via the scheduler bot. - - Args: - message: Text to send (plain text or Markdown) - silent: If True, send as silent notification (no sound on phone) - parse_mode: Telegram parse mode ("Markdown" or "HTML"). None for plain text. - - Returns: - True if sent successfully, False otherwise - """ - try: - with open(CONFIG_PATH, "r", encoding="utf-8") as f: - config = json.load(f) - bot_token = config["telegram_bot_token"] - chat_id = config["telegram_chat_id"] - except (FileNotFoundError, KeyError, json.JSONDecodeError): - return False - - url = f"https://api.telegram.org/bot{bot_token}/sendMessage" - payload_dict: dict[str, object] = {"chat_id": chat_id, "text": message} - if silent: - payload_dict["disable_notification"] = True - if parse_mode: - payload_dict["parse_mode"] = parse_mode - - data = json.dumps(payload_dict).encode("utf-8") - req = Request(url, data=data, headers={"Content-Type": "application/json"}) - - try: - with urlopen(req, timeout=15) as resp: - result = json.loads(resp.read()) - return result.get("ok", False) - except (URLError, Exception): - return False - - -# ============================================= -# CLI INTERFACE (cross-branch use) -# ============================================= - -if __name__ == "__main__": - args = sys.argv[1:] - if not args or args[0] in ("-h", "--help"): - print("Usage: python3 notifier.py [--silent] [--markdown] \"message\"") - print(" --silent Send as silent notification (no sound)") - print(" --markdown Use Telegram Markdown parse mode") - sys.exit(0) - - silent_flag = "--silent" in args - markdown_flag = "--markdown" in args - msg_args = [a for a in args if not a.startswith("--")] - - if not msg_args: - print("Error: no message provided", file=sys.stderr) - sys.exit(1) - - msg = " ".join(msg_args) - mode = "Markdown" if markdown_flag else None - ok = send_telegram_notification(msg, silent=silent_flag, parse_mode=mode) - sys.exit(0 if ok else 1) diff --git a/src/aipass/api/apps/handlers/telegram/output_parser.py(disabled) b/src/aipass/api/apps/handlers/telegram/output_parser.py(disabled) deleted file mode 100644 index 7891d50d..00000000 --- a/src/aipass/api/apps/handlers/telegram/output_parser.py(disabled) +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 - -# ===================AIPASS==================== -# META DATA HEADER -# Name: output_parser.py - Claude Stream JSON Output Parser -# Date: 2026-02-10 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-10): Initial - parse stream-json from Claude CLI -# -# CODE STANDARDS: -# - Pure functions with proper error raising -# - No Prax imports (handler tier 3) -# ============================================= - -""" -Claude Stream JSON Output Parser - -Parses newline-delimited JSON output from Claude CLI when invoked with ---output-format stream-json --verbose. - -Output format (3 line types): - 1. system/init - session_id, model, tools - 2. assistant - message content blocks (text, tool_use) - 3. result - success/error, session_id, cost, duration -""" - -import sys -import json -import logging -from dataclasses import dataclass -from pathlib import Path -from typing import List, Optional - -# Infrastructure — disabled file, no sys.path manipulation needed - -logger = logging.getLogger("telegram_output_parser") - - -@dataclass -class ParsedResult: - """Structured result from parsing Claude stream-json output.""" - success: bool - text: str - session_id: Optional[str] = None - cost_usd: Optional[float] = None - duration_ms: Optional[int] = None - error_message: Optional[str] = None - - -class OutputParser: - """Parse Claude CLI stream-json output into structured results.""" - - @staticmethod - def parse_stream(raw_output: str) -> ParsedResult: - """ - Parse raw stream-json output from Claude CLI. - - Splits output by newlines, parses each line as JSON, and extracts: - - Text from assistant message content blocks (type='text' only) - - session_id from result line - - cost from result line total_cost_usd - - Error detection from result line is_error / subtype='error' - - Falls back to returning raw text if JSON parsing fails entirely. - - Args: - raw_output: Raw stdout from Claude CLI with --output-format stream-json - - Returns: - ParsedResult with extracted fields - """ - if not raw_output or not raw_output.strip(): - return ParsedResult(success=False, text="", error_message="Empty output") - - text_parts: List[str] = [] - session_id: Optional[str] = None - cost_usd: Optional[float] = None - duration_ms: Optional[int] = None - is_error = False - error_message: Optional[str] = None - parsed_any = False - - for line in raw_output.strip().splitlines(): - line = line.strip() - if not line: - continue - - try: - data = json.loads(line) - except json.JSONDecodeError: - logger.info("Skipping non-JSON line: %s", line[:100]) - continue - - parsed_any = True - line_type = data.get("type") - - if line_type == "assistant": - # Extract text from content blocks - message = data.get("message", {}) - content_blocks = message.get("content", []) - for block in content_blocks: - if block.get("type") == "text": - text_parts.append(block.get("text", "")) - - elif line_type == "result": - # Extract session_id, cost, duration, error status - session_id = data.get("session_id", session_id) - cost_usd = data.get("total_cost_usd", cost_usd) - duration_ms = data.get("duration_ms", duration_ms) - - if data.get("is_error", False) or data.get("subtype") == "error": - is_error = True - error_message = data.get("result", "Unknown error") - - elif line_type == "system": - # Init line - capture session_id as fallback - if not session_id: - session_id = data.get("session_id") - - # If we couldn't parse any JSON at all, fall back to raw text - if not parsed_any: - logger.warning("No valid JSON lines found, returning raw output") - return ParsedResult(success=True, text=raw_output.strip()) - - combined_text = "\n".join(text_parts) if text_parts else "" - - if is_error: - return ParsedResult( - success=False, - text=combined_text, - session_id=session_id, - cost_usd=cost_usd, - duration_ms=duration_ms, - error_message=error_message, - ) - - if not combined_text: - return ParsedResult( - success=False, - text="", - session_id=session_id, - cost_usd=cost_usd, - duration_ms=duration_ms, - error_message="No text content in response", - ) - - return ParsedResult( - success=True, - text=combined_text, - session_id=session_id, - cost_usd=cost_usd, - duration_ms=duration_ms, - ) diff --git a/src/aipass/api/apps/handlers/telegram/response_router.py b/src/aipass/api/apps/handlers/telegram/response_router.py deleted file mode 100644 index 2267626e..00000000 --- a/src/aipass/api/apps/handlers/telegram/response_router.py +++ /dev/null @@ -1,345 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: response_router.py - CWD-safe response routing for multi-bot architecture -# Date: 2026-02-24 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-24): Initial - CWD-safe pending file matching with directory tree resolution -# -# CODE STANDARDS: -# - Pure functions with proper error handling (graceful - never raise) -# - Uses Prax system_logger (FPLAN-0382 migration) -# - Stdlib only (no external deps) -# ============================================= - -""" -CWD-Safe Response Routing for Multi-Bot Architecture - -Fixes the CWD mismatch bug in the Stop hook. When Claude fires the Stop hook, -the working directory may be a subdirectory of the branch root (e.g., -/git_repo/ instead of /). The old logic used -Path.cwd().name which fails in subdirectories. - -New logic uses cwd.relative_to(work_dir) which succeeds if CWD is ANYWHERE -in the bot's directory tree. - -Pending file naming: -- v2 (new): bot-{bot_id}.json -- v1 (legacy): telegram-{branch_name}.json -Both formats are supported during the transition period. -""" - -# Infrastructure -import sys -from pathlib import Path - -# Standard library -import json -import os -import subprocess -import time -from typing import Optional - -# Logging (Prax system_logger — FPLAN-0382) -from aipass.prax.apps.modules.logger import system_logger as logger - -# ============================================= -# CONSTANTS -# ============================================= - -def _aipass_data_dir() -> Path: - """User data directory for AIPass runtime files.""" - env = os.environ.get("AIPASS_DATA_DIR") - if env: - return Path(env) - return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "aipass" - -_DATA_DIR = _aipass_data_dir() -PENDING_DIR = _DATA_DIR / "telegram_pending" -PENDING_TTL = 3600 # 1 hour - - -# ============================================= -# DIRECTORY TREE MATCHING -# ============================================= - - -def is_cwd_in_tree(cwd: Path, work_dir) -> bool: - """ - Check if cwd is within work_dir's directory tree using relative_to(). - - This is the core fix for the CWD mismatch bug. Instead of comparing - directory names (which fails in subdirectories), we check if cwd is - a child of work_dir at any depth. - - Args: - cwd: Current working directory to check. - work_dir: Bot's configured working directory (str or Path). - - Returns: - True if cwd is within work_dir's tree, False otherwise. - """ - try: - cwd.relative_to(Path(work_dir)) - return True - except ValueError: - return False - - -# ============================================= -# TMUX SESSION CHECKING -# ============================================= - - -def is_tmux_alive(session_name: str) -> bool: - """ - Check if a tmux session exists. - - Args: - session_name: Name of the tmux session to check. - - Returns: - True if the session exists, False otherwise. - """ - try: - result = subprocess.run( - ["tmux", "has-session", "-t", session_name], - capture_output=True, - text=True, - timeout=5, - ) - return result.returncode == 0 - except (subprocess.TimeoutExpired, OSError): - return False - - -# ============================================= -# PENDING FILE EXPIRY -# ============================================= - - -def is_pending_expired(pending_data: dict) -> bool: - """ - Check if a pending file is expired. - - A pending file is considered expired only when BOTH conditions are met: - 1. The timestamp is older than PENDING_TTL seconds - 2. The associated tmux session is no longer alive - - This prevents premature cleanup of pending files for long-running sessions. - - Args: - pending_data: Parsed contents of a pending file. - - Returns: - True if the pending file should be cleaned up, False otherwise. - """ - # Condition 1: Check TTL - timestamp = pending_data.get("timestamp", 0) - if isinstance(timestamp, str): - try: - timestamp = float(timestamp) - except ValueError: - timestamp = 0 - - if time.time() - timestamp <= PENDING_TTL: - return False # Still within TTL, not expired - - # Condition 2: Check tmux session - # Derive session name from bot_id or branch_name - bot_id = pending_data.get("bot_id", "") - branch_name = pending_data.get("branch_name", "") - - # Try the bot_id-based tmux session name first (v2) - if bot_id: - if is_tmux_alive(f"telegram-{bot_id}"): - return False # Session alive, not expired - - # Try the branch-based tmux session name (v1) - if branch_name and branch_name != bot_id: - if is_tmux_alive(f"telegram-{branch_name}"): - return False # Session alive, not expired - - # Past TTL AND no tmux session alive - return True - - -# ============================================= -# PENDING FILE LOADING -# ============================================= - - -def _load_pending_file(pending_path: Path) -> Optional[dict]: - """ - Load and parse a pending file from disk. - - Args: - pending_path: Path to the pending JSON file. - - Returns: - Parsed dict with "pending_path" key added, or None on error. - """ - try: - data = json.loads(pending_path.read_text(encoding="utf-8")) - if not isinstance(data, dict): - return None - data["pending_path"] = str(pending_path) - return data - except (json.JSONDecodeError, OSError): - return None - - -# ============================================= -# MAIN ROUTING LOGIC -# ============================================= - - -def find_pending_bot( - cwd: Optional[Path] = None, - session_id: Optional[str] = None, - env_bot_id: Optional[str] = None, -) -> Optional[dict]: - """ - Find which bot's pending file matches the current context. - - Uses a priority-based matching strategy: - - Priority 1: AIPASS_BOT_ID env var (set in tmux session by BaseBot) - Direct match: look for bot-{env_bot_id}.json - - Priority 2: cwd.relative_to(work_dir) - CWD anywhere in bot's directory tree - Load each pending file, check if cwd is within its work_dir - - Priority 3: session_id match - fallback for legacy compatibility - Check session_id field in each pending file - - Args: - cwd: Current working directory. Defaults to Path.cwd(). - session_id: Claude Code session ID for fallback matching. - env_bot_id: Bot ID from environment. Defaults to AIPASS_BOT_ID env var. - - Returns: - Pending file data dict with "pending_path" key, or None if no match. - """ - if not PENDING_DIR.exists(): - return None - - if cwd is None: - try: - cwd = Path.cwd() - except OSError: - cwd = Path.cwd() - - if env_bot_id is None: - env_bot_id = os.environ.get("AIPASS_BOT_ID") - - # Priority 1: Direct match via AIPASS_BOT_ID env var - if env_bot_id: - # v2 naming: bot-{bot_id}.json - PENDING_V2 = PENDING_DIR / f"bot-{env_bot_id}.json" - if PENDING_V2.exists(): - data = _load_pending_file(PENDING_V2) - if data and not is_pending_expired(data): - logger.info("Matched pending by AIPASS_BOT_ID: %s", env_bot_id) - return data - - # Also check v1 naming for this bot_id - PENDING_V1 = PENDING_DIR / f"telegram-{env_bot_id}.json" - if PENDING_V1.exists(): - data = _load_pending_file(PENDING_V1) - if data and not is_pending_expired(data): - logger.info("Matched pending by AIPASS_BOT_ID (v1 naming): %s", env_bot_id) - return data - - # Priority 2: CWD directory tree matching - # Check all pending files and see if CWD is within any bot's work_dir - ALL_PENDING = list(PENDING_DIR.glob("bot-*.json")) + list(PENDING_DIR.glob("telegram-*.json")) - - for pending_path in ALL_PENDING: - data = _load_pending_file(pending_path) - if not data: - continue - - if is_pending_expired(data): - continue - - work_dir = data.get("work_dir", "") - if work_dir and is_cwd_in_tree(cwd, work_dir): - logger.info("Matched pending by CWD tree: cwd=%s within work_dir=%s", cwd, work_dir) - return data - - # Legacy v1 files may not have work_dir - try branch_name directory matching - branch_name = data.get("branch_name", "") - if branch_name and not work_dir: - # CWD's directory name or any parent matches branch_name - path_cursor = cwd - while path_cursor != path_cursor.parent: - if path_cursor.name == branch_name: - logger.info("Matched pending by branch name in CWD path: %s", branch_name) - return data - path_cursor = path_cursor.parent - - # Priority 3: Session ID fallback - if session_id: - for pending_path in ALL_PENDING: - data = _load_pending_file(pending_path) - if not data: - continue - - if is_pending_expired(data): - continue - - if data.get("session_id") == session_id: - logger.info("Matched pending by session_id: %s", session_id[:8]) - return data - - return None - - -# ============================================= -# CLEANUP -# ============================================= - - -def clean_expired_pending() -> int: - """ - Remove all expired pending files from the pending directory. - - A file is expired when it is past TTL AND its tmux session is dead. - - Returns: - Number of expired files removed. - """ - if not PENDING_DIR.exists(): - return 0 - - REMOVED_COUNT = 0 - ALL_PENDING = list(PENDING_DIR.glob("bot-*.json")) + list(PENDING_DIR.glob("telegram-*.json")) - - for pending_path in ALL_PENDING: - data = _load_pending_file(pending_path) - if not data: - # Corrupt or unreadable file - remove it - try: - pending_path.unlink(missing_ok=True) - REMOVED_COUNT += 1 - logger.info("Removed corrupt pending file: %s", pending_path.name) - except OSError as e: - logger.warning("Failed to remove corrupt pending file %s: %s", pending_path.name, e) - continue - - if is_pending_expired(data): - try: - pending_path.unlink(missing_ok=True) - REMOVED_COUNT += 1 - logger.info("Removed expired pending file: %s", pending_path.name) - except OSError as e: - logger.warning("Failed to remove expired pending file %s: %s", pending_path.name, e) - - if REMOVED_COUNT > 0: - logger.info("Cleaned %d expired pending file(s)", REMOVED_COUNT) - - return REMOVED_COUNT diff --git a/src/aipass/api/apps/handlers/telegram/spawner.py(disabled) b/src/aipass/api/apps/handlers/telegram/spawner.py(disabled) deleted file mode 100644 index b2e958f4..00000000 --- a/src/aipass/api/apps/handlers/telegram/spawner.py(disabled) +++ /dev/null @@ -1,573 +0,0 @@ -#!/usr/bin/env python3 - -# ===================AIPASS==================== -# META DATA HEADER -# Name: spawner.py - Claude Session Spawner -# Date: 2026-02-03 -# Version: 4.3.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v4.3.0 (2026-02-12): Branch targeting - @branch prefix routes Claude to branch CWD -# - v4.2.0 (2026-02-10): Add raw_prompt flag to run_claude_capture (Phase 5 FPLAN-0312) -# - v4.1.0 (2026-02-10): Session persistence - use stored session_id for resume, deterministic as fallback -# - v4.0.0 (2026-02-10): Stream JSON output parsing - structured results with session_id, cost -# - v3.2.0 (2026-02-10): Security fix - subprocess_exec for capture mode (no shell injection) -# - v3.1.0 (2026-02-09): Fix session ID collision - random UUID for new sessions, deterministic for resume only -# -# CODE STANDARDS: -# - Pure functions with proper error raising -# - No Prax imports (handler tier 3) -# ============================================= - -""" -Claude Session Spawner - -Two modes of operation: -1. Visual mode: Spawn visible sessions (gnome-terminal/tmux) - fire and forget -2. Capture mode: Run Claude directly and capture stdout for response - -Supports branch targeting: messages starting with @branch_name spawn Claude -in that branch's directory. -""" - -import sys -import os -import re -import json -import subprocess -import shutil -import asyncio -import uuid -from pathlib import Path -from typing import Tuple, Optional, List - -# Infrastructure — disabled file, kept for reference only - -import logging -logger = logging.getLogger("telegram_spawner") - -# NOTE: These imports reference old non-aipass namespace and won't work. -# from api.apps.handlers.telegram.output_parser import OutputParser, ParsedResult -# from api.apps.handlers.telegram.session_store import get_session, save_session - -# Constants — package-relative -DEFAULT_SESSION_PATH = Path.cwd() -BRANCH_REGISTRY_PATH = Path.cwd() / "AIPASS_REGISTRY.json" -CLAUDE_BIN = shutil.which("claude") or "claude" -TMUX_SESSION_NAME = "telegram-claude" -TELEGRAM_CHAR_LIMIT = 4096 -DEFAULT_TIMEOUT = 120 # seconds - -# Backwards compatibility -SESSION_PATH = DEFAULT_SESSION_PATH - - -def resolve_branch_target(message: str) -> Tuple[str, Path]: - """ - Extract @branch target from message and resolve to a directory path. - - If message starts with @branch_name, look up the branch in BRANCH_REGISTRY.json - and return the cleaned message (without the @branch prefix) and the branch path. - If no @branch prefix or branch not found, returns original message and DEFAULT_SESSION_PATH. - - Returns: - Tuple of (cleaned_message, target_path) - """ - match = re.match(r'^@(\w+)\s*(.*)', message, re.DOTALL) - if not match: - return message, DEFAULT_SESSION_PATH - - branch_name = match.group(1).lower() - rest_of_message = match.group(2).strip() - - try: - with open(BRANCH_REGISTRY_PATH, 'r', encoding='utf-8') as f: - registry = json.load(f) - - branches = registry.get("branches", []) - for branch_entry in branches: - # Match against email (@branch_name) or name field - clean_email = branch_entry.get("email", "").replace("@", "").lower() - if clean_email == branch_name: - branch_path = Path(branch_entry.get("path", "")) - if branch_path.is_dir(): - logger.info("Branch target resolved: @%s -> %s", branch_name, branch_path) - return rest_of_message or "hi", branch_path - else: - logger.warning("Branch path not found on disk: %s", branch_path) - return message, DEFAULT_SESSION_PATH - - except (FileNotFoundError, json.JSONDecodeError, KeyError) as e: - logger.warning("Failed to resolve branch @%s: %s", branch_name, e) - - return message, DEFAULT_SESSION_PATH - - -def has_display() -> bool: - """Check if DISPLAY environment variable is set (GUI available).""" - return bool(os.environ.get("DISPLAY")) - - -def has_gnome_terminal() -> bool: - """Check if gnome-terminal is available.""" - return shutil.which("gnome-terminal") is not None - - -def has_tmux() -> bool: - """Check if tmux is available.""" - return shutil.which("tmux") is not None - - -def build_prompt(message: str) -> str: - """ - Build the Claude prompt from Telegram message (shell-escaped). - - Used by visual mode (gnome-terminal, tmux) which still requires shell escaping. - - Args: - message: The message text - - Returns: - Formatted prompt string with shell-safe escaping - """ - # Escape single quotes for shell safety - safe_message = message.replace("'", "'\"'\"'") - return f"Patrick via Telegram: {safe_message}" - - -def build_prompt_clean(message: str) -> str: - """ - Build the Claude prompt from Telegram message (no shell escaping). - - Used by capture mode where subprocess_exec passes args directly, - bypassing the shell entirely. - - Args: - message: The message text - - Returns: - Formatted prompt string (raw, no escaping needed) - """ - return f"Patrick via Telegram: {message}" - - -def spawn_gnome_terminal(prompt: str) -> Tuple[bool, str, Optional[int]]: - """ - Spawn Claude in a visible gnome-terminal window. - - Args: - prompt: The prompt to pass to Claude - - Returns: - Tuple of (success, message, pid or None) - """ - try: - # Escape prompt for shell - safe_prompt = prompt.replace("'", "'\"'\"'") - - # Build command: gnome-terminal spawns, runs claude, stays open - cmd = [ - "gnome-terminal", - "--", - "bash", "-c", - f"cd {SESSION_PATH} && claude -p '{safe_prompt}' --session-id {uuid.uuid4()} --permission-mode bypassPermissions; exec bash" - ] - - logger.info("Spawning gnome-terminal at %s", SESSION_PATH) - - process = subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - start_new_session=True - ) - - return True, f"gnome-terminal spawned (PID: {process.pid})", process.pid - - except Exception as e: - logger.error("gnome-terminal spawn failed: %s", e) - return False, str(e), None - - -def spawn_tmux(prompt: str) -> Tuple[bool, str, Optional[int]]: - """ - Spawn Claude in a tmux session (headless fallback). - - Creates or attaches to session named 'telegram-claude'. - - Args: - prompt: The prompt to pass to Claude - - Returns: - Tuple of (success, message, pid or None) - """ - try: - # Escape prompt for shell - safe_prompt = prompt.replace("'", "'\"'\"'") - - # Check if session already exists - check_cmd = ["tmux", "has-session", "-t", TMUX_SESSION_NAME] - session_exists = subprocess.run( - check_cmd, - capture_output=True - ).returncode == 0 - - if session_exists: - # Kill existing session to start fresh - subprocess.run( - ["tmux", "kill-session", "-t", TMUX_SESSION_NAME], - capture_output=True - ) - logger.info("Killed existing tmux session: %s", TMUX_SESSION_NAME) - - # Create new session with Claude - cmd = [ - "tmux", "new-session", - "-d", # Detached - "-s", TMUX_SESSION_NAME, - "-c", str(SESSION_PATH), # Working directory - f"claude -p '{safe_prompt}' --session-id {uuid.uuid4()} --permission-mode bypassPermissions" - ] - - logger.info("Spawning tmux session '%s' at %s", TMUX_SESSION_NAME, SESSION_PATH) - - process = subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - process.wait() - - if process.returncode == 0: - msg = f"tmux session '{TMUX_SESSION_NAME}' created. Attach with: tmux attach -t {TMUX_SESSION_NAME}" - return True, msg, None # tmux doesn't give us the claude PID directly - else: - return False, f"tmux exited with code {process.returncode}", None - - except Exception as e: - logger.error("tmux spawn failed: %s", e) - return False, str(e), None - - -def spawn_claude_session(sender_name: str, message: str) -> Tuple[bool, str]: - """ - Spawn a visible Claude session for a Telegram message. - - Strategy: - 1. If DISPLAY exists and gnome-terminal available → use gnome-terminal - 2. Otherwise if tmux available → use tmux - 3. If neither available → return error - - Args: - sender_name: Telegram sender name/username - message: The message text from Telegram - - Returns: - Tuple of (success, status_message) - """ - _ = sender_name # Reserved for future use (e.g., multi-user filtering) - prompt = build_prompt(message) - - # Strategy 1: gnome-terminal (GUI) - if has_display() and has_gnome_terminal(): - logger.info("Using gnome-terminal (DISPLAY available)") - success, msg, _ = spawn_gnome_terminal(prompt) - return success, msg - - # Strategy 2: tmux (headless) - if has_tmux(): - logger.info("Using tmux (headless fallback)") - success, msg, _ = spawn_tmux(prompt) - return success, msg - - # No spawner available - error_msg = "No spawner available. Install gnome-terminal (GUI) or tmux (headless)." - logger.error(error_msg) - return False, error_msg - - -# ============================================= -# RESPONSE CHUNKING -# ============================================= - -def chunk_response(text: str, limit: int = TELEGRAM_CHAR_LIMIT) -> List[str]: - """ - Split text into chunks for Telegram's message limit. - - Attempts to split at sentence boundaries when possible. - - Args: - text: The full response text - limit: Maximum characters per chunk (default 4096) - - Returns: - List of text chunks, each within the limit - """ - if len(text) <= limit: - return [text] - - chunks: List[str] = [] - remaining = text - - while remaining: - if len(remaining) <= limit: - chunks.append(remaining) - break - - # Try to find a sentence boundary within limit - chunk = remaining[:limit] - - # Look for sentence endings (. ! ?) followed by space or newline - best_break = -1 - for i in range(len(chunk) - 1, max(0, len(chunk) - 500), -1): - if chunk[i] in '.!?' and (i + 1 >= len(chunk) or chunk[i + 1] in ' \n'): - best_break = i + 1 - break - - # If no sentence boundary, try paragraph break - if best_break == -1: - newline_pos = chunk.rfind('\n\n') - if newline_pos > limit // 2: - best_break = newline_pos + 2 - - # If still no good break, try single newline - if best_break == -1: - newline_pos = chunk.rfind('\n') - if newline_pos > limit // 2: - best_break = newline_pos + 1 - - # Last resort: break at space - if best_break == -1: - space_pos = chunk.rfind(' ') - if space_pos > limit // 2: - best_break = space_pos + 1 - - # Ultimate fallback: hard break at limit - if best_break == -1: - best_break = limit - - chunks.append(remaining[:best_break].rstrip()) - remaining = remaining[best_break:].lstrip() - - return chunks - - -# ============================================= -# CAPTURE MODE - Run Claude and capture output -# ============================================= - -def _chat_id_to_uuid(chat_id: int) -> str: - """Generate a deterministic UUID from a Telegram chat ID.""" - namespace = uuid.UUID("a1a55000-0000-4000-8000-000000000000") - return str(uuid.uuid5(namespace, str(chat_id))) - - -async def _run_claude_cmd( - args: List[str], - timeout: int = DEFAULT_TIMEOUT, - target_cwd: Optional[Path] = None -) -> Tuple[bool, str, Optional[str]]: - """ - Execute a Claude CLI command and capture output. - - Uses subprocess_exec (no shell) to prevent shell injection attacks. - Parses stream-json output via OutputParser. - - Args: - args: Command arguments as a list (e.g. [CLAUDE_BIN, '-p', prompt, ...]) - timeout: Maximum seconds to wait for response - target_cwd: Working directory for the Claude process (defaults to DEFAULT_SESSION_PATH) - - Returns: - Tuple of (success, response_text or error_message, session_id or None) - """ - session_cwd = target_cwd or DEFAULT_SESSION_PATH - try: - process = await asyncio.create_subprocess_exec( - *args, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=session_cwd - ) - - try: - raw_stdout, raw_stderr = await asyncio.wait_for( - process.communicate(), - timeout=timeout - ) - except asyncio.TimeoutError: - process.kill() - await process.wait() - return False, f"Claude timed out after {timeout} seconds", None - - if process.returncode != 0: - error_text = raw_stderr.decode('utf-8', errors='replace').strip() - return False, f"Claude error: {error_text or 'Unknown error'}", None - - raw_output = raw_stdout.decode('utf-8', errors='replace').strip() - - if not raw_output: - return False, "Claude returned empty response", None - - # Parse stream-json output - result = OutputParser.parse_stream(raw_output) - - if result.success: - logger.info( - "Parsed response: %d chars, session=%s, cost=$%.4f", - len(result.text), - result.session_id or "unknown", - result.cost_usd or 0.0, - ) - return True, result.text, result.session_id - else: - error = result.error_message or "Parse failed" - logger.warning("Parse error: %s", error) - return False, error, result.session_id - - except Exception as e: - return False, f"Capture failed: {str(e)}", None - - -async def run_claude_capture( - message: str, - chat_id: int = 0, - timeout: int = DEFAULT_TIMEOUT, - raw_prompt: bool = False, - target_cwd: Optional[Path] = None -) -> Tuple[bool, str, Optional[str]]: - """ - Run Claude CLI and capture its response with session persistence. - - Strategy (when chat_id provided): - 1. Look up stored session_id → try --resume with it - 2. If no stored or resume fails → create new with random UUID - 3. If random create fails → fall back to deterministic UUID - - Args: - message: The message/prompt to send to Claude - chat_id: Telegram chat ID for session persistence - timeout: Maximum seconds to wait for response - raw_prompt: If True, use message as-is (skip build_prompt_clean) - target_cwd: Working directory for the Claude process (branch targeting) - - Returns: - Tuple of (success, response_text or error_message, session_id or None) - """ - if raw_prompt: - prompt = message - else: - prompt = build_prompt_clean(message) - - if chat_id: - # Strategy 1: Try stored session_id from session_store - stored = get_session(chat_id) - if stored and stored.get("session_id"): - stored_id = stored["session_id"] - resume_args = [ - CLAUDE_BIN, '-p', prompt, - '--resume', stored_id, - '--output-format', 'stream-json', '--verbose', - '--permission-mode', 'bypassPermissions' - ] - logger.info("Attempting resume with stored session %s for chat %s", stored_id[:8], chat_id) - - success, response, session_id = await _run_claude_cmd(resume_args, timeout, target_cwd=target_cwd) - if success: - logger.info("Stored session resumed - response captured (%d chars)", len(response)) - return True, response, session_id - - logger.info("Stored session resume failed for chat %s, creating new session", chat_id) - - # Strategy 2: Create new session with random UUID - random_id = str(uuid.uuid4()) - logger.info("Creating new session for chat %s with random ID %s", chat_id, random_id[:8]) - create_args = [ - CLAUDE_BIN, '-p', prompt, - '--session-id', random_id, - '--output-format', 'stream-json', '--verbose', - '--permission-mode', 'bypassPermissions' - ] - success, response, session_id = await _run_claude_cmd(create_args, timeout, target_cwd=target_cwd) - if success: - return True, response, session_id - - # Strategy 3: Deterministic UUID as last resort - deterministic_id = _chat_id_to_uuid(chat_id) - logger.info("Random create failed, falling back to deterministic ID %s", deterministic_id[:8]) - fallback_args = [ - CLAUDE_BIN, '-p', prompt, - '--resume', deterministic_id, - '--output-format', 'stream-json', '--verbose', - '--permission-mode', 'bypassPermissions' - ] - return await _run_claude_cmd(fallback_args, timeout, target_cwd=target_cwd) - else: - # No chat_id - one-shot with no session ID - one_shot_args = [ - CLAUDE_BIN, '-p', prompt, - '--output-format', 'stream-json', '--verbose', - '--permission-mode', 'bypassPermissions' - ] - logger.info("Running Claude in one-shot mode (timeout: %ds)", timeout) - return await _run_claude_cmd(one_shot_args, timeout, target_cwd=target_cwd) - - -async def spawn_and_capture( - sender_name: str, - message: str, - chat_id: int = 0, - timeout: int = DEFAULT_TIMEOUT, - target_cwd: Optional[Path] = None -) -> Tuple[bool, List[str], Optional[str]]: - """ - Run Claude and return chunked response for Telegram. - - This is the main entry point for capture mode. - - Args: - sender_name: Telegram sender name/username (for logging) - message: The message text from Telegram - chat_id: Telegram chat ID (for logging) - timeout: Maximum seconds to wait for Claude - target_cwd: Working directory for the Claude process (branch targeting) - - Returns: - Tuple of (success, list_of_response_chunks, session_id or None) - """ - logger.info("spawn_and_capture called by %s (chat_id: %s)", sender_name, chat_id) - - success, response, session_id = await run_claude_capture(message, chat_id, timeout, target_cwd=target_cwd) - - if not success: - return False, [response], session_id - - chunks = chunk_response(response) - logger.info("Response split into %d chunk(s)", len(chunks)) - - return True, chunks, session_id - - -if __name__ == "__main__": - # Test the spawner - print("=" * 60) - print("TELEGRAM CLAUDE SPAWNER") - print("=" * 60) - print() - print("Environment check:") - print(f" DISPLAY: {os.environ.get('DISPLAY', 'Not set')}") - print(f" gnome-terminal: {'Available' if has_gnome_terminal() else 'Not found'}") - print(f" tmux: {'Available' if has_tmux() else 'Not found'}") - print() - print(f"Session path: {SESSION_PATH}") - print(f"tmux session name: {TMUX_SESSION_NAME}") - print() - print("Would spawn with:") - if has_display() and has_gnome_terminal(): - print(" → gnome-terminal (GUI mode)") - elif has_tmux(): - print(" → tmux (headless mode)") - else: - print(" → ERROR: No spawner available") - print() diff --git a/src/aipass/api/apps/handlers/telegram/telegram_standards.py b/src/aipass/api/apps/handlers/telegram/telegram_standards.py deleted file mode 100644 index 14d1b58f..00000000 --- a/src/aipass/api/apps/handlers/telegram/telegram_standards.py +++ /dev/null @@ -1,370 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: telegram_standards.py - Shared Telegram Bot Standards -# Date: 2026-02-15 -# Version: 1.0.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-15): Initial - shared commands, templates, and utilities for all bots -# -# CODE STANDARDS: -# - stdlib ONLY (importable by all bots) -# - Provides text generation, not message delivery -# - Single source of truth for standard bot commands -# ============================================= - -""" -Shared Telegram Bot Standards for AIPass - -Central definitions for standard commands, response templates, and text -builders used by ALL AIPass Telegram bots (bridge, assistant, test, scheduler). - -Two bot types exist in AIPass: - - Async bots (python-telegram-bot): Use the text builders directly and - deliver via their own async handlers. - - Sync bots (stdlib urllib): Use parse_command() and handle_standard_command() - to process commands, then deliver via their own send functions. - -This module has ZERO external dependencies (stdlib only). Every bot can import it. - -Usage (async bot): - from aipass.api.apps.handlers.telegram.telegram_standards import ( - build_welcome_text, build_status_text, STANDARD_COMMANDS, - ) - -Usage (sync bot): - from aipass.api.apps.handlers.telegram.telegram_standards import ( - parse_command, handle_standard_command, STANDARD_COMMANDS, - ) -""" - -import subprocess -from typing import Optional - - -# ============================================= -# STANDARD COMMAND REGISTRY -# ============================================= - -STANDARD_COMMANDS: dict[str, dict[str, str]] = { - "start": { - "description": "Welcome message and command list", - "menu_text": "Start / welcome message", - }, - "help": { - "description": "Show available commands", - "menu_text": "Show help", - }, - "new": { - "description": "Kill current session and start fresh (clean Claude context)", - "menu_text": "Fresh session", - }, - "status": { - "description": "Show session info (branch, uptime, session state)", - "menu_text": "Session status", - }, -} - - -# ============================================= -# RESPONSE TEMPLATES -# ============================================= - -PROCESSING_MSG = "Processing..." - -ERROR_TEMPLATE = "Something went wrong: {error}" - -HELP_FOOTER = "\nSend any message to chat with Claude." - -# Internal templates (used by builder functions) -_WELCOME_HEADER = "Hello! I'm {bot_name}." -_WELCOME_BRANCH = "Branch: @{branch_name}" - -_STATUS_HEADER = "Session Status" - - -# ============================================= -# TEXT BUILDERS -# ============================================= - -def _format_command_list( - standard_commands: dict[str, dict[str, str]], - custom_commands: Optional[dict[str, dict[str, str]]] = None, -) -> str: - """ - Format a combined command list as readable text. - - Each command appears as: /command - description - - Args: - standard_commands: The STANDARD_COMMANDS dict (or a subset). - custom_commands: Optional additional commands in the same format. - - Returns: - Multi-line string of formatted commands. - """ - lines: list[str] = [] - for cmd, info in standard_commands.items(): - lines.append(f"/{cmd} - {info['description']}") - if custom_commands: - for cmd, info in custom_commands.items(): - lines.append(f"/{cmd} - {info['description']}") - return "\n".join(lines) - - -def build_help_text( - standard_commands: Optional[dict[str, dict[str, str]]] = None, - custom_commands: Optional[dict[str, dict[str, str]]] = None, -) -> str: - """ - Build a /help message combining standard and custom commands. - - Args: - standard_commands: Command registry dict. Defaults to STANDARD_COMMANDS. - custom_commands: Optional bot-specific commands in the same format. - - Returns: - Formatted help text string. - """ - if standard_commands is None: - standard_commands = STANDARD_COMMANDS - - parts: list[str] = [ - "Commands:", - _format_command_list(standard_commands, custom_commands), - HELP_FOOTER, - ] - return "\n".join(parts) - - -def build_welcome_text( - bot_name: str, - branch_name: str, - standard_commands: Optional[dict[str, dict[str, str]]] = None, - custom_commands: Optional[dict[str, dict[str, str]]] = None, -) -> str: - """ - Build the /start welcome message. - - Args: - bot_name: Display name of the bot (e.g., "AIPass Bridge Bot"). - branch_name: The branch this bot operates on (e.g., "dev_central"). - standard_commands: Command registry dict. Defaults to STANDARD_COMMANDS. - custom_commands: Optional bot-specific commands in the same format. - - Returns: - Formatted welcome text string. - """ - if standard_commands is None: - standard_commands = STANDARD_COMMANDS - - parts: list[str] = [ - _WELCOME_HEADER.format(bot_name=bot_name), - _WELCOME_BRANCH.format(branch_name=branch_name), - "", - "Commands:", - _format_command_list(standard_commands, custom_commands), - HELP_FOOTER, - ] - return "\n".join(parts) - - -def build_status_text( - session_name: str, - branch_name: str, - uptime: Optional[str] = None, - message_count: Optional[int] = None, - chat_id: Optional[str | int] = None, -) -> str: - """ - Build the /status response. - - Checks tmux session state via subprocess. Reports branch, session, - activity status, and optional metrics. - - Args: - session_name: tmux session name (e.g., "telegram-assistant"). - branch_name: Branch name (e.g., "assistant"). - uptime: Optional human-readable uptime string. - message_count: Optional count of messages processed. - chat_id: Optional Telegram chat ID to display. - - Returns: - Formatted status text string. - """ - active = _tmux_session_exists(session_name) - - lines: list[str] = [_STATUS_HEADER] - if chat_id is not None: - lines.append(f"Chat ID: {chat_id}") - lines.append(f"Branch: @{branch_name}") - lines.append(f"Session: {session_name}") - lines.append(f"State: {'Active' if active else 'Inactive'}") - if uptime: - lines.append(f"Uptime: {uptime}") - if message_count is not None: - lines.append(f"Messages: {message_count}") - - return "\n".join(lines) - - -def build_botfather_commands( - standard_commands: Optional[dict[str, dict[str, str]]] = None, - custom_commands: Optional[dict[str, dict[str, str]]] = None, -) -> list[dict[str, str]]: - """ - Build command list for BotFather setMyCommands API. - - Returns the format expected by Telegram's setMyCommands endpoint: - [{"command": "start", "description": "Start / welcome message"}, ...] - - Args: - standard_commands: Command registry dict. Defaults to STANDARD_COMMANDS. - custom_commands: Optional bot-specific commands in the same format. - - Returns: - List of dicts with "command" and "description" keys. - """ - if standard_commands is None: - standard_commands = STANDARD_COMMANDS - - result: list[dict[str, str]] = [] - for cmd, info in standard_commands.items(): - result.append({"command": cmd, "description": info["menu_text"]}) - if custom_commands: - for cmd, info in custom_commands.items(): - result.append({"command": cmd, "description": info["menu_text"]}) - return result - - -# ============================================= -# SYNC BOT UTILITIES (stdlib bots) -# ============================================= - -def parse_command(text: str) -> Optional[tuple[str, str]]: - """ - Extract command name and arguments from message text. - - Handles both '/command' and '/command@bot_username' formats. - Returns None if the text is not a command. - - Args: - text: Raw message text from Telegram. - - Returns: - Tuple of (command_name, args_string) or None if not a command. - command_name is lowercase without the leading slash. - args_string is everything after the command, stripped. - - Examples: - parse_command("/status") -> ("status", "") - parse_command("/new please") -> ("new", "please") - parse_command("/help@mybot") -> ("help", "") - parse_command("hello world") -> None - """ - if not text or not text.startswith("/"): - return None - - # Split on whitespace: first part is /command[@botname], rest is args - parts = text.split(None, 1) - raw_command = parts[0][1:] # Remove leading / - args = parts[1] if len(parts) > 1 else "" - - # Strip @bot_username suffix if present - if "@" in raw_command: - raw_command = raw_command.split("@", 1)[0] - - command = raw_command.lower().strip() - if not command: - return None - - return (command, args.strip()) - - -def handle_standard_command( - command: str, - session_name: str, - branch_name: str, - bot_name: str, - custom_commands: Optional[dict[str, dict[str, str]]] = None, - chat_id: Optional[str | int] = None, - message_count: Optional[int] = None, - uptime: Optional[str] = None, -) -> Optional[str | tuple[str, str]]: - """ - Handle a standard command and return the response text. - - For most commands, returns a string with the response text. - For /new, returns a tuple ("new", instructions_text) to signal - the caller that they need to kill and restart their tmux session. - The caller is responsible for tmux operations and for sending - the response text. - - Returns None if the command is not a standard command. - - Args: - command: The command name (lowercase, no slash). - session_name: tmux session name (e.g., "telegram-assistant"). - branch_name: Branch name (e.g., "assistant"). - bot_name: Display name of the bot. - custom_commands: Optional bot-specific commands for help text. - chat_id: Optional Telegram chat ID (for /status display). - message_count: Optional message count (for /status display). - uptime: Optional uptime string (for /status display). - - Returns: - - str: Response text for /start, /help, /status - - tuple[str, str]: ("new", response_text) for /new command - - None: Command is not a standard command - """ - if command == "start": - return build_welcome_text( - bot_name=bot_name, - branch_name=branch_name, - custom_commands=custom_commands, - ) - - if command == "help": - return build_help_text(custom_commands=custom_commands) - - if command == "new": - response_text = f"Session cleared for @{branch_name}. Next message starts fresh." - return ("new", response_text) - - if command == "status": - return build_status_text( - session_name=session_name, - branch_name=branch_name, - uptime=uptime, - message_count=message_count, - chat_id=chat_id, - ) - - return None - - -# ============================================= -# INTERNAL HELPERS -# ============================================= - -def _tmux_session_exists(session_name: str) -> bool: - """ - Check if a tmux session exists by name. - - Args: - session_name: The tmux session name to check. - - Returns: - True if the session is running, False otherwise. - """ - try: - result = subprocess.run( - ["tmux", "has-session", "-t", session_name], - capture_output=True, - ) - return result.returncode == 0 - except FileNotFoundError: - # tmux not installed - return False diff --git a/src/aipass/api/apps/handlers/telegram/tmux_manager.py b/src/aipass/api/apps/handlers/telegram/tmux_manager.py deleted file mode 100644 index 8e930a8c..00000000 --- a/src/aipass/api/apps/handlers/telegram/tmux_manager.py +++ /dev/null @@ -1,314 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: tmux_manager.py - tmux Session Manager for Telegram Bridge -# Date: 2026-02-12 -# Version: 1.2.0 -# Category: api/handlers/telegram -# -# CHANGELOG (Max 5 entries): -# - v1.2.0 (2026-03-01): Add AIPASS_SESSION_TYPE env var + auto /rename for Claude session identification -# - v1.1.0 (2026-02-24): Add bot_id awareness — AIPASS_BOT_ID env var in tmux sessions -# - v1.0.0 (2026-02-12): Initial - tmux session create/kill/send/list for persistent Claude sessions -# -# CODE STANDARDS: -# - Pure functions with proper error raising -# - No Prax imports (handler tier 3) -# ============================================= - -""" -tmux Session Manager for Telegram Bridge - -Manages persistent Claude Code sessions in tmux: -- Create named sessions (telegram-{branch_name}) running Claude Code -- Inject messages via tmux send-keys -l (literal mode) -- Kill/list sessions -- Capture pane content for status display - -Each tmux session runs `claude --permission-mode bypassPermissions` continuously. -Messages are injected via send-keys, responses captured via Stop hook. -""" - -# Infrastructure -import sys -from pathlib import Path - -import asyncio -import shutil -import subprocess -import time -from typing import List, Optional - - -# Constants -SESSION_PREFIX = "telegram-" -DEFAULT_BRANCH = "dev_central" -CLAUDE_BIN = shutil.which("claude") or "claude" -SEND_KEYS_DELAY = 0.5 # seconds between text injection and Enter - - -RENAME_DELAY = 3 # seconds to wait for Claude to initialize before /rename - - -def _session_name(branch_name: str) -> str: - """Build tmux session name from branch name.""" - return f"{SESSION_PREFIX}{branch_name}" - - -def _send_rename(session_name: str, branch_name: str) -> None: - """Send /rename to a tmux session after Claude initializes.""" - time.sleep(RENAME_DELAY) - rename_cmd = f"/rename {branch_name.upper()}-telegram" - subprocess.run( - ["tmux", "send-keys", "-t", session_name, rename_cmd, "Enter"], - capture_output=True, - ) - - -def has_tmux() -> bool: - """Check if tmux is available on the system.""" - return shutil.which("tmux") is not None - - -def session_exists(branch_name: str) -> bool: - """ - Check if a tmux session exists for the given branch. - - Args: - branch_name: Branch name (e.g. 'dev_central') - - Returns: - True if session is alive - """ - name = _session_name(branch_name) - result = subprocess.run( - ["tmux", "has-session", "-t", name], - capture_output=True, - ) - return result.returncode == 0 - - -def create_session(branch_name: str, branch_path: Path, *, bot_id: Optional[str] = None) -> bool: - """ - Create a tmux session and launch Claude Code inside it. - - Session is named telegram-{branch_name} and starts in branch_path. - Claude is launched with AIPASS_SESSION_TYPE=telegram and - --permission-mode bypassPermissions. After initialization, - sends /rename BRANCH-telegram for the /resume picker. - - Args: - branch_name: Branch name for session naming - branch_path: Working directory for Claude Code - bot_id: Optional bot ID — sets AIPASS_BOT_ID env var in tmux session - - Returns: - True if session was created successfully - """ - name = _session_name(branch_name) - - if session_exists(branch_name): - print("[INFO]", "Session %s already exists", name) - return True - - if not branch_path.is_dir(): - print("[ERROR]", "Branch path does not exist: %s", branch_path) - return False - - try: - # Create detached tmux session - result = subprocess.run( - [ - "tmux", "new-session", - "-d", # Detached - "-s", name, # Session name - "-c", str(branch_path), # Working directory - ], - capture_output=True, - text=True, - ) - - if result.returncode != 0: - print("[ERROR]", "Failed to create tmux session %s: %s", name, result.stderr) - return False - - # Set bot_id environment variable if provided - if bot_id: - subprocess.run( - ["tmux", "set-environment", "-t", name, "AIPASS_BOT_ID", bot_id], - capture_output=True, - text=True, - ) - - # Launch Claude Code inside the session - # Explicit cd guarantees CWD even if shell profile drifts it - # AIPASS_SESSION_TYPE=telegram lets drone status label this session - claude_cmd = ( - f"cd '{branch_path}' && " - f"AIPASS_SESSION_TYPE=telegram {CLAUDE_BIN} --permission-mode bypassPermissions" - ) - subprocess.run( - ["tmux", "send-keys", "-t", name, claude_cmd, "Enter"], - capture_output=True, - ) - - # Rename the Claude conversation for the /resume picker - # Claude needs a few seconds to initialize before /rename works - _send_rename(name, branch_name) - - print("[INFO]", "Created tmux session %s at %s", name, branch_path) - return True - - except Exception as e: - print("[ERROR]", "Error creating tmux session %s: %s", name, e) - return False - - -async def send_message(branch_name: str, message: str) -> bool: - """ - Inject a message into a tmux session via send-keys. - - Uses -l flag for literal mode (no shell interpretation). - Sends text first, waits briefly, then sends Enter. - - Args: - branch_name: Branch name identifying the session - message: The message text to inject - - Returns: - True if message was sent successfully - """ - name = _session_name(branch_name) - - if not session_exists(branch_name): - print("[ERROR]", "Session %s does not exist", name) - return False - - try: - # Send text literally (no shell interpretation) - result = subprocess.run( - ["tmux", "send-keys", "-t", name, "-l", message], - capture_output=True, - text=True, - ) - - if result.returncode != 0: - print("[ERROR]", "Failed to send text to %s: %s", name, result.stderr) - return False - - # Wait before sending Enter (prevents rapid keystroke issues) - await asyncio.sleep(SEND_KEYS_DELAY) - - # Send Enter to submit the message - result = subprocess.run( - ["tmux", "send-keys", "-t", name, "Enter"], - capture_output=True, - text=True, - ) - - if result.returncode != 0: - print("[ERROR]", "Failed to send Enter to %s: %s", name, result.stderr) - return False - - print("[INFO]", "Injected message into %s (%d chars)", name, len(message)) - return True - - except Exception as e: - print("[ERROR]", "Error sending to tmux session %s: %s", name, e) - return False - - -def kill_session(branch_name: str) -> bool: - """ - Kill a tmux session for the given branch. - - Args: - branch_name: Branch name identifying the session - - Returns: - True if session was killed (or didn't exist) - """ - name = _session_name(branch_name) - - if not session_exists(branch_name): - print("[INFO]", "Session %s does not exist, nothing to kill", name) - return True - - try: - result = subprocess.run( - ["tmux", "kill-session", "-t", name], - capture_output=True, - text=True, - ) - - if result.returncode == 0: - print("[INFO]", "Killed tmux session %s", name) - return True - else: - print("[ERROR]", "Failed to kill session %s: %s", name, result.stderr) - return False - - except Exception as e: - print("[ERROR]", "Error killing tmux session %s: %s", name, e) - return False - - -def list_sessions() -> List[str]: - """ - List all active telegram-* tmux sessions. - - Returns: - List of branch names with active sessions - """ - try: - result = subprocess.run( - ["tmux", "list-sessions", "-F", "#{session_name}"], - capture_output=True, - text=True, - ) - - if result.returncode != 0: - return [] - - sessions = [] - for line in result.stdout.strip().split("\n"): - line = line.strip() - if line.startswith(SESSION_PREFIX): - branch = line[len(SESSION_PREFIX):] - if branch: - sessions.append(branch) - - return sessions - - except Exception: - return [] - - -def get_session_pane(branch_name: str) -> Optional[str]: - """ - Capture current visible pane content from a tmux session. - - Args: - branch_name: Branch name identifying the session - - Returns: - Pane content as string, or None if session doesn't exist - """ - name = _session_name(branch_name) - - if not session_exists(branch_name): - return None - - try: - result = subprocess.run( - ["tmux", "capture-pane", "-t", name, "-p"], - capture_output=True, - text=True, - ) - - if result.returncode == 0: - return result.stdout - return None - - except Exception: - return None diff --git a/src/aipass/api/apps/handlers/telegram_service/__init__.py b/src/aipass/api/apps/handlers/telegram_service/__init__.py deleted file mode 100644 index fe53ea9c..00000000 --- a/src/aipass/api/apps/handlers/telegram_service/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# ===================AIPASS==================== -# META DATA HEADER -# Name: __init__.py - Telegram Service Handler Package -# Date: 2026-02-03 -# Version: 1.0.0 -# Category: api/handlers -# CODE STANDARDS: Seed v1.0.0 -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-03): Initial package -# ============================================= - -"""Telegram Service Handler - systemd control operations""" - -from aipass.api.apps.handlers.telegram_service.service import ( - start_service, - stop_service, - get_status, - get_logs, - SERVICE_NAME, -) - -__all__ = [ - "start_service", - "stop_service", - "get_status", - "get_logs", - "SERVICE_NAME", -] diff --git a/src/aipass/api/apps/handlers/telegram_service/service.py b/src/aipass/api/apps/handlers/telegram_service/service.py deleted file mode 100644 index 9e4de694..00000000 --- a/src/aipass/api/apps/handlers/telegram_service/service.py +++ /dev/null @@ -1,121 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: service.py - Telegram Service Handler -# Date: 2026-02-03 -# Version: 1.0.0 -# Category: api/handlers -# CODE STANDARDS: Seed v1.0.0 -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-03): Initial handler - systemd service operations -# ============================================= - -""" -Telegram Service Handler - -Low-level systemd operations for telegram-bridge service. -""" - -import os -import subprocess -from pathlib import Path -from typing import Tuple - -def _find_repo_root() -> Path: - """Walk up from this file to find AIPASS_REGISTRY.json (repo root).""" - current = Path(__file__).resolve().parent - for parent in [current] + list(current.parents): - if (parent / "AIPASS_REGISTRY.json").exists(): - return parent - return Path.cwd() - -SERVICE_NAME = "telegram-bridge" -LOG_FILE = _find_repo_root() / "logs" / "telegram_bridge.log" - - -def start_service() -> Tuple[bool, str]: - """ - Start the telegram-bridge service - - Returns: - Tuple of (success, message) - """ - result = subprocess.run( - ["systemctl", "--user", "start", SERVICE_NAME], - capture_output=True, - text=True - ) - - if result.returncode == 0: - return True, "Service started" - return False, result.stderr.strip() if result.stderr else "Unknown error" - - -def stop_service() -> Tuple[bool, str]: - """ - Stop the telegram-bridge service - - Returns: - Tuple of (success, message) - """ - result = subprocess.run( - ["systemctl", "--user", "stop", SERVICE_NAME], - capture_output=True, - text=True - ) - - if result.returncode == 0: - return True, "Service stopped" - return False, result.stderr.strip() if result.stderr else "Unknown error" - - -def get_status() -> Tuple[str, str]: - """ - Get telegram-bridge service status - - Returns: - Tuple of (status_code, details) - status_code: 'running', 'stopped', 'not_found', 'unknown' - """ - result = subprocess.run( - ["systemctl", "--user", "status", SERVICE_NAME], - capture_output=True, - text=True - ) - - output = result.stdout if result.stdout else result.stderr - - if "Active: active (running)" in output: - return "running", output - elif "Active: inactive (dead)" in output: - return "stopped", output - elif "could not be found" in output.lower(): - return "not_found", output - return "unknown", output - - -def get_logs(lines: int = 30) -> Tuple[bool, str]: - """ - Get recent service logs - - Args: - lines: Number of recent lines to return - - Returns: - Tuple of (success, log_content or error_message) - """ - if not LOG_FILE.exists(): - return False, f"No log file found at {LOG_FILE}" - - try: - with open(LOG_FILE, encoding="utf-8") as f: - all_lines = f.readlines() - recent = all_lines[-lines:] if len(all_lines) > lines else all_lines - - if not recent: - return False, "Log file is empty" - - return True, "".join(recent) - except OSError as e: - return False, f"Failed to read logs: {e}" diff --git a/src/aipass/api/apps/modules/telegram_bot.py b/src/aipass/api/apps/modules/telegram_bot.py deleted file mode 100644 index d902448d..00000000 --- a/src/aipass/api/apps/modules/telegram_bot.py +++ /dev/null @@ -1,415 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: telegram_bot.py - Telegram multi-bot module (replaces telegram_bridge.py + telegram_chat.py) -# Date: 2026-02-24 -# Version: 1.0.0 -# Category: api/modules -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-24): Initial - unified multi-bot public API module -# -# CODE STANDARDS: -# - Module layer: thin orchestration over handler functions -# - This is the PUBLIC API that other branches import from -# - Replaces telegram_bridge.py and telegram_chat.py -# ============================================= - -""" -Telegram Multi-Bot Module - Public API - -Unified entry point for the AIPass multi-bot Telegram architecture. -Re-exports all handler internals so other branches can import them -without triggering the cross-branch handler guard. - -Replaces: - - telegram_bridge.py (bridge operations) - - telegram_chat.py (direct chat + standards re-exports) - -Usage from other branches: - from aipass.api.apps.modules.telegram_bot import BaseBot, BranchPlugin - from aipass.api.apps.modules.telegram_bot import create_bot, delete_bot - from aipass.api.apps.modules.telegram_bot import list_bots, get_bot - from aipass.api.apps.modules.telegram_bot import load_bot_config - from aipass.api.apps.modules.telegram_bot import ( - STANDARD_COMMANDS, build_help_text, build_welcome_text, - build_status_text, parse_command, handle_standard_command, - ) -""" - -# Infrastructure -import sys -from pathlib import Path - -# Prax logger and CLI utilities -from aipass.prax.apps.modules.logger import system_logger as logger -from aipass.cli.apps.modules import console, header, success, error, warning - -# Handler imports (operations) -from aipass.api.apps.handlers.telegram import bot_operations - -# ============================================= -# RE-EXPORTS FROM HANDLERS (Public API) -# ============================================= - -from aipass.api.apps.handlers.telegram.base_bot import BaseBot -from aipass.api.apps.handlers.telegram.branch_plugin import BranchPlugin -from aipass.api.apps.handlers.telegram.bot_registry import ( - list_bots, - get_bot, - register_bot, - get_bot_by_branch, -) -from aipass.api.apps.handlers.telegram.bot_factory import create_bot, delete_bot -from aipass.api.apps.handlers.telegram.config import load_bot_config -from aipass.api.apps.handlers.telegram.telegram_standards import ( - STANDARD_COMMANDS, - PROCESSING_MSG, - build_help_text, - build_welcome_text, - build_status_text, - build_botfather_commands, - parse_command, - handle_standard_command, -) - -# ============================================= -# MODULE INTROSPECTION -# ============================================= - - -def print_introspection() -> None: - """Show module introspection - connected handlers and capabilities""" - console.print() - header("Telegram Multi-Bot Module Introspection") - console.print() - - console.print("[cyan]Purpose:[/cyan] Multi-bot Telegram architecture for AIPass") - console.print() - - console.print("[cyan]Connected Handlers:[/cyan]") - console.print(" - api.apps.handlers.telegram.base_bot") - console.print(" - api.apps.handlers.telegram.branch_plugin") - console.print(" - api.apps.handlers.telegram.bot_registry") - console.print(" - api.apps.handlers.telegram.bot_factory") - console.print(" - api.apps.handlers.telegram.bot_operations") - console.print(" - api.apps.handlers.telegram.config") - console.print(" - api.apps.handlers.telegram.telegram_standards") - console.print() - - console.print("[cyan]Available Commands:[/cyan]") - console.print(" - start - Start a specific bot (polling loop)") - console.print(" - stop - Stop a bot's systemd service") - console.print(" - status [bot_id] - Show bot status (single or all)") - console.print(" - list - List all registered bots") - console.print(" - create [options] - Create new bot") - console.print(" - delete - Delete a bot") - console.print() - - -def print_help() -> None: - """Print module help with argparse""" - import argparse - - parser = argparse.ArgumentParser( - prog="python3 telegram_bot.py", - description="Telegram Multi-Bot Module - AIPass multi-bot management", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -COMMANDS: - start - Start a bot (long-polling, blocks) - stop - Stop a bot's systemd service - status [bot_id] - Show bot status (single or all) - list - List all registered bots - create [opts] - Create a new bot - delete - Delete a bot - -CREATE OPTIONS: - --branch - Associate with an AIPass branch - --work-dir - Working directory for Claude sessions - -EXAMPLES: - # Start a bot (blocks until Ctrl+C) - python3 telegram_bot.py start dev_central - - # Check all bot statuses - python3 telegram_bot.py status - - # Create a new branch bot - python3 telegram_bot.py create dev_central 123:ABC --branch dev_central - - # List all bots - python3 telegram_bot.py list - """ - ) - - subparsers = parser.add_subparsers(dest="command", help="Available commands") - - start_p = subparsers.add_parser("start", help="Start a bot") - start_p.add_argument("bot_id", help="Bot identifier") - - stop_p = subparsers.add_parser("stop", help="Stop a bot's systemd service") - stop_p.add_argument("bot_id", help="Bot identifier") - - status_p = subparsers.add_parser("status", help="Show bot status") - status_p.add_argument("bot_id", nargs="?", help="Bot identifier (optional)") - - subparsers.add_parser("list", help="List all registered bots") - - create_p = subparsers.add_parser("create", help="Create a new bot") - create_p.add_argument("bot_id", help="Bot identifier") - create_p.add_argument("token", help="Telegram bot token") - create_p.add_argument("--branch", help="AIPass branch name") - create_p.add_argument("--work-dir", help="Working directory") - - delete_p = subparsers.add_parser("delete", help="Delete a bot") - delete_p.add_argument("bot_id", help="Bot identifier") - - console.print(parser.format_help()) - - -# ============================================= -# COMMAND HANDLING -# ============================================= - - -def handle_command(command: str, args: list) -> bool: - """ - Handle module commands routed via drone. - - Args: - command: Command name (e.g., "telegram_bot") - args: Command arguments - - Returns: - True if command was handled, False to pass through - """ - if not args: - return False - - subcommand = args[0] - - if subcommand == "start" and len(args) >= 2: - _cmd_start(args[1]) - return True - - elif subcommand == "stop" and len(args) >= 2: - _cmd_stop(args[1]) - return True - - elif subcommand == "status": - bot_id = args[1] if len(args) >= 2 else None - _cmd_status(bot_id) - return True - - elif subcommand == "list": - _cmd_list() - return True - - elif subcommand == "create" and len(args) >= 3: - _cmd_create(args[1:]) - return True - - elif subcommand == "delete" and len(args) >= 2: - _cmd_delete(args[1]) - return True - - elif subcommand == "help": - print_help() - return True - - return False - - -# ============================================= -# THIN ORCHESTRATION (delegates to bot_operations handler) -# ============================================= - - -def _cmd_start(bot_id: str) -> None: - """Orchestrate bot start: header, delegate, exit.""" - header(f"Starting Bot: {bot_id}") - console.print() - - config = load_bot_config(bot_id) - if not config: - error(f"No config found for bot '{bot_id}'") - console.print("[dim]Expected: ~/.aipass/telegram_bots/{bot_id}.json[/dim]") - return - - if not config.get("bot_token"): - error(f"No bot_token in config for '{bot_id}'") - return - - bot_name = config.get("bot_name", f"AIPass {bot_id} Bot") - logger.info("Starting bot '%s' (%s)", bot_id, bot_name) - success(f"Launching {bot_name}...") - console.print() - - exit_code = bot_operations.start_bot(bot_id) - if exit_code is None: - error(f"Bot '{bot_id}' failed to start") - else: - sys.exit(exit_code) - - -def _cmd_stop(bot_id: str) -> None: - """Orchestrate bot stop: header, delegate, display result.""" - header(f"Stopping Bot: {bot_id}") - console.print() - - ok, msg = bot_operations.stop_bot(bot_id) - if ok: - logger.info("Stopped bot '%s'", bot_id) - success(msg) - else: - logger.warning("Failed to stop bot '%s': %s", bot_id, msg) - error(msg) - - console.print() - - -def _cmd_status(bot_id: str | None = None) -> None: - """Orchestrate status display: header, delegate, format.""" - if bot_id: - header(f"Bot Status: {bot_id}") - else: - header("All Bots Status") - console.print() - - bots = bot_operations.get_status(bot_id) - if not bots: - if bot_id: - error(f"Bot '{bot_id}' not found in registry") - else: - warning("No bots registered") - console.print("[dim]Use 'create' to add a new bot[/dim]") - console.print() - return - - for bot in bots: - for line in bot_operations.format_bot_details(bot): - console.print(f" [cyan]{line}[/cyan]") - console.print() - - -def _cmd_list() -> None: - """Orchestrate bot listing: header, delegate, format.""" - header("Registered Bots") - console.print() - - bots = bot_operations.get_all_bots() - if not bots: - warning("No bots registered") - console.print("[dim]Use 'create' to add a new bot[/dim]") - console.print() - return - - for line in bot_operations.format_bot_table(bots): - console.print(line) - console.print() - - -def _cmd_create(args: list) -> None: - """Orchestrate bot creation: parse args, delegate, display result.""" - parsed = bot_operations.parse_create_args(args) - if not parsed: - error("Usage: create [--branch ] [--work-dir ]") - return - - bot_id = parsed["bot_id"] - header(f"Creating Bot: {bot_id}") - console.print() - - result = create_bot( - bot_id=bot_id, - bot_token=parsed["bot_token"], - branch_name=parsed["branch_name"], - work_dir=parsed["work_dir"], - ) - - if result: - logger.info("Bot created via module: %s", bot_id) - success(f"Bot '{bot_id}' created successfully") - console.print() - details = bot_operations.format_bot_details({ - "bot_id": result["bot_id"], - "username": result["username"], - "branch_name": result.get("branch_name"), - "work_dir": result["work_dir"], - "status": "active", - "service_name": result["service_name"], - }) - for line in details: - console.print(f" [cyan]{line}[/cyan]") - else: - logger.warning("Bot creation failed for '%s'", bot_id) - error(f"Failed to create bot '{bot_id}'. Check logs for details.") - - console.print() - - -def _cmd_delete(bot_id: str) -> None: - """Orchestrate bot deletion: verify, delegate, display result.""" - header(f"Deleting Bot: {bot_id}") - console.print() - - bot = get_bot(bot_id) - if not bot: - error(f"Bot '{bot_id}' not found in registry") - console.print() - return - - result = delete_bot(bot_id) - - if result: - logger.info("Bot deleted via module: %s", bot_id) - success(f"Bot '{bot_id}' deleted successfully") - else: - logger.warning("Bot deletion failed for '%s'", bot_id) - error(f"Failed to delete bot '{bot_id}'. Check logs for details.") - - console.print() - - -# ============================================= -# STANDALONE EXECUTION -# ============================================= - -if __name__ == "__main__": - """Standalone execution mode""" - args = sys.argv[1:] - - # Show introspection when run without arguments - if len(args) == 0: - print_introspection() - sys.exit(0) - - # Show help for explicit help flags - if args[0] in ['--help', '-h', 'help']: - print_help() - sys.exit(0) - - # Execute command directly (standalone mode, not drone-routed) - command = args[0] - - if command == "start" and len(args) >= 2: - _cmd_start(args[1]) - elif command == "stop" and len(args) >= 2: - _cmd_stop(args[1]) - elif command == "status": - bot_id = args[1] if len(args) >= 2 else None - _cmd_status(bot_id) - elif command == "list": - _cmd_list() - elif command == "create" and len(args) >= 3: - _cmd_create(args[1:]) - elif command == "delete" and len(args) >= 2: - _cmd_delete(args[1]) - else: - console.print() - console.print(f"[red]Unknown command: {command}[/red]") - console.print() - console.print("Run [dim]python3 telegram_bot.py --help[/dim] for available commands") - console.print() - sys.exit(1) diff --git a/src/aipass/api/apps/modules/telegram_service.py b/src/aipass/api/apps/modules/telegram_service.py deleted file mode 100644 index c8d9e730..00000000 --- a/src/aipass/api/apps/modules/telegram_service.py +++ /dev/null @@ -1,230 +0,0 @@ - -# ===================AIPASS==================== -# META DATA HEADER -# Name: telegram_service.py - Telegram Service Control Module -# Date: 2026-02-03 -# Version: 1.0.0 -# Category: api/modules -# CODE STANDARDS: Seed v1.0.0 -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-02-03): Initial module - systemd service control for telegram bridge -# ============================================= - -""" -Telegram Service Control Module - -Manages the telegram-bridge systemd user service: -- drone @api telegram start → Start service -- drone @api telegram stop → Stop service -- drone @api telegram status → Check service status -""" - -import sys -from pathlib import Path - -from typing import List -from aipass.prax.apps.modules.logger import system_logger as logger -from aipass.cli.apps.modules import console, header, success, error, warning -from aipass.api.apps.handlers.telegram_service import service - - -def print_introspection() -> None: - """Show module introspection""" - console.print() - header("Telegram Service Control Introspection") - console.print() - - console.print("[cyan]Purpose:[/cyan] Manage telegram-bridge systemd service") - console.print() - - console.print("[cyan]Connected Handlers:[/cyan]") - console.print(" • api.apps.handlers.telegram_service.service") - console.print() - - console.print("[cyan]Available Commands:[/cyan]") - console.print(" • telegram start - Start the service") - console.print(" • telegram stop - Stop the service") - console.print(" • telegram status - Check service status") - console.print(" • telegram logs - View recent logs") - console.print() - - console.print("[cyan]Service:[/cyan]") - console.print(f" • Name: {service.SERVICE_NAME}.service") - console.print(" • Type: systemd user service") - console.print() - - -def print_help() -> None: - """Print module help""" - import argparse - - parser = argparse.ArgumentParser( - prog="drone @api telegram", - description="Telegram Service Control - Manage the telegram-bridge service", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -SUBCOMMANDS: - start - Start the telegram-bridge service - stop - Stop the telegram-bridge service - status - Check service status - logs - View recent service logs - -USAGE: - drone @api telegram start - drone @api telegram stop - drone @api telegram status - drone @api telegram logs - -EXAMPLES: - # Start the bot - drone @api telegram start - - # Check if running - drone @api telegram status - - # Stop the bot - drone @api telegram stop - - # View logs - drone @api telegram logs - """ - ) - console.print(parser.format_help()) - - -def handle_command(command: str, args: List[str]) -> bool: - """ - Handle telegram service commands - - Args: - command: Command name (should be 'telegram') - args: Subcommand and arguments - - Returns: - True if command was handled, False otherwise - """ - if command != "telegram": - return False - - try: - if not args: - print_help() - return True - - subcommand = args[0] - - if subcommand == "start": - _handle_start() - elif subcommand == "stop": - _handle_stop() - elif subcommand == "status": - _handle_status() - elif subcommand == "logs": - _handle_logs() - elif subcommand in ["--help", "-h", "help"]: - print_help() - else: - error(f"Unknown subcommand: {subcommand}") - console.print() - console.print("[dim]Use 'drone @api telegram --help' for usage[/dim]") - - return True - except Exception as e: - logger.error("Error in telegram_service.handle_command: %s", str(e)) - raise - - -def _handle_start() -> None: - """Orchestrate service start""" - header("Telegram Bridge Service") - console.print() - - ok, msg = service.start_service() - - if ok: - success(msg) - console.print() - console.print("[dim]Check status: drone @api telegram status[/dim]") - console.print("[dim]View logs: drone @api telegram logs[/dim]") - else: - error(f"Failed to start service: {msg}") - - -def _handle_stop() -> None: - """Orchestrate service stop""" - header("Telegram Bridge Service") - console.print() - - ok, msg = service.stop_service() - - if ok: - success(msg) - else: - error(f"Failed to stop service: {msg}") - - -def _handle_status() -> None: - """Orchestrate status check""" - header("Telegram Bridge Service Status") - console.print() - - status_code, output = service.get_status() - - if status_code == "running": - success("Service is running") - elif status_code == "stopped": - warning("Service is stopped") - elif status_code == "not_found": - error("Service not found - run 'systemctl --user daemon-reload'") - else: - warning("Unknown status") - - console.print() - - for line in output.split("\n"): - line = line.strip() - if any(k in line for k in ["Active:", "Main PID:", "Memory:", "CPU:"]): - console.print(f" {line}") - - console.print() - console.print("[dim]Logs: drone @api telegram logs[/dim]") - - -def _handle_logs() -> None: - """Orchestrate log retrieval""" - header("Telegram Bridge Service Logs") - console.print() - - ok, content = service.get_logs(30) - - if not ok: - warning(content) - return - - console.print(f"[dim]Showing last 30 lines[/dim]") - console.print() - - for line in content.split("\n"): - console.print(line.rstrip()) - - -if __name__ == "__main__": - """Standalone execution mode""" - args = sys.argv[1:] - - if len(args) == 0: - print_introspection() - sys.exit(0) - - if args[0] in ['--help', '-h', 'help']: - print_help() - sys.exit(0) - - if handle_command("telegram", args): - sys.exit(0) - else: - console.print() - console.print(f"[red]Unknown command: {args[0]}[/red]") - console.print() - sys.exit(1) diff --git a/src/aipass/cli/.claude/settings.local.json b/src/aipass/cli/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/cli/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/cli/.seed/bypass.json b/src/aipass/cli/.seed/bypass.json index c33d4a59..c565945b 100644 --- a/src/aipass/cli/.seed/bypass.json +++ b/src/aipass/cli/.seed/bypass.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "created": "2026-03-06T03:16:10.906535", + "created": "2026-03-06T14:27:34.810230", "description": "Standards bypass configuration for this branch" }, "bypass": [], diff --git a/src/aipass/cli/README.md b/src/aipass/cli/README.md index 4d0f6dfe..00e17203 100644 --- a/src/aipass/cli/README.md +++ b/src/aipass/cli/README.md @@ -1,51 +1,50 @@ # CLI -**Purpose:** Terminal display formatting - headers, success, error, warning helpers -**Module:** `aipass.cli` -**Created:** 2026-03-05 +Display and output formatting service for AIPass modules. Provides consistent terminal output — headers, success/error/warning messages, section breaks, and operation templates — so every module looks the same without duplicating Rich formatting code. ---- +## Usage -## Overview +```python +from aipass.cli import header, success, error, warning, section -### What I Do +header("Creating Branch", {"Name": "feature", "Type": "module"}) +success("Files created", items=12, time="2.3s") +error("Path not found", suggestion="Check spelling") +warning("Config missing, using defaults") +section("Results") +``` +### Operation Templates -### How I Work -- **Entry Point:** `apps/cli.py` -- **Pattern:** Auto-discovers and routes to modules +```python +from aipass.cli import operation_start, operation_complete ---- +operation_start("Processing", count=10) +# ... do work ... +operation_complete(created=5, skipped=3, failed=0) +``` + +### Direct Console Access + +```python +from aipass.cli import console + +console.print("[bold cyan]Custom Rich output[/bold cyan]") +``` ## Architecture ``` -CLI/ +cli/ ├── apps/ -│ ├── cli.py # Entry point -│ ├── modules/ # Business logic -│ ├── handlers/ # Implementation -│ └── plugins/ # Extensions -├── docs/ -├── tests/ -├── passport.json # Identity -├── local.json # Session history -├── observations.json # Collaboration patterns -└── README.md +│ ├── cli.py # Entry point +│ ├── modules/ +│ │ ├── display.py # header, success, error, warning, section +│ │ └── templates.py # operation_start, operation_complete +│ └── handlers/ +│ └── json/ # JSON file management +└── tests/ ``` ---- - -## Commands - -*Configure after initialization* - ---- - -## Integration Points - -### Depends On - - -### Provides To - +- `apps/modules/` — Public API. Import from here. +- `apps/handlers/` — Internal implementation. Don't import directly. diff --git a/src/aipass/cli/apps/handlers/__init__.py b/src/aipass/cli/apps/handlers/__init__.py index 5693db54..e164d47c 100755 --- a/src/aipass/cli/apps/handlers/__init__.py +++ b/src/aipass/cli/apps/handlers/__init__.py @@ -64,7 +64,7 @@ def _find_real_caller(): def _extract_branch_name(filepath: str) -> str: """Extract branch name from a file path.""" - parts = filepath.split("/") + parts = Path(filepath).parts for i, part in enumerate(parts): if part == "aipass": if i + 1 < len(parts): diff --git a/src/aipass/cli/apps/handlers/json/json_handler.py b/src/aipass/cli/apps/handlers/json/json_handler.py index 3df5eee2..d574c837 100755 --- a/src/aipass/cli/apps/handlers/json/json_handler.py +++ b/src/aipass/cli/apps/handlers/json/json_handler.py @@ -15,15 +15,27 @@ # - No Prax imports (handler tier 3) # ============================================= +"""JSON Auto-Creating Handler - manages CLI JSON files with templates and auto-rotation.""" + import json from pathlib import Path from datetime import datetime -from typing import Dict, List, Any, Optional -import sys +from typing import Dict, Any, Optional import inspect +from aipass.prax.apps.modules.logger import system_logger as logger -# Constants — package-relative paths (portable across any machine) -CLI_ROOT = Path(__file__).resolve().parents[3] # json_handler.py -> json -> handlers -> apps -> cli +def _find_repo_root() -> Path: + """Walk up from this file to find the repo root (contains pyproject.toml or AIPASS_REGISTRY.json).""" + current = Path(__file__).resolve().parent + for parent in [current] + list(current.parents): + if (parent / "pyproject.toml").exists() or (parent / "AIPASS_REGISTRY.json").exists(): + return parent + return Path.cwd() + + +# Constants — resolved via repo root walk-up (portable across any machine) +_REPO_ROOT = _find_repo_root() +CLI_ROOT = _REPO_ROOT / "src" / "aipass" / "cli" CLI_JSON_DIR = CLI_ROOT / "cli_json" JSON_TEMPLATES_DIR = CLI_ROOT / "apps" / "json_templates" @@ -108,9 +120,8 @@ def ensure_json_exists(module_name: str, json_type: str) -> bool: if validate_json_structure(data, json_type): return True # If corrupted, fall through to regenerate - except Exception: - # If unreadable, fall through to regenerate - pass + except Exception as exc: + logger.warning("JSON unreadable at %s, regenerating: %s", json_path, exc) template = load_template(json_type, module_name) diff --git a/src/aipass/devpulse/.claude/settings.local.json b/src/aipass/devpulse/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/devpulse/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/devpulse/README.md b/src/aipass/devpulse/README.md index c39324d7..a2953e00 100644 --- a/src/aipass/devpulse/README.md +++ b/src/aipass/devpulse/README.md @@ -1,22 +1,17 @@ -# DEVPULSE +# DevPulse -**Purpose:** Orchestration hub for AIPass public repo. Manages dev notes, DPLANs, system status dashboards. The DEV_CENTRAL of AIPass. +**Purpose:** Dev notes and status tracking for AIPass projects **Module:** `aipass.devpulse` -**Created:** 2026-03-06 -**Last Updated:** 2026-03-06 +**Status:** Building --- ## Overview -### What I Do -- Coordinate work across all 10 AIPass modules -- Track system health via seedgo audits and recon reports -- Manage DPLANs and development notes -- Test and wire modules in Docker container environment +DevPulse tracks development notes, plans, and project status across an AIPass ecosystem. It provides a shared notation layer where both humans and agents can log issues, todos, and progress — giving visibility into what's happening without requiring meetings or status emails. -### How I Work -- **Entry Point:** `apps/branch.py` +### How It Works +- **Entry Point:** `apps/devpulse.py` - **Pattern:** Auto-discovers modules in `apps/modules/` with `handle_command()` and routes commands --- @@ -24,27 +19,15 @@ ## Architecture ``` -DEVPULSE/ +devpulse/ ├── apps/ -│ ├── branch.py # Entry point (auto-discovery + routing) -│ ├── modules/ # Business logic (empty, building out) -│ ├── handlers/ # Implementation -│ └── plugins/ # Extensions -├── .trinity/ -│ ├── passport.json # Identity -│ ├── local.json # Session history -│ └── observations.json # Collaboration patterns -├── .agent/ # System metadata -├── .aipass/ # Branch prompt context -├── artifacts/ # Birth certificate +│ ├── devpulse.py # Entry point (auto-discovery + routing) +│ ├── modules/ # Business logic +│ ├── handlers/ # Implementation +│ └── plugins/ # Extensions +├── devpulse_json/ # JSON storage ├── docs/ -│ └── sub_agent_drops/ # Recon reports from sub-agents -├── tools/ -│ └── verify_branch.py # Template verification -├── devpulse_json/ # Branch JSON storage ├── tests/ -├── DASHBOARD.local.json # System status -├── flow.local.md # Issues and todos └── README.md ``` @@ -52,16 +35,33 @@ DEVPULSE/ ## Commands -*No modules built yet — commands will be added as modules are created.* +```bash +drone @devpulse --help # Show available commands +``` + +*Modules are being built out — commands will appear as they ship.* + +--- + +## Python Usage + +```python +from aipass.devpulse.apps.devpulse import discover_modules, route_command + +# Discover available sub-modules +modules = discover_modules() + +# Route a command +route_command("status", [], modules) +``` --- ## Integration Points ### Depends On -- `aipass.prax` — Logger +- `aipass.prax` — Logging - `aipass.cli` — Display formatting -- `aipass.seedgo` — Standards auditing ### Provides To -- All modules — orchestration coordination, system health tracking +- All modules — dev notes, plan tracking, project status diff --git a/src/aipass/devpulse/__init__.py b/src/aipass/devpulse/__init__.py new file mode 100644 index 00000000..1daf88d5 --- /dev/null +++ b/src/aipass/devpulse/__init__.py @@ -0,0 +1 @@ +"""DevPulse - System monitoring and dashboards for AIPass.""" diff --git a/src/aipass/drone/.claude/settings.local.json b/src/aipass/drone/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/drone/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/drone/.seed/bypass.json b/src/aipass/drone/.seed/bypass.json index 2c7d28a1..7452d6be 100644 --- a/src/aipass/drone/.seed/bypass.json +++ b/src/aipass/drone/.seed/bypass.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "created": "2026-03-06T03:16:10.929991", + "created": "2026-03-06T14:27:37.777414", "description": "Standards bypass configuration for this branch" }, "bypass": [], diff --git a/src/aipass/drone/README.md b/src/aipass/drone/README.md index 19697d22..44ee3ea2 100644 --- a/src/aipass/drone/README.md +++ b/src/aipass/drone/README.md @@ -1,51 +1,80 @@ -# DRONE +# Drone -**Purpose:** Command routing, @ resolution, module discovery and dispatch -**Module:** `aipass.drone` -**Created:** 2026-03-05 +Command router and symbolic addressing for AIPass. Resolves `@branch` names to paths at runtime via `AIPASS_REGISTRY.json`, routes commands to module entry points, and discovers available commands across the system. ---- +## CLI -## Overview +```bash +drone systems # List all registered modules and branches +drone @seedgo verify # Route "verify" to the seedgo module +drone @seedgo audit aipass # Route "audit aipass" to seedgo +drone @module --help # Show help for any module +drone --version # Show version +``` -### What I Do +## Python API +```python +from aipass.drone import resolve_branch, list_branches, route_command -### How I Work -- **Entry Point:** `apps/drone.py` -- **Pattern:** Auto-discovers and routes to modules +# Resolve @name to absolute path +path = resolve_branch("@seedgo") ---- +# List all registered branches +branches = list_branches() # All branches +active = list_branches(status="active") # Filter by status + +# Route a command to a branch +result = route_command("@seedgo", "verify") +print(result.stdout) # Command output +print(result.exit_code) # 0 on success +``` + +### Registry Management + +```python +from aipass.drone import set_registry_path, get_registry_path + +# Use a custom registry location +set_registry_path("/path/to/AIPASS_REGISTRY.json") + +# Or set via environment variable +# export AIPASS_REGISTRY_PATH=/path/to/registry.json +``` + +### Error Handling + +```python +from aipass.drone import resolve_branch, BranchNotFoundError, CommandExecutionError + +try: + path = resolve_branch("@nonexistent") +except BranchNotFoundError: + print("Branch not found in registry") + +try: + result = route_command("@seedgo", "audit", args=["aipass"], timeout=120) +except CommandExecutionError as e: + print(f"Command failed: {e}") +``` ## Architecture ``` -DRONE/ +drone/ +├── cli.py # pip entry point (drone command) +├── drone_adapter.py # Self-routing adapter for drone @drone +├── __init__.py # Public API exports ├── apps/ -│ ├── drone.py # Entry point -│ ├── modules/ # Business logic -│ ├── handlers/ # Implementation -│ └── plugins/ # Extensions -├── docs/ -├── tests/ -├── passport.json # Identity -├── local.json # Session history -├── observations.json # Collaboration patterns -└── README.md +│ ├── drone.py # Core entry point +│ ├── modules/ # Business logic +│ │ ├── config.py # Registry path resolution +│ │ ├── resolver.py # Branch resolution (@name -> path) +│ │ ├── router.py # Command routing via subprocess +│ │ ├── discovery.py # Module and command discovery +│ │ └── module_registry.py # Internal module routing +│ └── handlers/ # Implementation +│ ├── executor.py # Safe subprocess execution +│ └── exceptions.py # Exception hierarchy +└── tests/ ``` - ---- - -## Commands - -*Configure after initialization* - ---- - -## Integration Points - -### Depends On - - -### Provides To - diff --git a/src/aipass/drone/apps/drone.py b/src/aipass/drone/apps/drone.py index f5a16559..d646f1af 100644 --- a/src/aipass/drone/apps/drone.py +++ b/src/aipass/drone/apps/drone.py @@ -84,7 +84,7 @@ def show_introspection() -> None: if branches: print(f"Registered Branches ({len(branches)}):") for name in sorted(branches): - print(f" @{name}") + print(f" {name}") if not modules and not branches: print("No branches or modules registered.") diff --git a/src/aipass/drone/apps/handlers/executor.py b/src/aipass/drone/apps/handlers/executor.py index be21b20d..66f26024 100644 --- a/src/aipass/drone/apps/handlers/executor.py +++ b/src/aipass/drone/apps/handlers/executor.py @@ -28,13 +28,22 @@ def execute_command( args: List[str], cwd: str, timeout: int = 30, + env: dict | None = None, ) -> CommandResult: """Execute a command via subprocess with safety guards. Never uses shell=True to prevent shell injection attacks. """ + import os + full_cmd = [executable] + list(args) + # Merge custom env vars with current environment + run_env = None + if env: + run_env = os.environ.copy() + run_env.update(env) + try: result = subprocess.run( full_cmd, @@ -42,6 +51,7 @@ def execute_command( capture_output=True, timeout=timeout, shell=False, + env=run_env, ) except subprocess.TimeoutExpired as e: raise CommandExecutionError( diff --git a/src/aipass/drone/apps/modules/discovery.py b/src/aipass/drone/apps/modules/discovery.py index 5cf7ba3d..90aa3552 100644 --- a/src/aipass/drone/apps/modules/discovery.py +++ b/src/aipass/drone/apps/modules/discovery.py @@ -7,6 +7,7 @@ and scanning module directories as a fallback. import logging import subprocess +import sys from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional @@ -82,7 +83,7 @@ def discover_modules(target: str) -> List[str]: if entry_point is not None: try: result = subprocess.run( - ["python3", str(entry_point.relative_to(branch_path)), "--help"], + [sys.executable, str(entry_point.relative_to(branch_path)), "--help"], cwd=branch_path, capture_output=True, timeout=10, @@ -121,7 +122,7 @@ def get_help(target: str, command: Optional[str] = None) -> HelpResult: try: result = subprocess.run( - ["python3"] + cmd_args, + [sys.executable] + cmd_args, cwd=branch_path, capture_output=True, timeout=10, diff --git a/src/aipass/drone/apps/modules/router.py b/src/aipass/drone/apps/modules/router.py index 8eba3dae..2c27fd26 100644 --- a/src/aipass/drone/apps/modules/router.py +++ b/src/aipass/drone/apps/modules/router.py @@ -6,6 +6,7 @@ locating the branch's apps/{name}.py entry point, and executing via subprocess. """ import logging +import sys from pathlib import Path from typing import Dict, List, Optional @@ -49,11 +50,15 @@ def route_command( relative_entry = str(entry_point.relative_to(branch_path)) cmd_args = [relative_entry, command] + list(args) + # Pass caller's CWD so target branches can detect who invoked them + caller_env = {"AIPASS_CALLER_CWD": str(Path.cwd())} + result = execute_command( - executable="python3", + executable=sys.executable, args=cmd_args, cwd=branch_path, timeout=timeout, + env=caller_env, ) return CommandResult( diff --git a/src/aipass/flow/.claude/settings.local.json b/src/aipass/flow/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/flow/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/flow/.seed/bypass.json b/src/aipass/flow/.seed/bypass.json index 17687f8d..d7fab058 100644 --- a/src/aipass/flow/.seed/bypass.json +++ b/src/aipass/flow/.seed/bypass.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "created": "2026-03-06T03:16:10.952307", + "created": "2026-03-06T14:27:39.378089", "description": "Standards bypass configuration for this branch" }, "bypass": [], diff --git a/src/aipass/flow/README.md b/src/aipass/flow/README.md index f544e9f0..8fe457f3 100644 --- a/src/aipass/flow/README.md +++ b/src/aipass/flow/README.md @@ -1,51 +1,67 @@ -# FLOW +# Flow -**Purpose:** Plan lifecycle management - DPLAN/FPLAN/MPLAN creation, tracking, closure -**Module:** `aipass.flow` -**Created:** 2026-03-05 +Plan lifecycle management for AIPass. Creates, tracks, closes, and archives numbered work plans (FPLANs) with registry-backed state, async post-processing, and cross-branch aggregation. ---- +## Usage -## Overview +### CLI (via drone) -### What I Do +```bash +drone @flow create . "Build authentication module" # Create plan in current dir +drone @flow create /path/to "Migration task" master # Master plan (multi-phase) +drone @flow close 42 # Close plan +drone @flow close --all # Close all open plans +drone @flow list # List plans +drone @flow aggregate # Aggregate plans across branches +drone @flow registry # Registry health monitor +``` +### Python -### How I Work -- **Entry Point:** `apps/flow.py` -- **Pattern:** Auto-discovers and routes to modules +```python +from aipass.flow.apps.modules.create_plan import create_plan +from aipass.flow.apps.modules.close_plan import close_plan +from aipass.flow.apps.modules.list_plans import list_plans +from aipass.flow.apps.handlers.registry.load_registry import load_registry ---- +# Load the plan registry +registry = load_registry() +``` ## Architecture ``` -FLOW/ +flow/ ├── apps/ -│ ├── flow.py # Entry point -│ ├── modules/ # Business logic -│ ├── handlers/ # Implementation -│ └── plugins/ # Extensions -├── docs/ -├── tests/ -├── passport.json # Identity -├── local.json # Session history -├── observations.json # Collaboration patterns -└── README.md +│ ├── flow.py # Entry point (auto-discovers modules) +│ ├── modules/ # Business logic +│ │ ├── create_plan.py # Plan creation with template support +│ │ ├── close_plan.py # Closure with async archival +│ │ ├── list_plans.py # Plan listing and filtering +│ │ ├── restore_plan.py # Plan recovery from backups +│ │ ├── registry_monitor.py # Orphan detection, auto-healing +│ │ ├── aggregate_central.py # Cross-branch plan aggregation +│ │ └── post_close_runner.py # Background post-processing +│ └── handlers/ # Implementation details +│ ├── plan/ # Lifecycle, file ops, validation +│ ├── registry/ # Load, save, auto-heal +│ ├── template/ # Plan templates (default, master, proposal) +│ ├── dashboard/ # Status aggregation +│ ├── mbank/ # Memory bank archival +│ └── summary/ # AI-generated plan summaries +├── templates/ # Plan template files +├── flow_json/ # Configuration and registry data +└── tests/ ``` ---- +## Plan Naming -## Commands +Plans follow the convention `FPLAN-XXXX_topic_slug_YYYY-MM-DD.md` where XXXX is an auto-incrementing number. -*Configure after initialization* +## Dependencies ---- - -## Integration Points - -### Depends On - - -### Provides To +- `aipass.cli` — terminal formatting +- `aipass.prax` — structured logging +- `aipass.trigger` — error reporting (optional) +Last Updated: 2026-03-06 diff --git a/src/aipass/flow/__init__.py b/src/aipass/flow/__init__.py new file mode 100644 index 00000000..d40f6f9c --- /dev/null +++ b/src/aipass/flow/__init__.py @@ -0,0 +1 @@ +"""Flow - Task planning and workflow management for AIPass.""" diff --git a/src/aipass/flow/apps/handlers/__init__.py b/src/aipass/flow/apps/handlers/__init__.py index 8ad4fb72..e42de25b 100644 --- a/src/aipass/flow/apps/handlers/__init__.py +++ b/src/aipass/flow/apps/handlers/__init__.py @@ -43,7 +43,7 @@ def _find_real_caller(): def _extract_branch_name(filepath: str) -> str: """Extract branch name from a file path.""" - parts = filepath.split("/") + parts = Path(filepath).parts for i, part in enumerate(parts): if part in ("aipass", "MEMORY_BANK", "Nexus"): if i + 1 < len(parts): diff --git a/src/aipass/prax/.claude/settings.local.json b/src/aipass/prax/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/prax/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/prax/.seed/bypass.json b/src/aipass/prax/.seed/bypass.json index be090e35..6de4af5a 100644 --- a/src/aipass/prax/.seed/bypass.json +++ b/src/aipass/prax/.seed/bypass.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "created": "2026-03-06T03:16:10.975118", + "created": "2026-03-06T14:27:42.646499", "description": "Standards bypass configuration for this branch" }, "bypass": [], diff --git a/src/aipass/prax/README.md b/src/aipass/prax/README.md index 5853c5d9..1327e02b 100644 --- a/src/aipass/prax/README.md +++ b/src/aipass/prax/README.md @@ -1,51 +1,80 @@ -# PRAX +# Prax -**Purpose:** System logging, event tracking, and real-time monitoring infrastructure -**Module:** `aipass.prax` -**Created:** 2026-03-05 +System-wide logging and real-time monitoring for AIPass. Prax auto-routes log output from any module to per-module log files and provides a live monitoring console (Mission Control) that shows file changes, log events, and command execution across all branches. ---- +## Usage -## Overview +### Logging -### What I Do +```python +from aipass.prax import logger +logger.info("Processing started") +logger.warning("Disk usage high") +logger.error("Connection failed") +``` -### How I Work -- **Entry Point:** `apps/prax.py` -- **Pattern:** Auto-discovers and routes to modules +Logs auto-route to `system_logs/_.log` based on the calling module. No configuration needed — prax detects the caller via stack introspection. ---- +For handlers or plugins that need to bypass the event pipeline: + +```python +from aipass.prax.apps.modules.logger import get_direct_logger + +log = get_direct_logger("my_handler") +log.info("Direct log entry") +``` + +### Mission Control + +Real-time monitoring console for watching system activity across all branches. + +```bash +drone @prax monitor +``` + +Interactive commands inside the monitor: + +``` +watch all # Watch all branches +watch prax # Watch specific branch +watch errors # Only show errors +status # Show current filters +quit # Exit +``` + +### CLI Commands + +| Command | Description | +|---------|-------------| +| `drone @prax monitor` | Launch Mission Control | +| `drone @prax init` | Initialize logging system | +| `drone @prax status` | Show system status | +| `drone @prax run` | Start continuous logging mode | +| `drone @prax shutdown` | Shutdown logging system | +| `drone @prax discover` | Discover Python modules in ecosystem | +| `drone @prax log-audit` | Audit log file sizes and health | ## Architecture ``` -PRAX/ +prax/ ├── apps/ -│ ├── prax.py # Entry point -│ ├── modules/ # Business logic -│ ├── handlers/ # Implementation -│ └── plugins/ # Extensions -├── docs/ -├── tests/ -├── passport.json # Identity -├── local.json # Session history -├── observations.json # Collaboration patterns -└── README.md +│ ├── prax.py # Entry point +│ ├── modules/ +│ │ ├── logger.py # SystemLogger (public API) +│ │ └── monitor_module.py # Mission Control +│ └── handlers/ +│ ├── logging/ # Log setup, rotation, introspection +│ ├── monitoring/ # Event queue, branch detection, stream output +│ ├── discovery/ # Module scanning and filtering +│ ├── config/ # Configuration loading +│ └── registry/ # Module registry management +└── tests/ ``` ---- - -## Commands - -*Configure after initialization* - ---- - -## Integration Points - -### Depends On - - -### Provides To +## How It Works +1. **Auto-routing** — When any module calls `logger.info()`, prax inspects the call stack to identify the caller and routes the log entry to the appropriate file. +2. **Dual output** — Each log entry goes to both a system-wide log (`system_logs/`) and a branch-local log (`{branch}/logs/`), both with rotation. +3. **Mission Control** — A multi-threaded monitoring console that watches file changes (via inotify), log events, and agent activity across all branches simultaneously. diff --git a/src/aipass/prax/apps/handlers/__init__.py b/src/aipass/prax/apps/handlers/__init__.py index 06135a4e..deb15203 100755 --- a/src/aipass/prax/apps/handlers/__init__.py +++ b/src/aipass/prax/apps/handlers/__init__.py @@ -43,7 +43,7 @@ def _find_real_caller(): def _extract_branch_name(filepath: str) -> str: """Extract branch name from a file path.""" - parts = filepath.split("/") + parts = Path(filepath).parts for i, part in enumerate(parts): if part == "aipass": if i + 1 < len(parts): diff --git a/src/aipass/prax/apps/handlers/config/load.py b/src/aipass/prax/apps/handlers/config/load.py index 2c04bb35..a0b1681d 100755 --- a/src/aipass/prax/apps/handlers/config/load.py +++ b/src/aipass/prax/apps/handlers/config/load.py @@ -62,7 +62,11 @@ def _find_repo_root() -> Path: _system_logs_dir_cache: Path | None = None def get_system_logs_dir() -> Path: - """Lazily resolve and create system_logs directory (package-relative).""" + """Lazily resolve and create system_logs directory (package-relative). + + DEPRECATED: Use get_module_logs_dir(module_name) for per-module logging. + Kept for monitoring code that scans a central directory. + """ global _system_logs_dir_cache if _system_logs_dir_cache is None: repo_root = _find_repo_root() @@ -70,6 +74,23 @@ def get_system_logs_dir() -> Path: _system_logs_dir_cache.mkdir(parents=True, exist_ok=True) return _system_logs_dir_cache + +def get_module_logs_dir(module_name: str) -> Path: + """Get the logs directory for a specific module. + + Returns ECOSYSTEM_ROOT / module_name / "logs", creating it if needed. + Each module logs to its own directory instead of a centralized system_logs/. + + Args: + module_name: Module name (e.g., "flow", "prax", "trigger") + + Returns: + Path to the module's logs directory + """ + logs_dir = ECOSYSTEM_ROOT / module_name / "logs" + logs_dir.mkdir(parents=True, exist_ok=True) + return logs_dir + # Config file PRAX_LOGGER_CONFIG_FILE = PRAX_JSON_DIR / "prax_logger_config.json" diff --git a/src/aipass/prax/apps/handlers/dashboard/agent_status_writer.py b/src/aipass/prax/apps/handlers/dashboard/agent_status_writer.py index 4c847c4e..1e547f9d 100644 --- a/src/aipass/prax/apps/handlers/dashboard/agent_status_writer.py +++ b/src/aipass/prax/apps/handlers/dashboard/agent_status_writer.py @@ -97,6 +97,8 @@ def _is_pid_alive(pid: int) -> bool: True if process exists and looks like a claude agent """ try: + if sys.platform != "linux": + return False cmdline_path = Path(f"/proc/{pid}/cmdline") if not cmdline_path.exists(): return False diff --git a/src/aipass/prax/apps/handlers/discovery/scanner.py b/src/aipass/prax/apps/handlers/discovery/scanner.py index 2e9f1630..19e961f5 100755 --- a/src/aipass/prax/apps/handlers/discovery/scanner.py +++ b/src/aipass/prax/apps/handlers/discovery/scanner.py @@ -30,7 +30,7 @@ from typing import Dict, Any # Import from prax config from aipass.prax.apps.handlers.config.load import ( ECOSYSTEM_ROOT, - get_system_logs_dir + get_module_logs_dir ) # Import filtering @@ -62,7 +62,7 @@ def scan_directory_safely(directory: Path, modules: Dict, max_depth: int = 10): modules[module_name] = { "file_path": str(item), "relative_path": str(relative_path), - "log_file": str(get_system_logs_dir() / f"prax_{module_name}.log"), + "log_file": str(get_module_logs_dir("prax") / f"{module_name}.log"), "discovered_time": datetime.now(timezone.utc).isoformat(), "size": item.stat().st_size, "modified_time": datetime.fromtimestamp(item.stat().st_mtime).isoformat(), diff --git a/src/aipass/prax/apps/handlers/discovery/watcher.py b/src/aipass/prax/apps/handlers/discovery/watcher.py index 9515c4f9..2a74802b 100755 --- a/src/aipass/prax/apps/handlers/discovery/watcher.py +++ b/src/aipass/prax/apps/handlers/discovery/watcher.py @@ -32,7 +32,7 @@ from watchdog.events import FileSystemEventHandler # Import from prax config from aipass.prax.apps.handlers.config.load import ( ECOSYSTEM_ROOT, - get_system_logs_dir + get_module_logs_dir ) # Import from prax registry handlers @@ -75,7 +75,7 @@ class PythonFileWatcher(FileSystemEventHandler): modules[module_name] = { "file_path": str(py_file), "relative_path": str(relative_path), - "log_file": str(get_system_logs_dir() / f"prax_{module_name}.log"), + "log_file": str(get_module_logs_dir("prax") / f"{module_name}.log"), "discovered_time": datetime.now(timezone.utc).isoformat(), "size": py_file.stat().st_size, "modified_time": datetime.fromtimestamp(py_file.stat().st_mtime).isoformat(), diff --git a/src/aipass/prax/apps/handlers/logging/direct.py b/src/aipass/prax/apps/handlers/logging/direct.py index f612bc73..bf132eae 100644 --- a/src/aipass/prax/apps/handlers/logging/direct.py +++ b/src/aipass/prax/apps/handlers/logging/direct.py @@ -42,15 +42,14 @@ Use this ONLY for infrastructure handlers that would cause recursion: Everyone else should use the regular system_logger. """ -from pathlib import Path - import inspect import logging from logging.handlers import RotatingFileHandler +from pathlib import Path from typing import Dict, Optional, Tuple from aipass.prax.apps.handlers.config.load import ( - get_system_logs_dir, + get_module_logs_dir, DEFAULT_LOG_LEVEL, load_log_config, lines_to_bytes @@ -99,9 +98,9 @@ def _create_direct_logger( branch_name: str, branch_path: Optional[str] ) -> logging.Logger: - """Create a standalone logger with RotatingFileHandlers. + """Create a standalone logger with a RotatingFileHandler. - Same dual-logging setup as setup_individual_logger but with NO + Same logging setup as setup_individual_logger but with NO connection to the event pipeline. Uses logging.Logger internally only for RotatingFileHandler management - no root logger propagation. @@ -111,11 +110,10 @@ def _create_direct_logger( branch_path: Module/branch name (e.g., 'prax') or None Returns: - Configured logger with dual file handlers and no propagation. + Configured logger with file handler and no propagation. """ - # Namespaced to avoid collision with regular captured_ loggers - LOGGER_KEY = f"direct_{branch_name}_{module_name}" - logger = logging.getLogger(LOGGER_KEY) + logger_key = f"direct_{branch_name}_{module_name}" + logger = logging.getLogger(logger_key) logger.setLevel(DEFAULT_LOG_LEVEL) logger.handlers.clear() logger.propagate = False # Critical: no root logger propagation @@ -126,33 +124,19 @@ def _create_direct_logger( config['date_format'] ) - # Handler 1: System-wide log - SYS_LOG_FILE = get_system_logs_dir() / f"{branch_name}_{module_name}.log" - SYS_LIMITS = config['system_logs'] - sys_handler = RotatingFileHandler( - SYS_LOG_FILE, - maxBytes=lines_to_bytes(SYS_LIMITS['max_lines']), - backupCount=SYS_LIMITS['backup_count'], + # Module-local log: each module logs to /logs/ + target = branch_name if branch_path else "prax" + logs_dir = get_module_logs_dir(target) + log_file = logs_dir / f"{module_name}.log" + limits = config['system_logs'] + handler = RotatingFileHandler( + log_file, + maxBytes=lines_to_bytes(limits['max_lines']), + backupCount=limits['backup_count'], encoding='utf-8' ) - sys_handler.setFormatter(formatter) - logger.addHandler(sys_handler) - - # Handler 2: Branch-local log - if branch_path: - from aipass.prax.apps.handlers.config.load import _find_repo_root - LOCAL_LOGS_DIR = _find_repo_root() / branch_path / "logs" - LOCAL_LOGS_DIR.mkdir(parents=True, exist_ok=True) - LOCAL_LOG_FILE = LOCAL_LOGS_DIR / f"{module_name}.log" - LOCAL_LIMITS = config['local_logs'] - local_handler = RotatingFileHandler( - LOCAL_LOG_FILE, - maxBytes=lines_to_bytes(LOCAL_LIMITS['max_lines']), - backupCount=LOCAL_LIMITS['backup_count'], - encoding='utf-8' - ) - local_handler.setFormatter(formatter) - logger.addHandler(local_handler) + handler.setFormatter(formatter) + logger.addHandler(handler) return logger @@ -170,7 +154,7 @@ def _get_or_create_logger( Returns: Cached or newly created direct logger. """ - branch_name = branch_path.split('/')[-1] if branch_path else "unknown" + branch_name = Path(branch_path).name if branch_path else "unknown" key = f"{branch_name}_{module_name}" if key not in _direct_loggers: _direct_loggers[key] = _create_direct_logger( diff --git a/src/aipass/prax/apps/handlers/logging/introspection.py b/src/aipass/prax/apps/handlers/logging/introspection.py index bc14359a..bbd42402 100755 --- a/src/aipass/prax/apps/handlers/logging/introspection.py +++ b/src/aipass/prax/apps/handlers/logging/introspection.py @@ -79,11 +79,13 @@ def get_calling_module_path() -> Optional[str]: finally: del frame +_AIPASS_PKG_ROOT = Path(__file__).resolve().parents[4] # logging/ → handlers/ → apps/ → prax/ → aipass/ + def detect_branch_from_path(module_path: str) -> Optional[str]: """Detect branch name from module file path The package structure is: .../src/aipass/{module}/apps/... - Find "aipass" in path parts, then the next part is the module name. + Resolves relative to the aipass package root found via __file__. Examples: .../src/aipass/flow/apps/module.py → "flow" @@ -96,18 +98,12 @@ def detect_branch_from_path(module_path: str) -> Optional[str]: if not module_path: return None - path = Path(module_path) - parts = path.parts - - # Find "aipass" in path parts and return the next part (the module name) - # Package structure: .../src/aipass/{module}/apps/... try: - aipass_idx = parts.index('aipass') - if aipass_idx + 1 < len(parts): - module_name = parts[aipass_idx + 1] - # Ensure there's more after it (not just a file in the aipass dir) - if aipass_idx + 2 < len(parts): - return module_name + path = Path(module_path).resolve() + relative = path.relative_to(_AIPASS_PKG_ROOT) + # relative is like: flow/apps/module.py → parts[0] = "flow" + if len(relative.parts) >= 2: + return relative.parts[0] except ValueError: pass diff --git a/src/aipass/prax/apps/handlers/logging/setup.py b/src/aipass/prax/apps/handlers/logging/setup.py index 25dd3b1f..bc50c170 100755 --- a/src/aipass/prax/apps/handlers/logging/setup.py +++ b/src/aipass/prax/apps/handlers/logging/setup.py @@ -17,15 +17,14 @@ Creates and configures individual loggers for modules. Handles dual logging (system-wide + branch-local) and terminal output. """ -from pathlib import Path - import logging +from pathlib import Path from typing import Dict, Optional from logging.handlers import RotatingFileHandler # Import from prax config from aipass.prax.apps.handlers.config.load import ( - get_system_logs_dir, + get_module_logs_dir, DEFAULT_LOG_LEVEL, load_log_config, lines_to_bytes @@ -58,8 +57,7 @@ def setup_individual_logger(module_name: str) -> logging.Logger: """Setup individual logger for a specific module with dual logging support Creates: - - System-wide log: {repo_root}/system_logs/prax_{module_name}.log - - Branch-local log: {repo_root}/src/aipass/{branch}/logs/{module_name}.log (if in a branch) + - Module-local log: src/aipass/{branch}/logs/{module_name}.log - Terminal handler (if enabled) Args: @@ -91,7 +89,7 @@ def setup_individual_logger(module_name: str) -> logging.Logger: # "prax" → "prax" # "flow" → "flow" if branch_path: - branch_name = branch_path.split('/')[-1] + branch_name = Path(branch_path).name else: branch_name = "prax" # Fallback to prax if no branch detected @@ -101,49 +99,22 @@ def setup_individual_logger(module_name: str) -> logging.Logger: log_config['date_format'] ) - # HANDLER 1: System-wide log (named after calling branch) - system_log_file = get_system_logs_dir() / f"{branch_name}_{module_name}.log" - system_limits = log_config['system_logs'] - system_max_bytes = lines_to_bytes(system_limits['max_lines']) - system_handler = RotatingFileHandler( - system_log_file, - maxBytes=system_max_bytes, - backupCount=system_limits['backup_count'], + # Module-local log: each module logs to /logs/ + logs_dir = get_module_logs_dir(branch_name) + log_file = logs_dir / f"{module_name}.log" + limits = log_config['system_logs'] + max_bytes = lines_to_bytes(limits['max_lines']) + handler = RotatingFileHandler( + log_file, + maxBytes=max_bytes, + backupCount=limits['backup_count'], encoding='utf-8' ) - system_handler.setFormatter(formatter) - logger.addHandler(system_handler) + handler.setFormatter(formatter) + logger.addHandler(handler) - # HANDLER 2: Branch-local log (if module is in a branch) - branch = branch_path # Use already detected branch_path - - if branch: - # Create branch logs directory if it doesn't exist - # Use repo-relative path for branch logs - from aipass.prax.apps.handlers.config.load import _find_repo_root - branch_logs_dir = _find_repo_root() / branch / "logs" - branch_logs_dir.mkdir(parents=True, exist_ok=True) - - # Create branch-local log file - branch_log_file = branch_logs_dir / f"{module_name}.log" - local_limits = log_config['local_logs'] - local_max_bytes = lines_to_bytes(local_limits['max_lines']) - branch_handler = RotatingFileHandler( - branch_log_file, - maxBytes=local_max_bytes, - backupCount=local_limits['backup_count'], - encoding='utf-8' - ) - branch_handler.setFormatter(formatter) - logger.addHandler(branch_handler) - - # Log to system logger - if _system_logger: - _system_logger.info(f"Logger created for {module_name} → system: {system_log_file} ({system_limits['max_lines']} lines), branch: {branch_log_file} ({local_limits['max_lines']} lines)") - else: - # Log to system logger - if _system_logger: - _system_logger.info(f"Logger created for {module_name} → {system_log_file} ({system_limits['max_lines']} lines)") + if _system_logger: + _system_logger.info(f"Logger created for {module_name} → {log_file} ({limits['max_lines']} lines)") # HANDLER 3: Terminal output (if enabled) if _terminal_output_enabled and _terminal_module_available: @@ -177,8 +148,8 @@ def setup_system_logger() -> logging.Logger: _system_logger.setLevel(DEFAULT_LOG_LEVEL) _system_logger.handlers.clear() - # Create prax_logger's own log file - log_file = get_system_logs_dir() / "prax_logger.log" + # Create prax_logger's own log file (in prax/logs/) + log_file = get_module_logs_dir("prax") / "prax_logger.log" # Create rotating file handler with config-driven limits system_limits = log_config['system_logs'] diff --git a/src/aipass/prax/apps/handlers/monitoring/MONITOR_MODULE_INTEGRATION.py b/src/aipass/prax/apps/handlers/monitoring/MONITOR_MODULE_INTEGRATION.py index 6ed78a65..91683007 100644 --- a/src/aipass/prax/apps/handlers/monitoring/MONITOR_MODULE_INTEGRATION.py +++ b/src/aipass/prax/apps/handlers/monitoring/MONITOR_MODULE_INTEGRATION.py @@ -170,7 +170,7 @@ def example_with_interactive_commands(): console.print(f" Log watcher: {'active' if is_log_watcher_active() else 'inactive'}") console.print(f" Queue size: {_event_queue.size() if _event_queue else 0}\n") - except: + except Exception: pass # Running flag for threads diff --git a/src/aipass/prax/apps/handlers/monitoring/event_queue.py b/src/aipass/prax/apps/handlers/monitoring/event_queue.py index cdd2f78c..2648dac0 100644 --- a/src/aipass/prax/apps/handlers/monitoring/event_queue.py +++ b/src/aipass/prax/apps/handlers/monitoring/event_queue.py @@ -72,7 +72,7 @@ class MonitoringQueue: if len(self.recent_events) > 100: self.recent_events.pop(0) return True - except: + except Exception: return False return False diff --git a/src/aipass/prax/apps/modules/logger.py b/src/aipass/prax/apps/modules/logger.py index 18afa657..0d9e5432 100755 --- a/src/aipass/prax/apps/modules/logger.py +++ b/src/aipass/prax/apps/modules/logger.py @@ -65,7 +65,7 @@ from aipass.prax.apps.handlers.discovery.watcher import ( ) from aipass.prax.apps.handlers.registry.save import save_module_registry from aipass.prax.apps.handlers.registry.load import load_module_registry -from aipass.prax.apps.handlers.config.load import get_system_logs_dir, PRAX_JSON_DIR +from aipass.prax.apps.handlers.config.load import get_module_logs_dir, PRAX_JSON_DIR from aipass.prax.apps.handlers.logging.direct import ( get_direct_logger, direct_log, @@ -162,7 +162,7 @@ def initialize_logging_system(): # Log system startup system_logger_instance.info("Prax logging system initialized") - system_logger_instance.info(f"System logs directory: {get_system_logs_dir()}") + system_logger_instance.info(f"Module logs directory: {get_module_logs_dir('prax')}") system_logger_instance.info(f"Found {len(modules)} modules for logging setup") # Install logger override @@ -242,7 +242,7 @@ def get_system_status() -> Dict[str, Any]: Dict with system status information: - total_modules: Number of discovered modules - individual_loggers: Number of active loggers - - system_logs_dir: Path to system logs + - module_logs_dir: Path to prax module logs - registry_file: Path to module registry - file_watcher_active: Watcher status - logger_override_active: Override status @@ -252,7 +252,7 @@ def get_system_status() -> Dict[str, Any]: return { "total_modules": len(modules), "individual_loggers": get_captured_loggers_count(), - "system_logs_dir": str(get_system_logs_dir()), + "module_logs_dir": str(get_module_logs_dir("prax")), "registry_file": str(DATA_FILE), "file_watcher_active": is_file_watcher_active(), "logger_override_active": is_override_active() diff --git a/src/aipass/prax/apps/modules/monitor_module.py b/src/aipass/prax/apps/modules/monitor_module.py index 2ad5a853..bb2d4a74 100755 --- a/src/aipass/prax/apps/modules/monitor_module.py +++ b/src/aipass/prax/apps/modules/monitor_module.py @@ -172,7 +172,7 @@ def _refresh_pid_cache() -> None: try: lock_data = _json.loads(lock_path.read_text(encoding="utf-8")) pid = lock_data.get("pid", 0) - if pid and Path(f"/proc/{pid}").exists(): + if pid and (sys.platform == "linux" and Path(f"/proc/{pid}").exists()): name = branch.get("name", "").upper() if name: new_cache[name] = pid diff --git a/src/aipass/prax/apps/modules/status_module.py b/src/aipass/prax/apps/modules/status_module.py index 65ce95b3..ed84014b 100755 --- a/src/aipass/prax/apps/modules/status_module.py +++ b/src/aipass/prax/apps/modules/status_module.py @@ -65,7 +65,7 @@ def handle_command(command: str, args: List[str]) -> bool: console.print("=" * 60) console.print(f"Total Modules: {status['total_modules']}") console.print(f"Active Loggers: {status['individual_loggers']}") - console.print(f"System Logs Dir: {status['system_logs_dir']}") + console.print(f"Module Logs Dir: {status['module_logs_dir']}") console.print(f"Registry File: {status['registry_file']}") console.print(f"File Watcher: {'🟢 Active' if status['file_watcher_active'] else '🔴 Inactive'}") console.print(f"Logger Override: {'🟢 Active' if status['logger_override_active'] else '🔴 Inactive'}") diff --git a/src/aipass/seedgo/.claude/settings.local.json b/src/aipass/seedgo/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/seedgo/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/seedgo/.seed/bypass.json b/src/aipass/seedgo/.seed/bypass.json index f1c0a0a6..966e843f 100644 --- a/src/aipass/seedgo/.seed/bypass.json +++ b/src/aipass/seedgo/.seed/bypass.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "created": "2026-03-05T17:31:55.022018", + "created": "2026-03-06T14:27:46.561948", "description": "Standards bypass configuration for this branch" }, "bypass": [], diff --git a/src/aipass/seedgo/README.md b/src/aipass/seedgo/README.md index 5b10e4c3..a00830e6 100644 --- a/src/aipass/seedgo/README.md +++ b/src/aipass/seedgo/README.md @@ -1,71 +1,54 @@ -# SEEDGO +# Seedgo -**Purpose:** Standards compliance platform with pluggable standard packs -**Module:** `aipass.seedgo` -**Created:** 2026-03-05 -**Last Updated:** 2026-03-05 +Standards compliance platform for AIPass modules. Audits Python code against configurable standard packs, scores each file, and reports violations. Ships with the `aipass` pack (21 standards covering imports, architecture, naming, logging, documentation, and more). ---- +## Usage -## Overview +### CLI -### What I Do -Seedgo audits code against configurable standard packs. Ships with the `aipass` pack (20 standards). Routes commands to packs — each pack is self-contained with its own handlers, modules, and manifests. +```bash +seedgo --help # Show packs and usage +seedgo audit aipass # Audit all modules against aipass standards +seedgo checklist aipass # Check a single file +seedgo list # Show installed standard packs +seedgo verify # Self-check seedgo internals +``` -### How I Work -- **Entry Point:** `apps/seedgo.py` -- **Pattern:** Pack router — discovers packs, dispatches audit/verify/checklist commands +### Python ---- +```python +from aipass.seedgo.apps.seedgo import main, discover_packs + +# List available standard packs +packs = discover_packs() +print(packs) # {'aipass': PosixPath('...')} +``` ## Architecture ``` seedgo/ ├── apps/ -│ ├── seedgo.py # Entry point + pack router +│ ├── seedgo.py # Entry point + pack router │ ├── modules/ -│ │ └── seedgo_verify.py # Self-check module -│ ├── handlers/ -│ ├── plugins/ +│ │ └── seedgo_verify.py # Self-check module │ └── standards/ -│ └── aipass/ # Built-in standard pack -│ ├── pack.json # Pack manifest (20 standards) -│ ├── pack_entry.py # Pack entry point -│ ├── handlers/ # Standard check implementations -│ └── modules/ # Standard orchestrators +│ └── aipass/ # Built-in standard pack (21 standards) +│ ├── pack.json # Pack manifest +│ ├── pack_entry.py # Pack entry point +│ ├── handlers/ # Standard check implementations +│ └── modules/ # Standard display + orchestrators ├── tests/ └── README.md ``` ---- - -## Commands - -``` -seedgo --help # Show packs and usage -seedgo list # Show installed standard packs -seedgo verify # Self-check (5 checks) -seedgo audit # Run full standards audit -seedgo checklist # Check single file -``` - ---- - ## Standard Packs -### aipass (built-in) -20 standards for AIPass framework modules: imports, architecture, naming, CLI, handlers, modules, documentation, error handling, testing, encapsulation, triggers, logging, permissions, META, README, JSON structure, type checking, CLI flags. +The `aipass` pack checks: architecture, CLI, CLI flags, diagnostics, documentation, encapsulation, error handling, handlers, imports, JSON structure, log handler, log level, log visibility, meta headers, modules, naming, permission flags, readme, shebang, testing, and trigger patterns. -Custom packs can be added to `apps/standards/` — each needs a `pack.json` manifest and `pack_entry.py`. +Custom packs go in `apps/standards/` — each needs a `pack.json` manifest and `pack_entry.py` entry point. See `apps/standards/app_development.example/` for a template. ---- +## Dependencies -## Integration Points - -### Depends On -- `aipass.cli` — display formatting (Rich) -- `aipass.prax` — logging - -### Provides To -- All modules — standards auditing and compliance checking +- `aipass.cli` — Rich-based display formatting +- `aipass.prax` — structured logging diff --git a/notepad.md b/src/aipass/seedgo/__init__.py similarity index 100% rename from notepad.md rename to src/aipass/seedgo/__init__.py diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/audit/branch_audit.py b/src/aipass/seedgo/apps/standards/aipass/handlers/audit/branch_audit.py index 762a17a8..18e03fa9 100644 --- a/src/aipass/seedgo/apps/standards/aipass/handlers/audit/branch_audit.py +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/audit/branch_audit.py @@ -45,6 +45,7 @@ from aipass.seedgo.apps.standards.aipass.handlers.standards import readme_check from aipass.seedgo.apps.standards.aipass.handlers.standards import diagnostics_check from aipass.seedgo.apps.standards.aipass.handlers.standards import meta_check from aipass.seedgo.apps.standards.aipass.handlers.standards import shebang_check +from aipass.seedgo.apps.standards.aipass.handlers.standards import log_structure_check from aipass.seedgo.apps.standards.aipass.handlers.config import ignore_handler @@ -443,6 +444,33 @@ def audit_branch(branch: Dict[str, str], bypass_rules: list) -> Dict: scores['shebang'] = int(sum(shebang_scores) / len(shebang_scores)) avg_score = int(sum(scores.values()) / len(scores)) if scores else 0 + # Check LOG_STRUCTURE on ALL files + log_structure_violations = [] + log_structure_scores = [] + if all_file_results: + for file_info in all_file_results: + try: + ls_result = log_structure_check.check_module(file_info['file'], bypass_rules=bypass_rules) + ls_score = ls_result.get('score', 0) + if ls_result.get('checks', []): + log_structure_scores.append(ls_score) + if not ls_result.get('passed', True): + failed_checks = [c for c in ls_result.get('checks', []) if not c.get('passed', False)] + if failed_checks: + log_structure_violations.append({ + 'file': file_info['name'], + 'path': file_info['file'], + 'score': ls_score, + 'issues': [c.get('message', 'Unknown') for c in failed_checks] + }) + except Exception: + pass + + # Update log_structure score + if log_structure_scores: + scores['log_structure'] = int(sum(log_structure_scores) / len(log_structure_scores)) + avg_score = int(sum(scores.values()) / len(scores)) if scores else 0 + # Run TYPE ERROR diagnostics on the branch (pyright) diagnostics_result = diagnostics_check.check_branch(str(branch_path)) type_errors = diagnostics_result.get('total_errors', 0) diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/standards/architecture_check.py b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/architecture_check.py index 55dff6ac..47d1361d 100644 --- a/src/aipass/seedgo/apps/standards/aipass/handlers/standards/architecture_check.py +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/architecture_check.py @@ -359,7 +359,7 @@ def _load_ignore_patterns(template_path: Path) -> Dict: "ignore_files": data.get("ignore_files", []), "ignore_patterns": data.get("ignore_patterns", []) } - except: + except Exception: return {"ignore_files": [], "ignore_patterns": []} diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/standards/error_handling_content.py b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/error_handling_content.py index 703b5e84..6e2f40a1 100644 --- a/src/aipass/seedgo/apps/standards/aipass/handlers/standards/error_handling_content.py +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/error_handling_content.py @@ -25,7 +25,7 @@ def get_error_handling_standards() -> str: "", "[yellow]CORE PRINCIPLE:[/yellow] Logging responsibility follows architectural hierarchy", "", - "[bold cyan]TIER 1: ENTRY POINTS[/bold cyan] (flow.py, seed.py, etc.)", + "[bold cyan]TIER 1: ENTRY POINTS[/bold cyan] (flow.py, seedgo.py, etc.)", " [dim]Prax Import:[/dim] YES (minimal)", " [dim]Logging Scope:[/dim] Operational only (discovery, routing, help)", " [red]✗ NO business error logging[/red]", diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/standards/json_structure_content.py b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/json_structure_content.py index 3c3987b6..5210792c 100644 --- a/src/aipass/seedgo/apps/standards/aipass/handlers/standards/json_structure_content.py +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/json_structure_content.py @@ -75,7 +75,7 @@ def get_json_structure_standards() -> str: " [dim]Expected: 100/100 on JSON STRUCTURE standard[/dim]", "", "[bold cyan]SPECIAL JSONS:[/bold cyan]", - " • [bold]Registries:[/bold] Collections (BRANCH_REGISTRY.json)", + " • [bold]Registries:[/bold] Collections (AIPASS_REGISTRY.json)", " • [bold]Custom:[/bold] Module-specific needs (no limit)", " • [dim]Not part of three-JSON pattern[/dim]", "", diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/standards/log_structure_check.py b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/log_structure_check.py new file mode 100644 index 00000000..2b02c731 --- /dev/null +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/log_structure_check.py @@ -0,0 +1,157 @@ +""" +Log Structure Standards Checker Handler + +Validates that AIPass modules follow the local logging pattern: +every module should have a logs/ directory for module-local logs. +No hardcoded absolute log paths. +""" + +# =================== META ==================== +# Name: log_structure_check.py +# Description: Log Structure Standards Checker Handler +# Version: 1.0.0 +# Created: 2026-03-06 +# Modified: 2026-03-06 +# ============================================= + + +import re +from pathlib import Path +from typing import Dict + + +def is_bypassed(file_path: str, standard: str, bypass_rules: list | None = None) -> bool: + """Check if a violation should be bypassed""" + if not bypass_rules: + return False + for rule in bypass_rules: + if rule.get('standard') and rule.get('standard') != standard: + continue + rule_file = rule.get('file', '') + if rule_file and rule_file not in file_path: + continue + rule_lines = rule.get('lines', []) + if not rule_lines: + return True + return False + + +def _find_module_root(file_path: Path) -> Path | None: + """Walk up from file to find the module root (directory containing apps/).""" + current = file_path.parent + for parent in [current] + list(current.parents): + if (parent / "apps").is_dir(): + return parent + # Stop at src/aipass level + if parent.name == "aipass" and parent.parent.name == "src": + break + return None + + +def check_module(module_path: str, bypass_rules: list | None = None) -> Dict: + """ + Check module logging structure. + + Checks: + 1. Module has a logs/ directory + 2. No hardcoded absolute log paths in source + 3. No /home/ references in logging configuration + + Args: + module_path: Path to Python module to check + bypass_rules: Optional list of bypass rules + + Returns: + Standard check result dict + """ + path = Path(module_path) + checks = [] + + if is_bypassed(module_path, 'log_structure', bypass_rules=bypass_rules): + return { + 'passed': True, + 'checks': [{'name': 'Bypassed', 'passed': True, 'message': 'Standard bypassed via .seed/bypass.json'}], + 'score': 100, + 'standard': 'LOG_STRUCTURE' + } + + if not path.exists(): + return { + 'passed': False, + 'checks': [{'name': 'File exists', 'passed': False, 'message': f'File not found: {module_path}'}], + 'score': 0, + 'standard': 'LOG_STRUCTURE' + } + + # Check 1: Module has logs/ directory + module_root = _find_module_root(path) + if module_root: + logs_dir = module_root / "logs" + has_logs_dir = logs_dir.is_dir() + checks.append({ + 'name': 'Module logs/ directory', + 'passed': has_logs_dir, + 'message': 'Module has local logs/ directory' if has_logs_dir + else f'Missing logs/ directory at {module_root.name}/' + }) + else: + # Can't determine module root — skip this check, pass it + checks.append({ + 'name': 'Module logs/ directory', + 'passed': True, + 'message': 'Module root not determinable (standalone file)' + }) + + # Check 2-3: Scan file for hardcoded log paths + try: + content = path.read_text(encoding='utf-8') + except Exception as e: + checks.append({ + 'name': 'File readable', + 'passed': False, + 'message': f'Error reading file: {e}' + }) + passed = all(c['passed'] for c in checks) + score = int(sum(1 for c in checks if c['passed']) / len(checks) * 100) + return {'passed': passed, 'checks': checks, 'score': score, 'standard': 'LOG_STRUCTURE'} + + lines = content.split('\n') + + # Check 2: No hardcoded absolute log paths + abs_log_issues = [] + for i, line in enumerate(lines, 1): + stripped = line.strip() + # Skip comments and docstrings + if stripped.startswith('#') or stripped.startswith('"""') or stripped.startswith("'''"): + continue + # Look for absolute paths in log-related contexts + if re.search(r'["\'][/\\](?:home|tmp|var|etc)[/\\].*\.log', stripped): + abs_log_issues.append(i) + + checks.append({ + 'name': 'No hardcoded log paths', + 'passed': len(abs_log_issues) == 0, + 'message': 'No hardcoded absolute log paths found' if not abs_log_issues + else f'Hardcoded log paths on lines: {abs_log_issues}' + }) + + # Check 3: No /home/ references in logging setup + home_log_issues = [] + for i, line in enumerate(lines, 1): + stripped = line.strip() + if stripped.startswith('#') or stripped.startswith('"""') or stripped.startswith("'''"): + continue + # Look for /home/ in log file handler or path config + if re.search(r'/home/\w+', stripped) and ('log' in stripped.lower() or 'LOG' in stripped): + home_log_issues.append(i) + + checks.append({ + 'name': 'No /home/ in log config', + 'passed': len(home_log_issues) == 0, + 'message': 'No /home/ references in logging configuration' if not home_log_issues + else f'/home/ references in log config on lines: {home_log_issues}' + }) + + passed = all(c['passed'] for c in checks) + score = int(sum(1 for c in checks if c['passed']) / len(checks) * 100) if checks else 0 + return {'passed': passed, 'checks': checks, 'score': score, 'standard': 'LOG_STRUCTURE'} diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/standards/log_structure_content.py b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/log_structure_content.py new file mode 100644 index 00000000..0dfcb26d --- /dev/null +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/standards/log_structure_content.py @@ -0,0 +1,51 @@ +""" +Log Structure Standards Content Handler + +Provides formatted display of log structure standards for terminal output. +""" + +# =================== META ==================== +# Name: log_structure_content.py +# Description: Log Structure Standards Content Handler +# Version: 1.0.0 +# Created: 2026-03-06 +# Modified: 2026-03-06 +# ============================================= + + +from typing import List + + +def get_log_structure_standards() -> List[str]: + """Return formatted log structure standards for display.""" + lines = [ + "[bold red]MODULE LOG STRUCTURE[/bold red]", + "", + "[yellow]CORE PRINCIPLE:[/yellow] Every module produces local logs in its own logs/ directory", + "", + "[bold cyan]REQUIREMENTS:[/bold cyan]", + " [green]1.[/green] Module has a [bold]logs/[/bold] directory at its root", + " [green]2.[/green] Log files written to module-local path, not just system_logs/", + " [green]3.[/green] No hardcoded absolute log paths (no /home/username/...)", + "", + "[bold cyan]DIRECTORY PATTERN:[/bold cyan]", + " [dim]my_module/[/dim]", + " [dim] apps/[/dim]", + " [dim] modules/[/dim]", + " [dim] handlers/[/dim]", + " [dim] logs/ <-- module-local logs[/dim]", + " [dim] tests/[/dim]", + "", + "[bold cyan]LOG PATH PATTERN:[/bold cyan]", + " [green]CORRECT:[/green]", + " [dim] LOG_DIR = Path(__file__).resolve().parents[N] / 'logs'[/dim]", + " [dim] log_path = LOG_DIR / 'module_name.log'[/dim]", + "", + " [red]WRONG:[/red]", + " [dim] log_path = '/home/aipass/system_logs/module.log'[/dim]", + " [dim] log_path = Path.home() / 'logs' / 'module.log'[/dim]", + "", + "[bold cyan]REFERENCE:[/bold cyan]", + " [dim]Examples: seedgo/logs/, trigger/logs/[/dim]", + ] + return lines diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/verify/checker_sync.py b/src/aipass/seedgo/apps/standards/aipass/handlers/verify/checker_sync.py index 309cff8b..db05da4d 100644 --- a/src/aipass/seedgo/apps/standards/aipass/handlers/verify/checker_sync.py +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/verify/checker_sync.py @@ -15,9 +15,8 @@ Catches drift between trigger_check.py patterns and trigger_content.py docs. import re -import sys from pathlib import Path -from typing import Dict, List +from typing import Dict # ============================================================================= # INFRASTRUCTURE SETUP @@ -190,8 +189,8 @@ def check_checker_sync() -> Dict: if count != actual_checkers: issues.append(f"SEED.id.json says {count} standards but {actual_checkers} checkers exist") - # Check seed.py --help for consistent standards count - seed_entry_path = PACK_ROOT / "apps/seed.py" + # Check seedgo.py --help for consistent standards count + seed_entry_path = PACK_ROOT / "apps/seedgo.py" if seed_entry_path.exists() and checkers_dir.exists(): seed_py_content = seed_entry_path.read_text() actual_checkers = len([f for f in checkers_dir.glob("*_check.py")]) @@ -200,7 +199,7 @@ def check_checker_sync() -> Dict: seed_py_counts = re.findall(r'(\d+)\s*(?:queryable\s*)?(?:code\s*)?standards?', seed_py_content, re.IGNORECASE) wrong_counts = [int(c) for c in set(seed_py_counts) if int(c) != actual_checkers and int(c) > 5] if wrong_counts: - issues.append(f"seed.py mentions {wrong_counts} standards but {actual_checkers} checkers exist") + issues.append(f"seedgo.py mentions {wrong_counts} standards but {actual_checkers} checkers exist") return { 'name': 'Checker-Doc Sync', diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/verify/command_check.py b/src/aipass/seedgo/apps/standards/aipass/handlers/verify/command_check.py index 5ecf06e5..119fef63 100644 --- a/src/aipass/seedgo/apps/standards/aipass/handlers/verify/command_check.py +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/verify/command_check.py @@ -27,7 +27,7 @@ def check_command_consistency() -> Dict: Scans audit module for flags, checks if they appear in: - README.md - - seed.py help text + - seedgo.py help text - docs/ files Returns: @@ -46,7 +46,7 @@ def check_command_consistency() -> Dict: # Files that should document commands doc_files = { 'readme': seed_path / "README.md", - 'seed_help': seed_path / "apps" / "seed.py", + 'seed_help': seed_path / "apps" / "seedgo.py", } for flag, description, source in command_flags: diff --git a/src/aipass/seedgo/apps/standards/aipass/handlers/verify/help_check.py b/src/aipass/seedgo/apps/standards/aipass/handlers/verify/help_check.py index f2c31212..13a1c9b8 100644 --- a/src/aipass/seedgo/apps/standards/aipass/handlers/verify/help_check.py +++ b/src/aipass/seedgo/apps/standards/aipass/handlers/verify/help_check.py @@ -1,7 +1,7 @@ """ Help Consistency Checker Handler -Checks that seed.py help text doesn't mention removed flags. +Checks that seedgo.py help text doesn't mention removed flags. Validates help documentation accuracy. """ @@ -14,7 +14,6 @@ Validates help documentation accuracy. # ============================================= -import sys from pathlib import Path from typing import Dict @@ -24,30 +23,31 @@ from aipass.seedgo.apps.standards.aipass.handlers.config import ignore_handler def check_help_consistency() -> Dict: """ - Check that seed.py help text doesn't mention removed flags + Check that seedgo.py help text doesn't mention removed flags Returns: Dict with check results """ seed_file = Path(__file__).resolve().parents[5] / "apps" / "seedgo.py" # seedgo root / apps / seedgo.py issues = [] + removed_flags = [] if not seed_file.exists(): return { 'name': 'Help Consistency', 'passed': False, - 'issues': ["seed.py not found"], + 'issues': ["seedgo.py not found"], 'score': 0 } - # Read seed.py and check for removed flags in help text + # Read seedgo.py and check for removed flags in help text try: content = file_handler.read_file(str(seed_file)) if content is None: return { 'name': 'Help Consistency', 'passed': False, - 'issues': ["Could not read seed.py"], + 'issues': ["Could not read seedgo.py"], 'score': 0 } @@ -75,6 +75,6 @@ def check_help_consistency() -> Dict: 'name': 'Help Consistency', 'passed': len(issues) == 0, 'issues': issues, - 'checked': [f"Scanned seed.py for: {', '.join(removed_flags)}"], + 'checked': [f"Scanned seedgo.py for: {', '.join(removed_flags)}"], 'score': 100 if len(issues) == 0 else 0 } diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/architecture_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/architecture_standard.py index d52fe89b..497b59fa 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/architecture_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/architecture_standard.py @@ -2,7 +2,7 @@ Architecture Standards Module Provides condensed architecture standards for AIPass branches. -Run directly or via: drone @seed architecture +Run directly or via: seedgo architecture """ # =================== META ==================== @@ -66,15 +66,15 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed architecture") + console.print(" seedgo architecture") console.print(" python3 architecture_standard.py") console.print(" python3 architecture_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed architecture") - console.print(" drone @seed arch") + console.print(" seedgo architecture") + console.print(" seedgo arch") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 architecture_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/cli_flags_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/cli_flags_standard.py index 13e87fbd..51b73137 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/cli_flags_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/cli_flags_standard.py @@ -2,7 +2,7 @@ CLI Flags Standards Module Provides condensed CLI flags standards for AIPass branches. -Run directly or via: drone @seed cli_flags +Run directly or via: seedgo cli_flags """ # =================== META ==================== @@ -60,14 +60,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed cli_flags") + console.print(" seedgo cli_flags") console.print(" python3 cli_flags_standard.py") console.print(" python3 cli_flags_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed cli_flags") + console.print(" seedgo cli_flags") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 cli_flags_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/cli_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/cli_standard.py index 50314aee..af48c260 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/cli_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/cli_standard.py @@ -2,7 +2,7 @@ CLI Standards Module Provides condensed CLI standards for AIPass branches. -Run directly or via: drone @seed cli +Run directly or via: seedgo cli """ # =================== META ==================== @@ -65,14 +65,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed cli") + console.print(" seedgo cli") console.print(" python3 cli_standard.py") console.print(" python3 cli_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed cli") + console.print(" seedgo cli") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 cli_standard.py") @@ -128,7 +128,7 @@ def run_demo(): console.print() try: - subprocess.run(["python3", demo_path], check=True) + subprocess.run([sys.executable, demo_path], check=True) except subprocess.CalledProcessError as e: logger.error(f"Error running demo: {e}") console.print(f"[red]Error running demo: {e}[/red]") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/diagnostics_audit.py b/src/aipass/seedgo/apps/standards/aipass/modules/diagnostics_audit.py index 92b94180..1bcffd4a 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/diagnostics_audit.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/diagnostics_audit.py @@ -5,8 +5,8 @@ Runs pyright on all AIPass branches to detect type errors, undefined variables, and other static analysis issues. Usage: - drone @seed diagnostics # All branches - drone @seed diagnostics flow # Specific branch + seedgo diagnostics # All branches + seedgo diagnostics flow # Specific branch """ # =================== META ==================== @@ -208,7 +208,7 @@ def print_introspection(): console.print() console.print("[yellow]Commands:[/yellow] diagnostics") console.print() - console.print("[dim]Run 'drone @seed diagnostics --help' for usage[/dim]") + console.print("[dim]Run 'seedgo diagnostics --help' for usage[/dim]") console.print() @@ -228,11 +228,11 @@ def print_help(): console.print("[yellow]USAGE:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed diagnostics") - console.print(" drone @seed diagnostics flow") + console.print(" seedgo diagnostics") + console.print(" seedgo diagnostics flow") console.print() - console.print(" python3 seed.py diagnostics") - console.print(" python3 seed.py diagnostics cortex") + console.print(" seedgo diagnostics") + console.print(" seedgo diagnostics cortex") console.print() console.print("[yellow]WHAT IT CHECKS:[/yellow]") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/documentation_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/documentation_standard.py index 3d94c24f..6129309f 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/documentation_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/documentation_standard.py @@ -2,7 +2,7 @@ Documentation Standards Module Provides condensed documentation standards for AIPass branches. -Run directly or via: drone @seed documentation +Run directly or via: seedgo documentation """ # =================== META ==================== @@ -42,15 +42,15 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed documentation") + console.print(" seedgo documentation") console.print(" python3 documentation_standard.py") console.print(" python3 documentation_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed documentation") - console.print(" drone @seed docs") + console.print(" seedgo documentation") + console.print(" seedgo docs") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 documentation_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/error_handling_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/error_handling_standard.py index 1dbc454c..238d668d 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/error_handling_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/error_handling_standard.py @@ -2,7 +2,7 @@ Error Handling Standards Module Provides condensed error handling standards for AIPass branches. -Run directly or via: drone @seed error_handling +Run directly or via: seedgo error_handling """ # =================== META ==================== @@ -67,16 +67,16 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed error_handling") - console.print(" drone @seed errors") - console.print(" drone @seed error") + console.print(" seedgo error_handling") + console.print(" seedgo errors") + console.print(" seedgo error") console.print(" python3 error_handling_standard.py") console.print(" python3 error_handling_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed error_handling") + console.print(" seedgo error_handling") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 error_handling_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/handlers_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/handlers_standard.py index 5eb21f48..ddf1902c 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/handlers_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/handlers_standard.py @@ -2,7 +2,7 @@ Handlers Standards Module Provides condensed handler standards for AIPass branches. -Run directly or via: drone @seed handlers +Run directly or via: seedgo handlers """ # =================== META ==================== @@ -65,14 +65,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed handlers") + console.print(" seedgo handlers") console.print(" python3 handlers_standard.py") console.print(" python3 handlers_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed handlers") + console.print(" seedgo handlers") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 handlers_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/imports_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/imports_standard.py index 5966b233..8c190060 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/imports_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/imports_standard.py @@ -2,7 +2,7 @@ Import Standards Module Provides condensed import standards for AIPass branches. -Run directly or via: drone @seed imports +Run directly or via: seedgo imports """ # =================== META ==================== @@ -65,14 +65,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed imports") + console.print(" seedgo imports") console.print(" python3 imports_standard.py") console.print(" python3 imports_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed imports") + console.print(" seedgo imports") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 imports_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/json_structure_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/json_structure_standard.py index 02c7b30c..3b08800d 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/json_structure_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/json_structure_standard.py @@ -2,7 +2,7 @@ JSON Structure Standards Module Provides condensed JSON structure standards for AIPass branches. -Run directly or via: drone @seed json +Run directly or via: seedgo json """ # =================== META ==================== @@ -67,14 +67,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed json") + console.print(" seedgo json") console.print(" python3 json_structure_standard.py") console.print(" python3 json_structure_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed json") + console.print(" seedgo json") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 json_structure_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/log_handler_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/log_handler_standard.py index 932042a9..72d6fc19 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/log_handler_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/log_handler_standard.py @@ -2,7 +2,7 @@ Log Handler Standards Module Provides condensed log handler rotation standards for AIPass branches. -Run directly or via: drone @seed log_handler +Run directly or via: seedgo log_handler """ # =================== META ==================== @@ -60,14 +60,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed log_handler") + console.print(" seedgo log_handler") console.print(" python3 log_handler_standard.py") console.print(" python3 log_handler_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed log_handler") + console.print(" seedgo log_handler") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 log_handler_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/log_level_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/log_level_standard.py index 63117dc2..c5dd20a4 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/log_level_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/log_level_standard.py @@ -2,7 +2,7 @@ Log Level Hygiene Standards Module Provides condensed log level hygiene standards for AIPass branches. -Run directly or via: drone @seed log_level +Run directly or via: seedgo log_level """ # =================== META ==================== @@ -60,14 +60,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed log_level") + console.print(" seedgo log_level") console.print(" python3 log_level_standard.py") console.print(" python3 log_level_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed log_level") + console.print(" seedgo log_level") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 log_level_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/log_structure_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/log_structure_standard.py new file mode 100644 index 00000000..5bb69f5b --- /dev/null +++ b/src/aipass/seedgo/apps/standards/aipass/modules/log_structure_standard.py @@ -0,0 +1,116 @@ +""" +Log Structure Standards Module + +Provides condensed module logging structure standards for AIPass modules. +Run directly or via: seedgo log_structure +""" + +# =================== META ==================== +# Name: log_structure_standard.py +# Description: Log Structure Standards Module +# Version: 1.0.0 +# Created: 2026-03-06 +# Modified: 2026-03-06 +# ============================================= + + +import sys +from pathlib import Path +from typing import List + +from aipass.prax import logger +from aipass.seedgo.apps.standards.aipass.handlers.json import json_handler +from aipass.seedgo.apps.standards.aipass.handlers.standards.log_structure_content import get_log_structure_standards + +from aipass.cli import console, header + + +def print_introspection(): + """Display module info and connected handlers""" + console.print() + console.print("[bold cyan]Log Structure Standards Module[/bold cyan]") + console.print() + + console.print("[yellow]Connected Handlers:[/yellow]") + console.print() + + handlers_dir = Path(__file__).parent.parent / "handlers" / "standards" + + if handlers_dir.exists(): + console.print(" [cyan]handlers/standards/[/cyan]") + console.print(" [dim]- log_structure_content.py[/dim]") + console.print(" [dim]- log_structure_check.py[/dim]") + console.print() + + console.print("[dim]Run 'python3 log_structure_standard.py --help' for usage[/dim]") + console.print() + + +def print_help(): + """Print drone-compliant help output""" + console.print() + console.print("[bold cyan]Log Structure Standards Module[/bold cyan]") + console.print("Module logging directory structure standards") + console.print() + + console.print("[yellow]COMMANDS:[/yellow]") + console.print(" Commands: log_structure, --help") + console.print() + console.print(" [cyan]log_structure[/cyan] - Display log structure standards") + console.print() + + console.print("[yellow]USAGE:[/yellow]") + console.print(" seedgo log_structure") + console.print(" python3 log_structure_standard.py") + console.print(" python3 log_structure_standard.py --help") + console.print() + + console.print("[yellow]EXAMPLES:[/yellow]") + console.print(" [dim]# Via seedgo[/dim]") + console.print(" seedgo log_structure") + console.print() + console.print(" [dim]# Standalone[/dim]") + console.print(" python3 log_structure_standard.py") + console.print() + + +def handle_command(command: str, args: List[str]) -> bool: + """Handle 'log_structure' command""" + if command != "log_structure": + return False + + json_handler.log_operation( + "standard_displayed", + {"command": command, "args": args} + ) + + print_standard() + return True + + +def print_standard(): + """Print log structure standards - orchestrates handler call""" + console.print() + header("Module Log Structure Standards") + console.print() + for line in get_log_structure_standards(): + console.print(f" {line}") + console.print() + + +if __name__ == "__main__": + if len(sys.argv) == 1: + print_introspection() + sys.exit(0) + + if sys.argv[1] in ['--help', '-h', 'help']: + print_help() + sys.exit(0) + + logger.info("Prax logger connected to log_structure_standard") + + json_handler.log_operation( + "standard_displayed", + {"command": "standalone"} + ) + print_standard() diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/log_visibility_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/log_visibility_standard.py index ae3d9a77..3821bb53 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/log_visibility_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/log_visibility_standard.py @@ -2,7 +2,7 @@ Log Visibility Standards Module Provides condensed log visibility standards for AIPass branches. -Run directly or via: drone @seed log_visibility +Run directly or via: seedgo log_visibility """ # =================== META ==================== @@ -60,14 +60,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed log_visibility") + console.print(" seedgo log_visibility") console.print(" python3 log_visibility_standard.py") console.print(" python3 log_visibility_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed log_visibility") + console.print(" seedgo log_visibility") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 log_visibility_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/modules_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/modules_standard.py index 6407ade2..cfddb33a 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/modules_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/modules_standard.py @@ -2,7 +2,7 @@ Modules Standards Module Provides condensed module standards for AIPass branches. -Run directly or via: drone @seed modules +Run directly or via: seedgo modules """ # =================== META ==================== @@ -64,14 +64,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed modules") + console.print(" seedgo modules") console.print(" python3 modules_standard.py") console.print(" python3 modules_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed modules") + console.print(" seedgo modules") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 modules_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/naming_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/naming_standard.py index fc50f8e1..6caf9c5b 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/naming_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/naming_standard.py @@ -2,7 +2,7 @@ Naming Standards Module Provides condensed naming standards for AIPass branches. -Run directly or via: drone @seed naming +Run directly or via: seedgo naming """ # =================== META ==================== @@ -65,14 +65,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed naming") + console.print(" seedgo naming") console.print(" python3 naming_standard.py") console.print(" python3 naming_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed naming") + console.print(" seedgo naming") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 naming_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/permission_flags_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/permission_flags_standard.py index ecbb6bce..41075198 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/permission_flags_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/permission_flags_standard.py @@ -2,7 +2,7 @@ Permission Flags Standards Module Provides condensed permission flags standards for AIPass branches. -Run directly or via: drone @seed permission_flags +Run directly or via: seedgo permission_flags """ # =================== META ==================== @@ -60,14 +60,14 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed permission_flags") + console.print(" seedgo permission_flags") console.print(" python3 permission_flags_standard.py") console.print(" python3 permission_flags_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed permission_flags") + console.print(" seedgo permission_flags") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 permission_flags_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/readme_update.py b/src/aipass/seedgo/apps/standards/aipass/modules/readme_update.py index dae0ac24..04be1815 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/readme_update.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/readme_update.py @@ -5,7 +5,7 @@ On-demand updating of auto-generated README sections for any AIPass branch. Uses readme_generator handler to produce content for sections marked with comment markers in README.md files. -Run: python3 seed.py readme update @branch +Run: seedgo readme update @branch """ # =================== META ==================== diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/standards_audit.py b/src/aipass/seedgo/apps/standards/aipass/modules/standards_audit.py index 3ce9c4ac..5aaf3bad 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/standards_audit.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/standards_audit.py @@ -4,7 +4,7 @@ Standards Audit Module Scans all AIPass branches and generates compliance dashboard. Shows per-branch scores, system-wide metrics, and top issues. -Run: python3 seed.py audit +Run: seedgo audit """ # =================== META ==================== @@ -207,20 +207,20 @@ def print_help(): console.print("[yellow]USAGE:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed audit") - console.print(" drone @seed audit cortex") + console.print(" seedgo audit") + console.print(" seedgo audit cortex") console.print() - console.print(" python3 seed.py audit") - console.print(" python3 seed.py audit cortex") - console.print(" python3 seed.py audit --help") + console.print(" seedgo audit") + console.print(" seedgo audit cortex") + console.print(" seedgo audit --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Full system audit (all branches, all files)[/dim]") - console.print(" python3 seed.py audit") + console.print(" seedgo audit") console.print() console.print(" [dim]# Full audit of specific branch[/dim]") - console.print(" python3 seed.py audit cortex") + console.print(" seedgo audit cortex") console.print() console.print("[yellow]REFERENCE:[/yellow]") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/standards_checklist.py b/src/aipass/seedgo/apps/standards/aipass/modules/standards_checklist.py index b73ba453..be51c03d 100755 --- a/src/aipass/seedgo/apps/standards/aipass/modules/standards_checklist.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/standards_checklist.py @@ -2,7 +2,7 @@ Standards Checklist Module Validates modules against AIPass code standards. -Run directly or via: drone @seed checklist +Run directly or via: seedgo checklist """ # =================== META ==================== @@ -56,6 +56,23 @@ from aipass.seedgo.apps.standards.aipass.handlers.standards import log_handler_c from aipass.seedgo.apps.standards.aipass.handlers.standards import log_visibility_check from aipass.seedgo.apps.standards.aipass.handlers.standards import permission_flags_check from aipass.seedgo.apps.standards.aipass.handlers.standards import shebang_check +from aipass.seedgo.apps.standards.aipass.handlers.standards import log_structure_check + +# ============================================================================= +# REGISTRY DISCOVERY +# ============================================================================= + +def _find_registry() -> Path: + """Find AIPASS_REGISTRY.json by walking up from this file's location.""" + current = Path(__file__).resolve().parent + for parent in [current] + list(current.parents): + candidate = parent / "AIPASS_REGISTRY.json" + if candidate.exists(): + return candidate + return Path.cwd() / "AIPASS_REGISTRY.json" + + +REGISTRY_PATH = _find_registry() # ============================================================================= # BYPASS SYSTEM - .seed/ config per branch @@ -91,7 +108,7 @@ BYPASS_TEMPLATE = { def get_branch_from_path(file_path: str) -> Optional[Dict[str, Any]]: """ - Detect which branch a file belongs to using BRANCH_REGISTRY + Detect which branch a file belongs to using AIPASS_REGISTRY Args: file_path: Absolute path to file being checked @@ -100,11 +117,11 @@ def get_branch_from_path(file_path: str) -> Optional[Dict[str, Any]]: Branch dict with name, path, etc. or None if not in a branch """ try: - if not BRANCH_REGISTRY_PATH.exists(): - logger.warning("[standards_checklist] BRANCH_REGISTRY.json not found") + if not REGISTRY_PATH.exists(): + logger.warning("[standards_checklist] AIPASS_REGISTRY.json not found") return None - with open(BRANCH_REGISTRY_PATH, 'r', encoding='utf-8') as f: + with open(REGISTRY_PATH, 'r', encoding='utf-8') as f: registry = json.load(f) file_path = str(Path(file_path).resolve()) @@ -241,7 +258,7 @@ def print_help(): console.print("[yellow]USAGE:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed checklist ") + console.print(" seedgo checklist ") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" drone @seedgo checklist ") @@ -846,10 +863,29 @@ def print_checklist(args: List[str]): console.print() logger.info(f"[{MODULE_NAME}] SHEBANG check complete: {shebang_score}/100") + # Run log_structure check + logger.info(f"[{MODULE_NAME}] Running LOG_STRUCTURE standard check on {file_path}") + console.print("[bold cyan]LOG_STRUCTURE STANDARD:[/bold cyan]") + log_structure_result = log_structure_check.check_module(file_path, bypass_rules=bypass_rules) + + # Display results + for check in log_structure_result['checks']: + symbol = "[green]✓[/green]" if check['passed'] else "[red]✗[/red]" + console.print(f" {symbol} {check['name']}: {check['message']}") + + console.print() + + # Show score + log_structure_score = log_structure_result['score'] + log_structure_status = "[green]PASS[/green]" if log_structure_result['passed'] else "[red]FAIL[/red]" + console.print(f" Score: {log_structure_score}/100 - {log_structure_status}") + console.print() + logger.info(f"[{MODULE_NAME}] LOG_STRUCTURE check complete: {log_structure_score}/100") + # Overall summary - avg_score = int((imports_score + architecture_score + naming_score + cli_score + handlers_score + modules_score + documentation_score + json_structure_score + testing_score + error_handling_score + encapsulation_score + trigger_score + log_level_score + cli_flags_score + log_handler_score + log_visibility_score + permission_flags_score + shebang_score) / 18) + avg_score = int((imports_score + architecture_score + naming_score + cli_score + handlers_score + modules_score + documentation_score + json_structure_score + testing_score + error_handling_score + encapsulation_score + trigger_score + log_level_score + cli_flags_score + log_handler_score + log_visibility_score + permission_flags_score + shebang_score + log_structure_score) / 19) console.print("─" * 70) - console.print(f"[bold]OVERALL:[/bold] 18/18 standards checked - {avg_score}% average compliance") + console.print(f"[bold]OVERALL:[/bold] 19/19 standards checked - {avg_score}% average compliance") console.print("─" * 70) console.print() logger.info(f"[{MODULE_NAME}] Standards check complete: {avg_score}% average compliance") diff --git a/src/aipass/seedgo/apps/standards/aipass/modules/trigger_standard.py b/src/aipass/seedgo/apps/standards/aipass/modules/trigger_standard.py index cf49086a..9388231a 100644 --- a/src/aipass/seedgo/apps/standards/aipass/modules/trigger_standard.py +++ b/src/aipass/seedgo/apps/standards/aipass/modules/trigger_standard.py @@ -2,7 +2,7 @@ Trigger Standards Module Provides condensed trigger event bus standards for AIPass branches. -Run directly or via: drone @seed trigger +Run directly or via: seedgo trigger """ # =================== META ==================== @@ -59,15 +59,15 @@ def print_help(): console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" drone @seed trigger") + console.print(" seedgo trigger") console.print(" python3 trigger_standard.py") console.print(" python3 trigger_standard.py --help") console.print() console.print("[yellow]EXAMPLES:[/yellow]") console.print(" [dim]# Via drone[/dim]") - console.print(" drone @seed trigger") - console.print(" drone @seed events") + console.print(" seedgo trigger") + console.print(" seedgo events") console.print() console.print(" [dim]# Standalone[/dim]") console.print(" python3 trigger_standard.py") diff --git a/src/aipass/seedgo/apps/standards/aipass/pack.json b/src/aipass/seedgo/apps/standards/aipass/pack.json index c69b20d8..8c73f4dc 100644 --- a/src/aipass/seedgo/apps/standards/aipass/pack.json +++ b/src/aipass/seedgo/apps/standards/aipass/pack.json @@ -4,7 +4,7 @@ "description": "AIPass framework standards for multi-agent Python projects", "author": "AIPass", "file_types": ["*.py"], - "standards_count": 20, + "standards_count": 21, "standards": [ "architecture", "cli", @@ -24,6 +24,7 @@ "naming", "permission_flags", "readme", + "shebang", "testing", "trigger" ], diff --git a/src/aipass/seedgo/docs/aipass_code_standards/architecture.md b/src/aipass/seedgo/docs/aipass_code_standards/architecture.md index 91275cad..095c58dc 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/architecture.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/architecture.md @@ -36,7 +36,7 @@ apps/handlers/ (Implementation Layer) **Rule:** All branches must match the Cortex template structure. -**Source of Truth:** `/home/aipass/aipass_core/cortex/templates/branch_template/.template_registry.json` +**Source of Truth:** `/cortex/templates/branch_template/.template_registry.json` **What it checks:** - All required files from template exist in branch (with proper name transformations) @@ -166,7 +166,7 @@ apps/handlers/ └── cli/ → Everything about user interaction ``` -**Verified from:** `/home/aipass/aipass_core/cortex/apps/handlers/` - These are actual domains from Cortex. +**Verified from:** `/cortex/apps/handlers/` - These are actual domains from Cortex. **Note:** Actual domain names will vary by branch purpose. See naming.md for domain naming standards. @@ -323,7 +323,7 @@ Run 'python3 create_plan.py --help' for usage Drone handles TWO critical responsibilities: -1. **@ Resolution:** Converts branch handles (@flow, @seed, @cortex) to absolute paths (/home/aipass/flow, /home/aipass/seed, /home/aipass/aipass_core/cortex) +1. **@ Resolution:** Converts branch handles (@flow, @seed, @cortex) to absolute paths (/home/aipass/flow, /home/aipass/seed, /cortex) 2. **Command Routing:** Routes the resolved command to the target branch's entry point ### How @ Resolution Works diff --git a/src/aipass/seedgo/docs/aipass_code_standards/cli.md b/src/aipass/seedgo/docs/aipass_code_standards/cli.md index f5d3617b..35b6fda8 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/cli.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/cli.md @@ -273,9 +273,9 @@ success("Operation complete!") **Reference:** - Standard: `/home/aipass/standards/CODE_STANDARDS/cli.md` -- Implementation: `/home/aipass/aipass_core/cli/` +- Implementation: `/cli/` - Example Usage: `/home/aipass/seed/apps/modules/test_cli_errors.py` -- Interactive Demo: `/home/aipass/aipass_core/planning/cli_layout_demo.py` +- Interactive Demo: `/planning/cli_layout_demo.py` - Module Demo: `python3 /home/aipass/seed/apps/modules/cli_standard.py` --- diff --git a/src/aipass/seedgo/docs/aipass_code_standards/error_handling.md b/src/aipass/seedgo/docs/aipass_code_standards/error_handling.md index 5c466572..ddc97424 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/error_handling.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/error_handling.md @@ -424,8 +424,8 @@ def handler_function(param1: str, param2: int) -> dict: ## Reference **See Flow for working example:** -- `/home/aipass/aipass_core/flow/apps/modules/` -- `/home/aipass/aipass_core/flow/apps/handlers/` +- `/flow/apps/modules/` +- `/flow/apps/handlers/` **See Seed for reference implementation:** - `/home/aipass/seed/apps/modules/` @@ -444,7 +444,7 @@ def handler_function(param1: str, param2: int) -> dict: Prax handlers in `apps/handlers/logging/` use Python's stdlib `logging` instead of `system_logger`. This is **intentional** to avoid circular dependencies — the logging handlers ARE the logging infrastructure. -**Status:** Documented in `/home/aipass/aipass_core/prax/.seed/bypass.json` +**Status:** Documented in `/prax/.seed/bypass.json` ### Trigger Infrastructure Handlers diff --git a/src/aipass/seedgo/docs/aipass_code_standards/handlers.md b/src/aipass/seedgo/docs/aipass_code_standards/handlers.md index 0cb386e3..5014ae37 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/handlers.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/handlers.md @@ -310,7 +310,7 @@ from cortex.apps.modules.error_tracking import track_operation **Location:** Each branch's `handlers/__init__.py` -**Reference implementation:** `/home/aipass/aipass_core/prax/apps/handlers/__init__.py` +**Reference implementation:** `/prax/apps/handlers/__init__.py` **How it works:** 1. Guard runs at import time when `handlers/` package is imported @@ -395,8 +395,8 @@ Example with library services: Drone routes commands from CLI to branches: ```bash drone @flow create "new plan" -# Drone resolves @flow → /home/aipass/aipass_core/flow/flow.py -# Drone runs: python3 /home/aipass/aipass_core/flow/flow.py create "new plan" +# Drone resolves @flow → /flow/flow.py +# Drone runs: python3 /flow/flow.py create "new plan" ``` The @ symbol is resolved by Drone BEFORE the command reaches the branch. @@ -415,7 +415,7 @@ When a branch is invoked via Drone: drone @flow create @project1 --name "test" # Drone resolves @ symbols: -@flow → /home/aipass/aipass_core/flow/flow.py +@flow → /flow/flow.py @project1 → /home/aipass/projects/project1 # Flow receives: diff --git a/src/aipass/seedgo/docs/aipass_code_standards/imports.md b/src/aipass/seedgo/docs/aipass_code_standards/imports.md index 85037206..73e66dab 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/imports.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/imports.md @@ -303,7 +303,7 @@ from drone.apps.modules.router import route_command # NO! # Working dir: /home/aipass/seed/ import subprocess -subprocess.run(["python3", "/home/aipass/aipass_core/ai_mail/apps/ai_mail.py", +subprocess.run(["python3", "/ai_mail/apps/ai_mail.py", "send", "@drone", "Subject", "Message"]) # AI_MAIL walks up from your CWD, finds SEED.id.json, knows you're @seed @@ -314,7 +314,7 @@ subprocess.run(["python3", "/home/aipass/aipass_core/ai_mail/apps/ai_mail.py", # ✗ WRONG - Call from wrong directory # Working dir: /home/aipass/ -subprocess.run(["python3", "/home/aipass/aipass_core/ai_mail/apps/ai_mail.py", +subprocess.run(["python3", "/ai_mail/apps/ai_mail.py", "send", "@drone", "Subject", "Message"]) # AI_MAIL can't find branch identity, falls back to @dev_central @@ -356,7 +356,7 @@ from pathlib import Path # Ensure you're in your branch directory branch_dir = Path(__file__).parent.parent # Adjust based on file depth result = subprocess.run( - ["python3", "/home/aipass/aipass_core/ai_mail/apps/ai_mail.py", + ["python3", "/ai_mail/apps/ai_mail.py", "send", "@recipient", "Subject", "Message"], cwd=str(branch_dir), # Force working directory to branch root capture_output=True diff --git a/src/aipass/seedgo/docs/aipass_code_standards/json_structure.md b/src/aipass/seedgo/docs/aipass_code_standards/json_structure.md index f4d9fbfe..ae6a3fe9 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/json_structure.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/json_structure.md @@ -20,7 +20,7 @@ drone_discovery_log.json **Location patterns:** - Seed modules: `/home/aipass/seed/seed_json/` -- Branch modules: `/home/aipass/aipass_core/{branch}/{branch}_json/` +- Branch modules: `/{branch}/{branch}_json/` --- @@ -125,14 +125,14 @@ JSON_TEMPLATES_DIR = {BRANCH}_ROOT / "apps" / "json_templates" **4. Create required directories:** ```bash -mkdir -p /home/aipass/aipass_core/{branch}/{branch}_json -mkdir -p /home/aipass/aipass_core/{branch}/apps/json_templates/default +mkdir -p /{branch}/{branch}_json +mkdir -p /{branch}/apps/json_templates/default ``` **5. Copy templates from SEED:** ```bash cp /home/aipass/seed/apps/json_templates/default/*.json \ - /home/aipass/aipass_core/{branch}/apps/json_templates/default/ + /{branch}/apps/json_templates/default/ ``` ### Validation @@ -140,7 +140,7 @@ cp /home/aipass/seed/apps/json_templates/default/*.json \ **Run standards checker on your json_handler.py:** ```bash python3 /home/aipass/seed/apps/modules/standards_checklist.py \ - /home/aipass/aipass_core/{branch}/apps/handlers/json/json_handler.py + /{branch}/apps/handlers/json/json_handler.py ``` **Expected result:** 100/100 on JSON STRUCTURE standard @@ -370,14 +370,14 @@ json_handler.log_operation("operation_name", {"key": "value"}) "branches": [ { "name": "FLOW", - "path": "/home/aipass/aipass_core/flow", + "path": "/flow", "email": "@flow", "status": "active", "created": "2025-10-30" }, { "name": "CORTEX", - "path": "/home/aipass/aipass_core/cortex", + "path": "/cortex", "email": "@cortex", "status": "active", "created": "2025-10-30" @@ -394,8 +394,8 @@ json_handler.log_operation("operation_name", {"key": "value"}) - Typically located at system root level **Other registries in use:** -- `/home/aipass/aipass_core/drone/drone_json/drone_registry.json` - Drone commands -- `/home/aipass/aipass_core/prax/prax_json/prax_registry.json` - Prax tracks +- `/drone/drone_json/drone_registry.json` - Drone commands +- `/prax/prax_json/prax_registry.json` - Prax tracks **When to create registry:** - Need to track multiple related items @@ -658,9 +658,9 @@ ls /branch/branch_json/ **Primary handler:** `/home/aipass/seed/apps/handlers/json/json_handler.py` **Branch implementations:** -- `/home/aipass/aipass_core/cortex/apps/handlers/json/json_handler.py` -- `/home/aipass/aipass_core/drone/apps/handlers/json/json_handler.py` -- `/home/aipass/aipass_core/prax/apps/handlers/json/json_handler.py` +- `/cortex/apps/handlers/json/json_handler.py` +- `/drone/apps/handlers/json/json_handler.py` +- `/prax/apps/handlers/json/json_handler.py` - (Other branches follow same pattern) **Status:** Production-ready, tested across Seed and core branches diff --git a/src/aipass/seedgo/docs/aipass_code_standards/modules.md b/src/aipass/seedgo/docs/aipass_code_standards/modules.md index ba456bfa..05c6e484 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/modules.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/modules.md @@ -252,7 +252,7 @@ Based on analysis of `/home/aipass/seed/apps/modules/` (14 modules): 4. **Change isolation** - Smaller files = smaller blast radius for changes **Example: CLI Display Module** -`/home/aipass/aipass_core/cli/apps/modules/display.py` - 184 lines +`/cli/apps/modules/display.py` - 184 lines - Public API for 5 display functions - Light orchestration over handler implementation - Includes docstrings, examples, demo @@ -771,7 +771,7 @@ def test_track_operation_exception(): - Shows orchestration flow - Demonstrates good practices -**Service module (184 lines):** `/home/aipass/aipass_core/cli/apps/modules/display.py` +**Service module (184 lines):** `/cli/apps/modules/display.py` - Public API wrapper - Thin orchestration over handlers - Multiple function exports diff --git a/src/aipass/seedgo/docs/aipass_code_standards/testing.md b/src/aipass/seedgo/docs/aipass_code_standards/testing.md index b9f51228..fd54b1f5 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/testing.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/testing.md @@ -98,7 +98,7 @@ Process: **Infrastructure in place:** - `pytest.ini` at root - Configuration for test discovery and markers - `/home/aipass/tests/` - Root test directory with conftest.py -- `/home/aipass/aipass_core/tests/` - Core system tests +- `/tests/` - Core system tests - Branch-specific test directories (API, Prax, CLI, etc.) **Already operational in some branches:** diff --git a/src/aipass/seedgo/docs/aipass_code_standards/trigger.md b/src/aipass/seedgo/docs/aipass_code_standards/trigger.md index e52d4f82..9accbb2e 100644 --- a/src/aipass/seedgo/docs/aipass_code_standards/trigger.md +++ b/src/aipass/seedgo/docs/aipass_code_standards/trigger.md @@ -103,7 +103,7 @@ def fire_event(name, **data): ## Handler Requirements -Handlers live in `/home/aipass/aipass_core/trigger/apps/handlers/events/` +Handlers live in `/trigger/apps/handlers/events/` ### Handler Interface ```python @@ -371,7 +371,7 @@ For Trigger branch itself: ## Reference -- **Trigger Core:** `/home/aipass/aipass_core/trigger/apps/modules/core.py` -- **Handler Registry:** `/home/aipass/aipass_core/trigger/apps/handlers/events/registry.py` -- **Event Handlers:** `/home/aipass/aipass_core/trigger/apps/handlers/events/*.py` +- **Trigger Core:** `/trigger/apps/modules/core.py` +- **Handler Registry:** `/trigger/apps/handlers/events/registry.py` +- **Event Handlers:** `/trigger/apps/handlers/events/*.py` - **Standard:** `/home/aipass/seed/standards/CODE_STANDARDS/trigger.md` diff --git a/src/aipass/spawn/README.md b/src/aipass/spawn/README.md new file mode 100644 index 00000000..67752585 --- /dev/null +++ b/src/aipass/spawn/README.md @@ -0,0 +1,75 @@ +# SPAWN + +**Purpose:** Agent creation and template management. Creates new agents from templates, replaces placeholders, and registers them. +**Module:** `aipass.spawn` +**Created:** 2026-03-05 + +--- + +## Overview + +### What I Do +- Create new agent directories from the bundled template +- Replace all `{{PLACEHOLDER}}` patterns with branch-specific values +- Register new agents in `AIPASS_REGISTRY.json` +- Regenerate `.template_registry.json` with fresh file hashes +- Validate no unreplaced placeholders remain + +### Usage + +```python +from aipass.spawn import spawn_agent + +result = spawn_agent( + "/path/to/new/agent", + role="Data Analyst", + purpose="Process incoming reports", + traits="Precise, thorough" +) + +# result dict includes: success, branch_name, path, files_copied, validation_issues +``` + +--- + +## Architecture + +``` +spawn/ +├── __init__.py # Public API (exports spawn_agent) +├── apps/ +│ ├── spawn.py # Entry point (CLI) +│ ├── modules/ +│ │ └── core.py # Orchestrator — coordinates all steps +│ └── handlers/ +│ ├── file_ops.py # Template copy, path renaming +│ ├── metadata.py # Branch name extraction, profile detection +│ ├── placeholders.py # {{PLACEHOLDER}} replacement engine +│ └── registry.py # AIPASS_REGISTRY.json CRUD +├── templates/ +│ ├── agent.template/ # Source template for new agents +│ └── agent_mock_branch/ # Reference implementation +└── tests/ + └── test_spawn.py # 13 tests covering all components +``` + +--- + +## Spawn Workflow + +1. **Resolve** — Extract branch name from target path, validate path doesn't exist +2. **Copy** — Recursive copy of `agent.template/` to target (skips `__pycache__`, `.gitkeep`) +3. **Rename** — Replace `{{BRANCH}}` in directory and file names +4. **Replace** — Substitute all `{{PLACEHOLDER}}` patterns in file contents +5. **Registry** — Regenerate `.template_registry.json`, register in `AIPASS_REGISTRY.json` +6. **Validate** — Scan for any remaining `{{...}}` patterns + +--- + +## Integration Points + +### Depends On +- Python stdlib only (`pathlib`, `json`, `shutil`, `hashlib`, `re`) + +### Provides To +- All modules — agent scaffolding and lifecycle setup diff --git a/src/aipass/spawn/__init__.py b/src/aipass/spawn/__init__.py index e5a89ec0..0ac8a55d 100644 --- a/src/aipass/spawn/__init__.py +++ b/src/aipass/spawn/__init__.py @@ -6,6 +6,6 @@ Usage: result = spawn_agent("/path/to/new/agent", role="My Role", purpose="My Purpose") """ -from aipass.spawn.spawn import spawn_agent +from aipass.spawn.apps.modules.core import spawn_agent __all__ = ["spawn_agent"] diff --git a/src/aipass/spawn/file_ops.py b/src/aipass/spawn/apps/handlers/file_ops.py similarity index 98% rename from src/aipass/spawn/file_ops.py rename to src/aipass/spawn/apps/handlers/file_ops.py index c5e80566..673a4db6 100644 --- a/src/aipass/spawn/file_ops.py +++ b/src/aipass/spawn/apps/handlers/file_ops.py @@ -3,7 +3,7 @@ import shutil from pathlib import Path -from aipass.spawn.placeholders import replace_placeholders +from aipass.spawn.apps.handlers.placeholders import replace_placeholders # Patterns to skip during template copy SKIP_NAMES = {"__pycache__", ".git", ".template_registry.json"} diff --git a/src/aipass/spawn/metadata.py b/src/aipass/spawn/apps/handlers/metadata.py similarity index 100% rename from src/aipass/spawn/metadata.py rename to src/aipass/spawn/apps/handlers/metadata.py diff --git a/src/aipass/spawn/placeholders.py b/src/aipass/spawn/apps/handlers/placeholders.py similarity index 100% rename from src/aipass/spawn/placeholders.py rename to src/aipass/spawn/apps/handlers/placeholders.py diff --git a/src/aipass/spawn/registry.py b/src/aipass/spawn/apps/handlers/registry.py similarity index 76% rename from src/aipass/spawn/registry.py rename to src/aipass/spawn/apps/handlers/registry.py index 0dbad640..5c06ef48 100644 --- a/src/aipass/spawn/registry.py +++ b/src/aipass/spawn/apps/handlers/registry.py @@ -1,14 +1,21 @@ """AIPASS_REGISTRY.json CRUD operations.""" import json +import os from datetime import datetime from pathlib import Path def find_registry(start_path=None): """ - Find AIPASS_REGISTRY.json by walking up from start_path. - Falls back to project root (where pyproject.toml or .git lives). + Find AIPASS_REGISTRY.json — consistent with drone's resolution. + + Priority: + 1. AIPASS_REGISTRY environment variable + 2. Project root (directory with pyproject.toml or .git) — walk up from __file__ + 3. Project root — walk up from start_path/cwd + 4. Walk up from __file__ for any existing registry + 5. Last resort: cwd Args: start_path: Directory to start searching from @@ -16,19 +23,33 @@ def find_registry(start_path=None): Returns: Path to AIPASS_REGISTRY.json """ - current = Path(start_path).resolve() if start_path else Path.cwd() + # Check environment variable first (same as drone's config.py) + env_path = os.environ.get("AIPASS_REGISTRY") + if env_path: + return Path(env_path) - # Walk up looking for existing registry - for parent in [current] + list(current.parents): - candidate = parent / "AIPASS_REGISTRY.json" - if candidate.exists(): + # Walk up from package location — find project root first + pkg_dir = Path(__file__).resolve().parent + for parent in [pkg_dir] + list(pkg_dir.parents): + if (parent / "pyproject.toml").exists() or (parent / ".git").exists(): + candidate = parent / "AIPASS_REGISTRY.json" + if candidate.exists(): + return candidate + # Project root found but no registry — create here return candidate - # No registry found — place it at project root (pyproject.toml or .git) + # Walk up from start_path or cwd + current = Path(start_path).resolve() if start_path else Path.cwd() for parent in [current] + list(current.parents): if (parent / "pyproject.toml").exists() or (parent / ".git").exists(): return parent / "AIPASS_REGISTRY.json" + # Fallback: any existing registry walking up from package + for parent in [pkg_dir] + list(pkg_dir.parents): + candidate = parent / "AIPASS_REGISTRY.json" + if candidate.exists(): + return candidate + # Last resort: cwd return Path.cwd() / "AIPASS_REGISTRY.json" diff --git a/src/aipass/spawn/spawn.py b/src/aipass/spawn/apps/modules/core.py similarity index 92% rename from src/aipass/spawn/spawn.py rename to src/aipass/spawn/apps/modules/core.py index 25f7db92..11d24620 100644 --- a/src/aipass/spawn/spawn.py +++ b/src/aipass/spawn/apps/modules/core.py @@ -15,13 +15,13 @@ import hashlib import json from pathlib import Path -from aipass.spawn.metadata import get_branch_name, normalize_branch_name, detect_profile -from aipass.spawn.placeholders import build_replacements_dict, validate_no_placeholders -from aipass.spawn.file_ops import copy_template, rename_placeholder_paths -from aipass.spawn.registry import find_registry, add_to_registry +from aipass.spawn.apps.handlers.metadata import get_branch_name, normalize_branch_name, detect_profile +from aipass.spawn.apps.handlers.placeholders import build_replacements_dict, validate_no_placeholders +from aipass.spawn.apps.handlers.file_ops import copy_template, rename_placeholder_paths +from aipass.spawn.apps.handlers.registry import find_registry, add_to_registry -# Default template location (relative to this file) -DEFAULT_TEMPLATE = Path(__file__).parent / "templates" / "agent.template" +# Default template location (relative to spawn package root) +DEFAULT_TEMPLATE = Path(__file__).parents[2] / "templates" / "agent.template" def spawn_agent(target_path, role="", traits="", purpose="", profile=None, diff --git a/src/aipass/spawn/apps/spawn.py b/src/aipass/spawn/apps/spawn.py index 6113c7aa..e9d43417 100644 --- a/src/aipass/spawn/apps/spawn.py +++ b/src/aipass/spawn/apps/spawn.py @@ -40,7 +40,7 @@ def print_help(): def handle_create(args): """Handle the create command.""" - from aipass.spawn.spawn import spawn_agent + from aipass.spawn.apps.modules.core import spawn_agent if not args: console.print("[red]Error: target path required[/red]") diff --git a/src/aipass/spawn/tests/test_spawn.py b/src/aipass/spawn/tests/test_spawn.py index 8332d639..bf773ffd 100644 --- a/src/aipass/spawn/tests/test_spawn.py +++ b/src/aipass/spawn/tests/test_spawn.py @@ -6,9 +6,9 @@ import pytest from pathlib import Path from aipass.spawn import spawn_agent -from aipass.spawn.metadata import get_branch_name, normalize_branch_name, detect_profile -from aipass.spawn.placeholders import build_replacements_dict, validate_no_placeholders -from aipass.spawn.registry import load_registry, add_to_registry +from aipass.spawn.apps.handlers.metadata import get_branch_name, normalize_branch_name, detect_profile +from aipass.spawn.apps.handlers.placeholders import build_replacements_dict, validate_no_placeholders +from aipass.spawn.apps.handlers.registry import load_registry, add_to_registry @pytest.fixture diff --git a/src/aipass/trigger/.claude/settings.local.json b/src/aipass/trigger/.claude/settings.local.json deleted file mode 100644 index 0c90b038..00000000 --- a/src/aipass/trigger/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [], - "deny": [], - "ask": [] - }, - "enabledMcpjsonServers": [] -} diff --git a/src/aipass/trigger/.seed/bypass.json b/src/aipass/trigger/.seed/bypass.json index a1503c34..da201288 100644 --- a/src/aipass/trigger/.seed/bypass.json +++ b/src/aipass/trigger/.seed/bypass.json @@ -1,7 +1,7 @@ { "metadata": { "version": "1.0.0", - "created": "2026-03-06T03:16:11.259352", + "created": "2026-03-06T14:27:53.459073", "description": "Standards bypass configuration for this branch" }, "bypass": [], diff --git a/src/aipass/trigger/README.md b/src/aipass/trigger/README.md index 4bee43bb..fa5fb4c5 100644 --- a/src/aipass/trigger/README.md +++ b/src/aipass/trigger/README.md @@ -1,51 +1,85 @@ -# TRIGGER +# Trigger -**Purpose:** Error detection, pattern matching, auto-dispatch to responsible agents -**Module:** `aipass.trigger` -**Created:** 2026-03-05 +Event bus for AIPass. Branches fire events, registered handlers react. Decouples producers from consumers — the module that detects a condition doesn't need to know what should happen next. ---- +## Usage -## Overview +### CLI (via drone) -### What I Do +```bash +drone @trigger --help # Show available commands +drone @trigger medic on # Enable error auto-dispatch +drone @trigger medic off # Disable error auto-dispatch +drone @trigger medic status # Show medic state +drone @trigger medic mute @branch # Suppress errors from a branch +drone @trigger medic unmute @branch # Resume errors from a branch +drone @trigger errors list # List tracked errors +drone @trigger errors stats # Error registry statistics +drone @trigger errors circuit-breaker # Circuit breaker state +drone @trigger --version # Show version +``` +### Python API -### How I Work -- **Entry Point:** `apps/trigger.py` -- **Pattern:** Auto-discovers and routes to modules +```python +from aipass.trigger.apps.modules.core import Trigger ---- +# Fire an event — all registered handlers run +Trigger.fire("plan_created", plan_id=42, branch="flow") + +# Register a handler +def on_plan_created(**data): + print(f"Plan {data['plan_id']} created") + +Trigger.on("plan_created", on_plan_created) + +# Remove a handler +Trigger.off("plan_created", on_plan_created) +``` + +### Cross-branch error reporting + +```python +from aipass.trigger.apps.modules.errors import report_error + +result = report_error( + branch="api", + error_type="ConnectionError", + message="Timeout reaching upstream", + source_file="client.py", +) +# Returns: {"is_new": True, "fingerprint": "abc123", "count": 1, ...} +``` + +## Events + +| Event | Fired when | +|-------|------------| +| `startup` | Branch session starts | +| `plan_created` | New flow plan created | +| `plan_closed` | Flow plan closed | +| `error_logged` | Error detected in logs | +| `error_detected` | Error registered and ready for dispatch | +| `warning_logged` | Warning detected in logs | +| `memory_threshold_exceeded` | Memory file approaching line limit | +| `memory_template_updated` | Memory template changed | +| `bulletin_created` | New system bulletin posted | + +## Medic + +Built-in error monitoring subsystem. Watches logs for errors, fingerprints them via SHA1, deduplicates, and dispatches fix-it notifications to the responsible branch. Includes circuit breaker, per-branch rate limiting, and mute controls. ## Architecture ``` -TRIGGER/ +trigger/ ├── apps/ -│ ├── trigger.py # Entry point -│ ├── modules/ # Business logic -│ ├── handlers/ # Implementation -│ └── plugins/ # Extensions -├── docs/ -├── tests/ -├── passport.json # Identity -├── local.json # Session history -├── observations.json # Collaboration patterns -└── README.md +│ ├── trigger.py # Entry point (auto-discovers modules) +│ ├── modules/ +│ │ ├── core.py # Event bus (Trigger.fire/on/off) +│ │ ├── errors.py # Error registry + cross-branch API +│ │ └── medic.py # Error monitoring commands +│ └── handlers/ +│ └── events/ # One handler per event type +└── tests/ ``` - ---- - -## Commands - -*Configure after initialization* - ---- - -## Integration Points - -### Depends On - - -### Provides To - diff --git a/src/aipass/trigger/__init__.py b/src/aipass/trigger/__init__.py new file mode 100644 index 00000000..3d18d18a --- /dev/null +++ b/src/aipass/trigger/__init__.py @@ -0,0 +1 @@ +"""Trigger - Event-driven automation for AIPass.""" diff --git a/src/aipass/trigger/apps/handlers/__init__.py b/src/aipass/trigger/apps/handlers/__init__.py index 55cb7ed3..1bbd77fc 100644 --- a/src/aipass/trigger/apps/handlers/__init__.py +++ b/src/aipass/trigger/apps/handlers/__init__.py @@ -26,7 +26,7 @@ def _find_real_caller(): def _extract_branch_name(filepath: str) -> str: """Extract branch name from a file path.""" - parts = filepath.split("/") + parts = Path(filepath).parts for i, part in enumerate(parts): if part in ("aipass", "seed", ".vscode"): if i + 1 < len(parts): diff --git a/src/aipass/trigger/apps/handlers/events/memory_template_updated.py b/src/aipass/trigger/apps/handlers/events/memory_template_updated.py index 76a794a1..9782f53d 100644 --- a/src/aipass/trigger/apps/handlers/events/memory_template_updated.py +++ b/src/aipass/trigger/apps/handlers/events/memory_template_updated.py @@ -47,8 +47,5 @@ def handle_memory_template_updated(**kwargs: Any) -> None: Args: **kwargs: Event data (template_name, updated_by, timestamp, etc.) """ - try: - from aipass.memory_bank.apps.handlers.templates.pusher import push_templates - push_templates(dry_run=False) - except Exception: - pass + # memory_bank integration (optional, requires memory_bank package) + pass diff --git a/src/aipass/trigger/trigger_json/trigger_data.json b/src/aipass/trigger/trigger_json/trigger_data.json index ab017b33..37ecdbbf 100644 --- a/src/aipass/trigger/trigger_json/trigger_data.json +++ b/src/aipass/trigger/trigger_json/trigger_data.json @@ -1,14 +1,14 @@ { "error_catchup": { - "last_scan_timestamp": "2026-03-06T20:38:05.532132", + "last_scan_timestamp": "2026-03-06T14:27:27.869613", "processed_hashes": [ - "68dd0ab6", - "4095ef97", - "02d93909", - "541e0b9b", - "69de737b", "ace01637", - "c7edc03b" + "69de737b", + "c7edc03b", + "4095ef97", + "68dd0ab6", + "541e0b9b", + "02d93909" ], "max_hashes": 500, "max_lookback_hours": 24 diff --git a/system_logs/ai_mail_ai_mail.log b/system_logs/ai_mail_ai_mail.log deleted file mode 100644 index b01f0233..00000000 --- a/system_logs/ai_mail_ai_mail.log +++ /dev/null @@ -1,35 +0,0 @@ -2026-03-06 19:26:01 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 19:26:01 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 19:26:01 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 19:26:01 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 19:26:01 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 19:26:27 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 19:26:27 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 19:26:27 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 19:26:27 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 19:26:27 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:32:14 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:32:14 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:32:14 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:32:14 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:32:14 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:32:37 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:32:37 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:32:37 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:32:37 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:32:37 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:32:42 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:32:42 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:32:42 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:32:42 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:32:42 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:33:19 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:33:19 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:33:19 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:33:19 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:33:19 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules -2026-03-06 20:33:19 - captured_ai_mail - INFO - [ai_mail] Discovering modules... -2026-03-06 20:33:19 - captured_ai_mail - ERROR - [-] aipass.ai_mail.apps.modules.email - import error: No module named 'aipass.dev_central' -2026-03-06 20:33:19 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.dispatch -2026-03-06 20:33:19 - captured_ai_mail - INFO - [+] aipass.ai_mail.apps.modules.branch_ping -2026-03-06 20:33:19 - captured_ai_mail - INFO - [ai_mail] Discovered 2 modules diff --git a/system_logs/ai_mail_dispatch.log b/system_logs/ai_mail_dispatch.log deleted file mode 100644 index 58f0a13f..00000000 --- a/system_logs/ai_mail_dispatch.log +++ /dev/null @@ -1 +0,0 @@ -2026-03-06 20:33:19 - captured_dispatch - INFO - [dispatch] Showing dispatch status diff --git a/system_logs/api_api.log b/system_logs/api_api.log deleted file mode 100644 index 85218703..00000000 --- a/system_logs/api_api.log +++ /dev/null @@ -1,29 +0,0 @@ -2026-03-06 19:25:57 - captured_api - INFO - [api] Discovering modules... -2026-03-06 19:25:57 - captured_api - INFO - [+] telegram_service -2026-03-06 19:25:57 - captured_api - INFO - [+] telegram_bot -2026-03-06 19:25:57 - captured_api - INFO - [+] api_key -2026-03-06 19:25:57 - captured_api - ERROR - [-] openrouter_client - import error: name 'OpenAI' is not defined -2026-03-06 19:25:57 - captured_api - INFO - [+] usage_tracker -2026-03-06 19:25:57 - captured_api - INFO - [api] Discovered 4 modules -2026-03-06 19:25:57 - captured_api - WARNING - Unknown command: test -2026-03-06 19:26:31 - captured_api - INFO - [api] Discovering modules... -2026-03-06 19:26:31 - captured_api - INFO - [+] telegram_service -2026-03-06 19:26:31 - captured_api - INFO - [+] telegram_bot -2026-03-06 19:26:31 - captured_api - INFO - [+] api_key -2026-03-06 19:26:31 - captured_api - ERROR - [-] openrouter_client - import error: name 'OpenAI' is not defined -2026-03-06 19:26:31 - captured_api - INFO - [+] usage_tracker -2026-03-06 19:26:31 - captured_api - INFO - [api] Discovered 4 modules -2026-03-06 19:26:38 - captured_api - INFO - [api] Discovering modules... -2026-03-06 19:26:38 - captured_api - INFO - [+] telegram_service -2026-03-06 19:26:38 - captured_api - INFO - [+] telegram_bot -2026-03-06 19:26:38 - captured_api - INFO - [+] api_key -2026-03-06 19:26:38 - captured_api - ERROR - [-] openrouter_client - import error: name 'OpenAI' is not defined -2026-03-06 19:26:38 - captured_api - INFO - [+] usage_tracker -2026-03-06 19:26:38 - captured_api - INFO - [api] Discovered 4 modules -2026-03-06 19:26:41 - captured_api - INFO - [api] Discovering modules... -2026-03-06 19:26:41 - captured_api - INFO - [+] telegram_service -2026-03-06 19:26:41 - captured_api - INFO - [+] telegram_bot -2026-03-06 19:26:41 - captured_api - INFO - [+] api_key -2026-03-06 19:26:41 - captured_api - ERROR - [-] openrouter_client - import error: name 'OpenAI' is not defined -2026-03-06 19:26:41 - captured_api - INFO - [+] usage_tracker -2026-03-06 19:26:41 - captured_api - INFO - [api] Discovered 4 modules diff --git a/system_logs/flow_aggregate_central.log b/system_logs/flow_aggregate_central.log deleted file mode 100644 index 16772a69..00000000 --- a/system_logs/flow_aggregate_central.log +++ /dev/null @@ -1,6 +0,0 @@ -2026-03-06 19:26:45 - captured_aggregate_central - INFO - [aggregate_central] Starting central aggregation -2026-03-06 19:26:45 - captured_aggregate_central - INFO - [aggregate_central] Processing branch: flow -2026-03-06 19:26:45 - captured_aggregate_central - INFO - [aggregate_central] SUCCESS: Aggregation complete: 0 active, 0 recently closed -2026-03-06 20:20:39 - captured_aggregate_central - INFO - [aggregate_central] Starting central aggregation -2026-03-06 20:20:39 - captured_aggregate_central - INFO - [aggregate_central] Processing branch: flow -2026-03-06 20:20:39 - captured_aggregate_central - INFO - [aggregate_central] SUCCESS: Aggregation complete: 1 active, 0 recently closed diff --git a/system_logs/flow_close_plan.log b/system_logs/flow_close_plan.log deleted file mode 100644 index f2bdde1a..00000000 --- a/system_logs/flow_close_plan.log +++ /dev/null @@ -1,4 +0,0 @@ -2026-03-06 19:26:52 - captured_close_plan - INFO - [close_plan] Deleted empty template file: /tmp/FPLAN-0001_test_plan_from_devpulse_2026-03-06.md -2026-03-06 19:26:52 - captured_close_plan - INFO - [close_plan] Removed FPLAN-0001 from registry -2026-03-06 20:21:02 - captured_close_plan - INFO - [close_plan] Deleted empty template file: /home/coder/workspace/src/aipass/flow/FPLAN-0002_2026-03-06.md -2026-03-06 20:21:02 - captured_close_plan - INFO - [close_plan] Removed FPLAN-0002 from registry diff --git a/system_logs/flow_create_plan.log b/system_logs/flow_create_plan.log deleted file mode 100644 index 5a9510b1..00000000 --- a/system_logs/flow_create_plan.log +++ /dev/null @@ -1,2 +0,0 @@ -2026-03-06 19:26:45 - captured_create_plan - INFO - [create_plan] Created FPLAN-0001 in /tmp -2026-03-06 20:20:39 - captured_create_plan - INFO - [create_plan] Created FPLAN-0002 in flow diff --git a/system_logs/flow_flow.log b/system_logs/flow_flow.log deleted file mode 100644 index ddf35a1b..00000000 --- a/system_logs/flow_flow.log +++ /dev/null @@ -1,77 +0,0 @@ -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:25:13 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:25:53 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:26:45 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:26:49 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 19:26:52 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:20:13 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:20:22 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:20:39 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:21:02 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:37:06 - captured_flow - INFO - [FLOW] Loaded module: create_plan -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Skipped post_close_runner - no handle_command() -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: aggregate_central -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: registry_monitor -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: restore_plan -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: list_plans -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: close_plan -2026-03-06 20:38:04 - captured_flow - INFO - [FLOW] Loaded module: create_plan diff --git a/system_logs/flow_list_plans.log b/system_logs/flow_list_plans.log deleted file mode 100644 index 3aa9e8f2..00000000 --- a/system_logs/flow_list_plans.log +++ /dev/null @@ -1,2 +0,0 @@ -2026-03-06 19:25:53 - captured_list_plans - INFO - [list_plans] No plans in registry -2026-03-06 19:26:49 - captured_list_plans - INFO - [list_plans] Listed plans (filter: open) diff --git a/system_logs/prax_monitor_module.log b/system_logs/prax_monitor_module.log deleted file mode 100644 index 3194a590..00000000 --- a/system_logs/prax_monitor_module.log +++ /dev/null @@ -1,2 +0,0 @@ -2026-03-06 20:19:23 - captured_monitor_module - INFO - Starting unified monitoring (args: []) -2026-03-06 20:19:23 - captured_monitor_module - INFO - All monitoring threads started diff --git a/system_logs/prax_unknown_module.log b/system_logs/prax_unknown_module.log deleted file mode 100644 index c3922564..00000000 --- a/system_logs/prax_unknown_module.log +++ /dev/null @@ -1,2 +0,0 @@ -2026-03-06 18:38:57 - captured_unknown_module - INFO - test -2026-03-06 18:39:46 - captured_unknown_module - INFO - DPLAN-047 Phase 2: Path portability fix verified diff --git a/system_logs/seedgo_seedgo_verify.log b/system_logs/seedgo_seedgo_verify.log deleted file mode 100644 index 367ee470..00000000 --- a/system_logs/seedgo_seedgo_verify.log +++ /dev/null @@ -1 +0,0 @@ -2026-03-06 19:24:36 - captured_seedgo_verify - INFO - Seedgo verify: 5/5 (100%) diff --git a/system_logs/seedgo_standards_checklist.log b/system_logs/seedgo_standards_checklist.log deleted file mode 100644 index 1cce6cb5..00000000 --- a/system_logs/seedgo_standards_checklist.log +++ /dev/null @@ -1,78 +0,0 @@ -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Starting standards compliance check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - ERROR - [standards_checklist] Error reading branch registry: name 'BRANCH_REGISTRY_PATH' is not defined -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running IMPORTS standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] IMPORTS check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running ARCHITECTURE standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] ARCHITECTURE check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running NAMING standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] NAMING check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running CLI standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] CLI check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running HANDLERS standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] HANDLERS check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running MODULES standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] MODULES check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running DOCUMENTATION standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] DOCUMENTATION check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running JSON_STRUCTURE standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] JSON_STRUCTURE check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running TESTING standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] TESTING check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running ERROR_HANDLING standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] ERROR_HANDLING check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running ENCAPSULATION standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] ENCAPSULATION check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running TRIGGER standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] TRIGGER check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_LEVEL standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] LOG_LEVEL check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running CLI_FLAGS standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] CLI_FLAGS check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_HANDLER standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] LOG_HANDLER check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_VISIBILITY standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] LOG_VISIBILITY check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running PERMISSION_FLAGS standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] PERMISSION_FLAGS check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Running SHEBANG standard check on src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] SHEBANG check complete: 0/100 -2026-03-06 19:25:42 - captured_standards_checklist - INFO - [standards_checklist] Standards check complete: 0% average compliance -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Starting standards compliance check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - ERROR - [standards_checklist] Error reading branch registry: name 'BRANCH_REGISTRY_PATH' is not defined -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running IMPORTS standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] IMPORTS check complete: 80/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running ARCHITECTURE standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] ARCHITECTURE check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running NAMING standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] NAMING check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running CLI standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] CLI check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running HANDLERS standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] HANDLERS check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running MODULES standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] MODULES check complete: 60/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running DOCUMENTATION standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] DOCUMENTATION check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running JSON_STRUCTURE standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] JSON_STRUCTURE check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running TESTING standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] TESTING check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running ERROR_HANDLING standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] ERROR_HANDLING check complete: 66/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running ENCAPSULATION standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] ENCAPSULATION check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running TRIGGER standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] TRIGGER check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_LEVEL standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] LOG_LEVEL check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running CLI_FLAGS standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] CLI_FLAGS check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_HANDLER standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] LOG_HANDLER check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running LOG_VISIBILITY standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] LOG_VISIBILITY check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running PERMISSION_FLAGS standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] PERMISSION_FLAGS check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Running SHEBANG standard check on /home/coder/workspace/src/aipass/drone/apps/modules/config.py -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] SHEBANG check complete: 100/100 -2026-03-06 19:25:46 - captured_standards_checklist - INFO - [standards_checklist] Standards check complete: 94% average compliance diff --git a/system_logs/trigger_core.log b/system_logs/trigger_core.log deleted file mode 100644 index 1fb9834e..00000000 --- a/system_logs/trigger_core.log +++ /dev/null @@ -1,128 +0,0 @@ -2026-03-06 18:38:57 - captured_core - INFO - [TRIGGER] logger fired: startup -2026-03-06 18:38:57 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 18:39:46 - captured_core - INFO - [TRIGGER] logger fired: startup -2026-03-06 18:39:46 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:02:03 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:02:03 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:24:36 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:24:36 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:24:39 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:24:39 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:24:49 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:24:49 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:09 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:09 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:13 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:13 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:13 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:13 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:21 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:21 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:21 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:21 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:27 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:27 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:42 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:42 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:42 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:42 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:46 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:46 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:46 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:46 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:53 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:53 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:57 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:57 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:01 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:01 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:07 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:07 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:27 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:27 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:31 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:31 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:31 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:26:31 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:38 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:38 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:38 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:26:38 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:41 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:41 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:41 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:26:41 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] flow fired: central_aggregated -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] 0 handlers responding -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] flow fired: plan_created -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] 0 handlers responding -2026-03-06 19:26:49 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:49 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:52 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:52 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:55:03 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:55:03 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:55:03 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:55:03 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:19:23 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:19:23 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:19:23 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:19:23 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:13 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:20:13 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:22 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:20:22 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:22 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:20:22 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] flow fired: central_aggregated -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] 0 handlers responding -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] flow fired: plan_created -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] 0 handlers responding -2026-03-06 20:21:02 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:21:02 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:32:14 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:32:14 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:32:37 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:32:37 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:32:42 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:32:42 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:33:19 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:33:19 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:33:19 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:33:19 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:03 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:03 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] 1 handlers responding diff --git a/system_logs/trigger_trigger.log b/system_logs/trigger_trigger.log deleted file mode 100644 index 6e5017a5..00000000 --- a/system_logs/trigger_trigger.log +++ /dev/null @@ -1,35 +0,0 @@ -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.medic: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.errors: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.branch_log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.core: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.medic: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.errors: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.branch_log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.core: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.medic: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.errors: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.branch_log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.core: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.medic: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.errors: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.branch_log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.core: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Loaded module: medic -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Loaded module: errors -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Loaded module: branch_log_events -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Skipped core - no handle_command() -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Loaded module: log_events -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Loaded module: medic -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Loaded module: errors -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Loaded module: branch_log_events -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Skipped core - no handle_command() -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Loaded module: log_events -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Loaded module: medic -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Loaded module: errors -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Loaded module: branch_log_events -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Skipped core - no handle_command() -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Loaded module: log_events diff --git a/trigger/logs/core.log b/trigger/logs/core.log deleted file mode 100644 index 1fb9834e..00000000 --- a/trigger/logs/core.log +++ /dev/null @@ -1,128 +0,0 @@ -2026-03-06 18:38:57 - captured_core - INFO - [TRIGGER] logger fired: startup -2026-03-06 18:38:57 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 18:39:46 - captured_core - INFO - [TRIGGER] logger fired: startup -2026-03-06 18:39:46 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:02:03 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:02:03 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:24:36 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:24:36 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:24:39 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:24:39 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:24:49 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:24:49 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:09 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:09 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:13 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:13 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:13 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:13 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:21 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:21 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:21 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:21 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:27 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:27 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:42 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:42 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:42 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:42 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:46 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:46 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:46 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:25:46 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:53 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:53 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:25:57 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:25:57 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:01 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:01 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:07 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:07 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:27 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:27 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:31 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:31 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:31 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:26:31 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:38 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:38 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:38 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:26:38 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:41 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:41 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:41 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:26:41 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] flow fired: central_aggregated -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] 0 handlers responding -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] flow fired: plan_created -2026-03-06 19:26:45 - captured_core - INFO - [TRIGGER] 0 handlers responding -2026-03-06 19:26:49 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:49 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:26:52 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:26:52 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:53:58 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:55:03 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 19:55:03 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 19:55:03 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 19:55:03 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:19:23 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:19:23 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:19:23 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:19:23 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:13 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:20:13 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:22 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:20:22 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:22 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:20:22 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] flow fired: central_aggregated -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] 0 handlers responding -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] flow fired: plan_created -2026-03-06 20:20:39 - captured_core - INFO - [TRIGGER] 0 handlers responding -2026-03-06 20:21:02 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:21:02 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:32:14 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:32:14 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:32:37 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:32:37 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:32:42 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:32:42 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:33:19 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:33:19 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:33:19 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:33:19 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:37:06 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:37:07 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:03 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:03 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:04 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] prax fired: startup -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] 1 handlers responding -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] cli fired: cli_header_displayed -2026-03-06 20:38:05 - captured_core - INFO - [TRIGGER] 1 handlers responding diff --git a/trigger/logs/trigger.log b/trigger/logs/trigger.log deleted file mode 100644 index 6e5017a5..00000000 --- a/trigger/logs/trigger.log +++ /dev/null @@ -1,35 +0,0 @@ -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.medic: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.errors: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.branch_log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.core: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:25:21 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.medic: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.errors: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.branch_log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.core: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:26:07 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.medic: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.errors: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.branch_log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.core: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.medic: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.errors: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.branch_log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.core: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:53:58 - captured_trigger - ERROR - [TRIGGER] Failed to load module trigger.apps.modules.log_events: No module named 'trigger.apps'; 'trigger' is not a package -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Loaded module: medic -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Loaded module: errors -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Loaded module: branch_log_events -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Skipped core - no handle_command() -2026-03-06 19:55:03 - captured_trigger - INFO - [TRIGGER] Loaded module: log_events -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Loaded module: medic -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Loaded module: errors -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Loaded module: branch_log_events -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Skipped core - no handle_command() -2026-03-06 20:37:07 - captured_trigger - INFO - [TRIGGER] Loaded module: log_events -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Loaded module: medic -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Loaded module: errors -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Loaded module: branch_log_events -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Skipped core - no handle_command() -2026-03-06 20:38:05 - captured_trigger - INFO - [TRIGGER] Loaded module: log_events diff --git a/your b/your deleted file mode 100644 index 87b0b639..00000000 --- a/your +++ /dev/null @@ -1,2 +0,0 @@ -export PATH="$HOME/.local/bin:$PATH" shell config file -export PATH="$HOME/.local/bin:$PATH" shell config file