feat(system): seedgo v2 operational, full system audit, 14/15 branches at 99%
Three days of intensive work bringing seedgo to full operational status and driving all branches through comprehensive standards compliance. Seedgo v2.0.0: - 22 checkers active (up from 20), standards pack fully operational - New introspection standard researched from Dev-Pass, FPLAN-0017 open - Bypass system for false positives (.seedgo config) - Standards query and audit commands fully functional System-wide audit (FPLAN-0016): - All 14 auditable branches at 99%+ compliance - CLI imports standardized across all branches (console from cli.apps.modules) - handle_command(command, args) → bool contract added to all modules - print_help() function naming fixed for checker pattern matching - Handler extraction: large modules split, file I/O moved to handler layer - New handlers created across ai_mail, backup, daemon, flow, skills, spawn, seedgo Branch-specific highlights: - ai_mail: email.py split 840→420 lines, 4 new handlers - flow: dplan_flow.py 688→591 lines, 4 new handlers - seedgo: massive restructure — standards moved to handlers/aipass_standards/, old standards/ tree removed, bypass system added, diagnostics module - commons: database module added, CLI imports fixed - skills: 5 handle_commands added, help function renamed - trigger: error reporter handler, handle_command routing - All branches: consistent architecture, clean drone routing Culture doc (CLAUDE.md) added — documents AIPass philosophy, identity, memory system, and collaboration principles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
09e759a8a4
commit
babedd9c64
@@ -1,5 +1,5 @@
|
||||
# AIPass System Context
|
||||
<!-- Injected on every prompt via hook. Branch-specific context appears below when in a branch directory. -->
|
||||
<!-- File: .aipass/aipass_global_prompt.md — Injected on every prompt via hook. Branch-specific context appears below when in a branch directory. -->
|
||||
|
||||
**This prompt is your guide.** The patterns shown here are exact. Don't guess command syntax — the examples ARE the API.
|
||||
|
||||
@@ -42,12 +42,15 @@ src/aipass/{name}/
|
||||
**15 branches:** drone, seedgo, prax, cli, flow, ai_mail, api, trigger, spawn, devpulse, backup, daemon, memory, commons, skills
|
||||
|
||||
## Commands
|
||||
|
||||
`drone` is a global CLI — never `cd` before running it. No full paths. Drone resolves everything.
|
||||
|
||||
```
|
||||
drone @branch command [args] # Route command to any branch
|
||||
drone @branch --help # Branch help
|
||||
drone systems # List all registered branches
|
||||
drone @seedgo audit aipass # Run standards audit on all branches
|
||||
drone @seedgo verify # Verify standards packs installed
|
||||
drone @seedgo standards_query aipass_standards # List all standards (then query by name)
|
||||
drone @prax monitor # Real-time monitoring (interactive)
|
||||
```
|
||||
|
||||
@@ -82,6 +85,7 @@ from aipass.prax import logger
|
||||
- **No deleting files.** Move to `.archive/` or rename with `(disabled)`.
|
||||
- **Verify after fixing.** Run a test or command to confirm. Don't say "fixed" until verified.
|
||||
- **Cross-platform.** AIPass is a public package — code must work on Linux, macOS, and Windows. Use `pathlib.Path` not string concatenation. Use `Path.home()` not `~` or `/home/`. Secrets live at `~/.secrets/aipass/` (`Path.home() / ".secrets" / "aipass"`).
|
||||
- **Fail to errors, never fall back silently.** When a command, handler, or module receives input it can't handle, return an explicit error — not a silent fallback to default output. No dimming, no swallowing, no showing the same screen regardless of input. The user must see that their input was received and rejected. Show what's missing (no help available, no introspection, no subcommands) and where to look (file path). Dead ends must announce themselves.
|
||||
|
||||
## Memories
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ def main():
|
||||
new_count = count_new_emails(branch_root)
|
||||
if new_count > 0:
|
||||
plural = "s" if new_count != 1 else ""
|
||||
print(f"\n You have {new_count} new email{plural} - check your inbox")
|
||||
print(f"You have {new_count} new email{plural} - check with: drone @ai_mail inbox | then: drone @ai_mail view <id> | close with: drone @ai_mail close <id>")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -49,3 +49,4 @@ tools/
|
||||
|
||||
# Test artifacts
|
||||
test/
|
||||
src/aipass/seedgo/apps/standards/aipass/pack.json
|
||||
|
||||
@@ -7,7 +7,7 @@ Injected every turn. Keep this lean — details live in README, --help, and .tri
|
||||
Greetings (`hi`, `hello`, `yo`, `hey`, `sup`, `good morning`, `good evening`, `what's up`) trigger startup protocol. Everything else is a direct task.
|
||||
|
||||
**On startup, read:** `.trinity/passport.json`, `local.json`, `observations.json`, `DASHBOARD.local.json`, `README.md`
|
||||
**Then run:** `git status`, `drone systems`, `drone @seedgo verify`
|
||||
**Then run:** `git status`, `drone systems`
|
||||
|
||||
## Navigation
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ dev = [
|
||||
|
||||
[project.scripts]
|
||||
drone = "aipass.drone.cli:main"
|
||||
seedgo = "aipass.seedgo.apps.seedgo:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/aipass"]
|
||||
|
||||
@@ -1,41 +1,23 @@
|
||||
# ai_mail
|
||||
# AI_MAIL
|
||||
|
||||
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.
|
||||
**Purpose:** 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.
|
||||
**Module:** `aipass.ai_mail`
|
||||
**Created:** 2025-11-08
|
||||
**Last Updated:** 2026-03-08
|
||||
|
||||
---
|
||||
|
||||
**Status:** Building. Core email workflow (send/inbox/reply/close) is functional. Dispatch system is working.
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI (via drone)
|
||||
## Commands / Usage
|
||||
|
||||
```bash
|
||||
# Send a message
|
||||
drone @ai_mail send @flow "Bug Report" "Found an issue in plan closing"
|
||||
|
||||
# Check inbox
|
||||
drone @ai_mail inbox
|
||||
|
||||
# View a message (marks as opened)
|
||||
drone @ai_mail view <message_id>
|
||||
|
||||
# Reply (auto-closes original)
|
||||
drone @ai_mail reply <message_id> "Fixed in v2.1"
|
||||
|
||||
# Close without reply
|
||||
drone @ai_mail close <message_id>
|
||||
|
||||
# Send with dispatch flag (recipient auto-executes the task)
|
||||
drone @ai_mail send @flow "Task" "Details here" --dispatch
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```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"])
|
||||
drone @ai_mail send @target "Subject" "Body" # Send inter-branch email
|
||||
drone @ai_mail send @target "Subject" "Body" --dispatch # Send task dispatch email
|
||||
drone @ai_mail dispatch wake @target # Wake a branch
|
||||
drone @ai_mail dispatch wake --fresh @target # Fresh wake (no context)
|
||||
drone @ai_mail inbox # Check inbox
|
||||
drone @ai_mail --help # Full help
|
||||
```
|
||||
|
||||
## Email Lifecycle
|
||||
@@ -78,8 +60,19 @@ ai_mail/
|
||||
│ └── users/ # Branch detection, config generation
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
## Integration Points
|
||||
|
||||
- `prax` — Logging
|
||||
- `cli` — Display formatting
|
||||
- `drone` — Command routing and `@branch` resolution
|
||||
### Depends On
|
||||
- `aipass.prax` — Logging via `system_logger`
|
||||
- `aipass.cli` — Console output and display formatting
|
||||
- `aipass.drone` — Command routing and `@branch` resolution
|
||||
- Python stdlib (`pathlib`, `json`, `argparse`, `importlib`)
|
||||
|
||||
### Provides To
|
||||
- All modules — inter-branch messaging (send/receive/reply/close)
|
||||
- Dispatch system — autonomous task execution via `--dispatch` flag
|
||||
- Branch contacts — address book for `@branch` routing
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2026-03-08*
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: ai_mail.py - AI_MAIL Branch Orchestrator
|
||||
# Date: 2025-11-08
|
||||
# =================== AIPass ====================
|
||||
# Name: ai_mail.py
|
||||
# Description: Entry point CLI for drone @ai_mail — inter-branch email system
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/orchestrator
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -26,7 +25,7 @@ from typing import Dict, Any, Optional, List
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
|
||||
# Dashboard integration (optional, requires dev_central package)
|
||||
_update_section = None # type: ignore
|
||||
_UPDATE_SECTION = None # type: ignore
|
||||
|
||||
# AIPass infrastructure imports
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: central_writer.py - AI_MAIL Central File Writer
|
||||
# Date: 2025-11-27
|
||||
# =================== AIPass ====================
|
||||
# Name: central_writer.py
|
||||
# Description: AI_MAIL Central File Writer
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-27): Initial implementation - central file writer
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler tier 3: pure functions, raises exceptions
|
||||
# - No CLI imports (Prax, Rich, etc.)
|
||||
# - Follows Seed handler patterns
|
||||
# Created: 2025-11-27
|
||||
# Modified: 2025-11-27
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: daemon.py - Dispatch Daemon Handler
|
||||
# Date: 2026-02-17
|
||||
# =================== AIPass ====================
|
||||
# Name: daemon.py
|
||||
# Description: Dispatch Daemon Handler
|
||||
# Version: 1.8.0
|
||||
# Category: ai_mail/handlers/dispatch
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.8.0 (2026-03-02): Wire spawn_agent through dispatch_monitor (bounce emails + guaranteed lock cleanup)
|
||||
# - v1.7.0 (2026-03-02): Redirect stderr to log file + reap zombie children (debug silent deaths)
|
||||
# - v1.6.0 (2026-03-01): Add AIPASS_SESSION_TYPE env var + session rename for /resume picker
|
||||
# - v1.5.0 (2026-02-22): Stale lock cleanup every cycle + orphaned opened email retry (>30 min)
|
||||
# - v1.4.0 (2026-02-21): Skip spawn if branch has active Claude session (prevent toe-stepping)
|
||||
# - v1.3.0 (2026-02-20): DPLAN-024 Phase 3 - strip heartbeat logic (migrated to assistant plugins)
|
||||
# - v1.2.0 (2026-02-20): DPLAN-024 - fix hardcoded paths, inline lock_utils
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-handler or module imports
|
||||
# - Pure business logic only
|
||||
# - Uses Prax system_logger (FPLAN-0382)
|
||||
# Created: 2026-02-17
|
||||
# Modified: 2026-02-17
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -238,7 +223,7 @@ def load_config() -> Dict[str, Any]:
|
||||
"kill_switch_path": str(_REPO_ROOT / ".aipass" / "autonomous_pause"),
|
||||
"poll_interval_seconds": 300,
|
||||
"max_depth": 3,
|
||||
"max_turns_per_wake": 50,
|
||||
"max_turns_per_wake": 100,
|
||||
"max_dispatches_per_branch_per_day": 10,
|
||||
"session_rotation_cycles": 12,
|
||||
"cold_start_prompt": "Hi. Check inbox, process new emails, update memories when done.",
|
||||
@@ -407,7 +392,7 @@ def spawn_agent(
|
||||
sender = message.get("from", "unknown")
|
||||
msg_id = message.get("id", "unknown")
|
||||
subject = message.get("subject", "")
|
||||
max_turns = config.get("max_turns_per_wake", 15)
|
||||
max_turns = config.get("max_turns_per_wake", 100)
|
||||
|
||||
lock_file_path = str(branch_path / ".ai_mail.local" / ".dispatch.lock")
|
||||
|
||||
@@ -480,11 +465,9 @@ def spawn_agent(
|
||||
notif_title = f"Daemon → {branch_email}"
|
||||
notif_body = f"Task from {sender}: \"{subject[:80]}\"" if subject else f"Dispatch from {sender}"
|
||||
try:
|
||||
subprocess.run(
|
||||
["notify-send", "-i", "dialog-information", notif_title, notif_body],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
||||
from aipass.ai_mail.apps.handlers.notify import send_notification
|
||||
send_notification(notif_title, notif_body, source=branch_email.lstrip("@"))
|
||||
except Exception:
|
||||
logger.info(f"Desktop notification unavailable for {branch_email}")
|
||||
|
||||
logger.info(f"SPAWN {branch_email} PID={monitor_pid} (monitor) sender={sender} subject=\"{subject[:60]}\"")
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: dispatch_monitor.py - Agent Lifecycle Monitor
|
||||
# Date: 2026-03-02
|
||||
# =================== AIPass ====================
|
||||
# Name: dispatch_monitor.py
|
||||
# Description: Agent Lifecycle Monitor
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/dispatch
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-03-02): Initial — wraps claude spawn, handles cleanup + bounce on failure
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-handler or module imports
|
||||
# - Runs as a detached background process
|
||||
# - Uses subprocess to send bounce emails (avoids imports)
|
||||
# Created: 2026-03-02
|
||||
# Modified: 2026-03-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -123,6 +114,12 @@ def main():
|
||||
for key in list(spawn_env.keys()):
|
||||
if key.startswith("CLAUDE") or key == "AIPASS_BOT_ID":
|
||||
spawn_env.pop(key)
|
||||
# Strip caller identity vars to prevent dispatch context leakage.
|
||||
spawn_env.pop("AIPASS_CALLER_BRANCH", None)
|
||||
spawn_env.pop("AIPASS_CALLER_CWD", None)
|
||||
# Set CWD-independent branch identity so agent knows who it is
|
||||
# even after cd'ing away. Drone reads this as fallback for caller detection.
|
||||
spawn_env["AIPASS_BRANCH_NAME"] = branch_email.lstrip("@")
|
||||
|
||||
# Extract CWD from lock file path (branch_path/.ai_mail.local/.dispatch.lock)
|
||||
lock_path = Path(lock_file)
|
||||
@@ -132,10 +129,15 @@ def main():
|
||||
start_time = time.time()
|
||||
|
||||
# Run claude — BLOCKING. Monitor stays alive as long as agent is working.
|
||||
stdout_log = str(branch_path / ".ai_mail.local" / "agent_stdout.log")
|
||||
try:
|
||||
stdout_fh = open(stdout_log, 'w', encoding='utf-8')
|
||||
except OSError:
|
||||
stdout_fh = subprocess.DEVNULL
|
||||
try:
|
||||
result = subprocess.run(
|
||||
claude_cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stdout=stdout_fh if isinstance(stdout_fh, int) else stdout_fh,
|
||||
stderr=stderr_fh,
|
||||
cwd=cwd,
|
||||
env=spawn_env,
|
||||
@@ -153,10 +155,29 @@ def main():
|
||||
|
||||
duration = int(time.time() - start_time)
|
||||
|
||||
# Close stdout log
|
||||
if not isinstance(stdout_fh, int):
|
||||
try:
|
||||
stdout_fh.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Check for max-turns hit (Claude exits 0 but output contains stop_reason)
|
||||
max_turns_hit = False
|
||||
try:
|
||||
with open(stdout_log, 'r', encoding='utf-8') as f:
|
||||
stdout_content = f.read()
|
||||
if '"stop_reason":"max_turns"' in stdout_content or '"stop_reason": "max_turns"' in stdout_content:
|
||||
max_turns_hit = True
|
||||
logger.warning("[monitor] %s HIT MAX TURNS after %ds — work may be incomplete", branch_email, duration)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Log completion
|
||||
if not isinstance(stderr_fh, int):
|
||||
try:
|
||||
stderr_fh.write(f"\n--- Agent exited: code={exit_code}, duration={duration}s ---\n")
|
||||
suffix = " [MAX TURNS HIT]" if max_turns_hit else ""
|
||||
stderr_fh.write(f"\n--- Agent exited: code={exit_code}, duration={duration}s{suffix} ---\n")
|
||||
stderr_fh.flush()
|
||||
stderr_fh.close()
|
||||
except OSError:
|
||||
@@ -188,17 +209,21 @@ def main():
|
||||
except OSError:
|
||||
logger.info("[monitor] Failed to clean up lock file %s", lock_file)
|
||||
|
||||
# Desktop notification on completion
|
||||
# Log completion to Prax
|
||||
status = "completed" if exit_code == 0 else f"FAILED (code {exit_code})"
|
||||
if max_turns_hit:
|
||||
status = f"MAX TURNS HIT ({duration}s)"
|
||||
logger.info("[monitor] %s agent %s — %ds", branch_email, status, duration)
|
||||
|
||||
# Desktop notification on completion
|
||||
try:
|
||||
subprocess.run(
|
||||
["notify-send", "-i",
|
||||
"dialog-information" if exit_code == 0 else "dialog-warning",
|
||||
f"Agent {branch_email} {status}",
|
||||
f"Duration: {duration}s"],
|
||||
capture_output=True, timeout=5
|
||||
from aipass.ai_mail.apps.handlers.notify import send_notification
|
||||
icon = "dialog-information" if exit_code == 0 else "dialog-warning"
|
||||
send_notification(
|
||||
f"Agent {branch_email} {status}", f"Duration: {duration}s",
|
||||
source=branch_email.lstrip("@"), icon=icon
|
||||
)
|
||||
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
||||
except Exception:
|
||||
logger.info("[monitor] Desktop notification unavailable")
|
||||
|
||||
sys.exit(0 if exit_code == 0 else 1)
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: pending_work.py - Pending Work Handler
|
||||
# Date: 2026-02-17
|
||||
# =================== AIPass ====================
|
||||
# Name: pending_work.py
|
||||
# Description: Pending Work Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/dispatch
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-17): Initial version - per-branch pending work tracking
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Pure business logic only
|
||||
# Created: 2026-02-17
|
||||
# Modified: 2026-02-17
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: status.py - Dispatch Status Handler
|
||||
# Date: 2026-02-02
|
||||
# =================== AIPass ====================
|
||||
# Name: status.py
|
||||
# Description: Dispatch Status Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/dispatch
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-02): Initial version - dispatch log operations
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Pure business logic only
|
||||
# Created: 2026-02-02
|
||||
# Modified: 2026-02-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: wake.py - Manual Branch Wake Handler
|
||||
# Date: 2026-03-02
|
||||
# =================== AIPass ====================
|
||||
# Name: wake.py
|
||||
# Description: Manual Branch Wake Handler
|
||||
# Version: 2.0.0
|
||||
# Category: ai_mail/handlers/dispatch
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.0 (2026-03-02): Step-by-step status, dispatch_monitor wrapper,
|
||||
# zombie cleanup, return-to-sender bounce, liveness check
|
||||
# - v1.7.0 (2026-03-02): Redirect stderr to log file instead of DEVNULL (debug silent deaths)
|
||||
# - v1.6.0 (2026-03-02): Manual wake returns success on active lock (agent will process inbox)
|
||||
# - v1.5.0 (2026-03-02): Manual wake bypasses autonomous_pause; add auto param for daemon use
|
||||
# - v1.4.0 (2026-03-01): Add AIPASS_SESSION_TYPE env var + session rename for /resume picker
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-handler or module imports
|
||||
# - Pure business logic only
|
||||
# - Uses Prax system_logger (FPLAN-0382)
|
||||
# Created: 2026-03-02
|
||||
# Modified: 2026-03-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -178,7 +164,7 @@ def _acquire_lock(branch_path: Path, pid: int) -> Tuple[bool, str]:
|
||||
|
||||
def _load_config() -> dict:
|
||||
"""Load safety config for max_turns."""
|
||||
defaults = {"max_turns_per_wake": 50}
|
||||
defaults = {"max_turns_per_wake": 100}
|
||||
config = _read_json(CONFIG_FILE)
|
||||
if config is None:
|
||||
return defaults
|
||||
@@ -381,7 +367,7 @@ def wake_branch(branch_email: str, custom_message: Optional[str] = None,
|
||||
|
||||
# Step 6: Build spawn command
|
||||
config = _load_config()
|
||||
max_turns = config.get("max_turns_per_wake", 50)
|
||||
max_turns = config.get("max_turns_per_wake", 100)
|
||||
|
||||
lock_file_path = str(branch_path / ".ai_mail.local" / ".dispatch.lock")
|
||||
if custom_message:
|
||||
@@ -473,11 +459,9 @@ def wake_branch(branch_email: str, custom_message: Optional[str] = None,
|
||||
# Desktop notification
|
||||
notif_body = custom_message[:80] if custom_message else "Manual wake: check inbox"
|
||||
try:
|
||||
subprocess.run(
|
||||
["notify-send", "-i", "dialog-information", f"Wake → {email}", notif_body],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
||||
from aipass.ai_mail.apps.handlers.notify import send_notification
|
||||
send_notification(f"Wake → {email}", notif_body, source=email.lstrip("@"))
|
||||
except Exception:
|
||||
logger.info("[wake] Desktop notification unavailable")
|
||||
|
||||
return status, True
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: close_ops.py
|
||||
# Description: Email Close Operations Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Email Close Operations Handler
|
||||
|
||||
Handles batch close operations and post-close cleanup.
|
||||
Independent handler - no module or display dependencies.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Callable, Optional
|
||||
|
||||
from aipass.prax import logger
|
||||
|
||||
|
||||
def batch_close(
|
||||
branch_path: Path,
|
||||
message_ids: List[str],
|
||||
mark_closed_fn: Callable,
|
||||
) -> Tuple[List[Tuple[str, bool, str]], int, int]:
|
||||
"""
|
||||
Close multiple emails by ID.
|
||||
|
||||
Args:
|
||||
branch_path: Path to branch directory
|
||||
message_ids: List of message IDs to close
|
||||
mark_closed_fn: Callable(branch_path, msg_id, skip_post_ops=bool) -> (bool, str)
|
||||
|
||||
Returns:
|
||||
Tuple of (results_list, closed_count, failed_count)
|
||||
results_list contains (message_id, success, message) tuples
|
||||
"""
|
||||
batch_mode = len(message_ids) > 1
|
||||
results = []
|
||||
closed_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for message_id in message_ids:
|
||||
success, message = mark_closed_fn(branch_path, message_id, skip_post_ops=batch_mode)
|
||||
results.append((message_id, success, message))
|
||||
if success:
|
||||
closed_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
|
||||
return results, closed_count, failed_count
|
||||
|
||||
|
||||
def batch_close_post_ops(
|
||||
branch_path: Path,
|
||||
push_dashboard_fn: Optional[Callable] = None,
|
||||
update_central_fn: Optional[Callable] = None,
|
||||
purge_deleted_fn: Optional[Callable] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Run post-operations after a batch close (dashboard update + purge).
|
||||
|
||||
Args:
|
||||
branch_path: Path to branch directory
|
||||
push_dashboard_fn: Optional push_dashboard_update callable
|
||||
update_central_fn: Optional update_central callable
|
||||
purge_deleted_fn: Optional purge_deleted_folder callable
|
||||
"""
|
||||
if push_dashboard_fn:
|
||||
try:
|
||||
push_dashboard_fn(branch_path)
|
||||
except Exception:
|
||||
pass
|
||||
if update_central_fn:
|
||||
try:
|
||||
update_central_fn()
|
||||
except Exception:
|
||||
pass
|
||||
if purge_deleted_fn:
|
||||
try:
|
||||
mailbox_path = branch_path / ".ai_mail.local"
|
||||
purge_deleted_fn(mailbox_path)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,21 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: create.py - Email File Creation Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: create.py
|
||||
# Description: Email File Creation Handler
|
||||
# Version: 1.2.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.4.0 (2026-02-04): Add auto-purge trigger after email creation
|
||||
# - v1.3.0 (2026-01-31): Add dispatched_to field for reply chain validation
|
||||
# - v1.2.0 (2026-01-29): Add auto-footer to all outgoing emails
|
||||
# - v1.1.0 (2026-01-29): Add reply_to parameter for redirecting replies
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Uses Prax system_logger (FPLAN-0382)
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -33,19 +21,9 @@ from typing import Dict
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
# Lazy imports
|
||||
_console = None
|
||||
_append_footer = None
|
||||
|
||||
|
||||
def _get_console():
|
||||
"""Lazy import console."""
|
||||
global _console
|
||||
if _console is None:
|
||||
from aipass.cli.apps.modules import console
|
||||
_console = console
|
||||
return _console
|
||||
|
||||
|
||||
def _get_append_footer():
|
||||
"""Lazy import append_footer."""
|
||||
global _append_footer
|
||||
@@ -169,7 +147,8 @@ def sanitize_subject(subject: str, max_length: int = 50) -> str:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
c = _get_console()
|
||||
from rich.console import Console
|
||||
c = Console()
|
||||
c.print("\n" + "="*70)
|
||||
c.print("EMAIL FILE CREATION HANDLER")
|
||||
c.print("="*70)
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: dashboard_sync.py - Dashboard Write-Through Helper
|
||||
# Date: 2026-02-25
|
||||
# =================== AIPass ====================
|
||||
# Name: dashboard_sync.py
|
||||
# Description: Dashboard Write-Through Helper
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-25): FPLAN-0373 Phase 2 - dashboard write-through for ai_mail
|
||||
# Reads inbox.json, calculates section data, calls write_section()
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-handler or module imports
|
||||
# - Can import AIPASS central services (devpulse write_section)
|
||||
# - No logger calls (module logs for handler)
|
||||
# - Pure business logic only
|
||||
# - BYPASS: Direct json.load required for reading inbox.json data files
|
||||
# - Dashboard failures never raised to caller
|
||||
# Created: 2026-02-25
|
||||
# Modified: 2026-02-25
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: delivery.py - Email Delivery Handler
|
||||
# Date: 2025-12-02
|
||||
# =================== AIPass ====================
|
||||
# Name: delivery.py
|
||||
# Description: Email Delivery Handler
|
||||
# Version: 3.0.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v3.1.0 (2026-02-25): Private branch inbound email blocking (DPLAN-035 email isolation)
|
||||
# - v3.0.0 (2026-02-17): Remove spawn logic — delivery is write-only, daemon handles all spawning
|
||||
# - v2.4.0 (2026-02-10): Seed compliance - remove logger calls, cross-handler imports, fix naming, add json_handler
|
||||
# - v2.3.0 (2026-02-10): Phase 3 polish - concise bounce messages, dispatch chain logging, hardened loop detection
|
||||
# - v2.2.0 (2026-02-10): DEV_CENTRAL dispatch protection, notification throttling, self-reply loop detection
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-handler or module imports
|
||||
# - No logger calls (module logs for handler)
|
||||
# - Pure business logic only
|
||||
# - Uses json_handler for JSON operations
|
||||
# Created: 2025-12-02
|
||||
# Modified: 2025-12-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -62,11 +48,11 @@ def _get_inbox_lock():
|
||||
|
||||
|
||||
def _get_console():
|
||||
"""Lazy import console."""
|
||||
"""Lazy import console - only for __main__ block."""
|
||||
global _CONSOLE
|
||||
if _CONSOLE is None:
|
||||
from aipass.cli.apps.modules import console
|
||||
_CONSOLE = console
|
||||
from rich.console import Console
|
||||
_CONSOLE = Console()
|
||||
return _CONSOLE
|
||||
|
||||
|
||||
@@ -495,13 +481,10 @@ def _send_desktop_notification(sender: str, recipient: str, subject: str, messag
|
||||
body = f"{subject}\n{preview}"
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
['notify-send', title, body],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
from aipass.ai_mail.apps.handlers.notify import send_notification
|
||||
send_notification(title, body, source=sender_name)
|
||||
_NOTIFICATION_TIMESTAMPS[recipient].append(now)
|
||||
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: error_dispatch.py
|
||||
# Description: Email Error Dispatch Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Email Error Dispatch Handler
|
||||
|
||||
Handles auto-dispatch of error reports when email delivery fails.
|
||||
Independent handler - no module or display dependencies.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Callable, Optional
|
||||
|
||||
from aipass.prax import logger
|
||||
|
||||
|
||||
def build_error_report(to_branch: str, subject: str, error_msg: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Build an error report email data dict for dispatch to @drone.
|
||||
|
||||
Args:
|
||||
to_branch: Intended recipient that failed
|
||||
subject: Original email subject
|
||||
error_msg: Error message from delivery failure
|
||||
|
||||
Returns:
|
||||
Email data dict ready for delivery, or empty dict on failure.
|
||||
"""
|
||||
sender = os.environ.get("AIPASS_CALLER_BRANCH", "ai_mail")
|
||||
sender = f"@{sender.lstrip('@')}"
|
||||
|
||||
error_body = (
|
||||
f"Email delivery failed.\n\n"
|
||||
f"From: {sender}\n"
|
||||
f"To: {to_branch}\n"
|
||||
f"Subject: {subject}\n"
|
||||
f"Error: {error_msg}\n\n"
|
||||
f"This error was auto-dispatched for investigation."
|
||||
)
|
||||
|
||||
return {
|
||||
"from": "@ai_mail",
|
||||
"from_name": "AI_MAIL",
|
||||
"to": "@drone",
|
||||
"subject": f"[ERROR] Send failed to {to_branch}: {error_msg[:50]}",
|
||||
"message": error_body,
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"auto_execute": False,
|
||||
"priority": "normal",
|
||||
"reply_to": "@dev_central",
|
||||
}
|
||||
|
||||
|
||||
def dispatch_send_error(
|
||||
to_branch: str,
|
||||
subject: str,
|
||||
error_msg: str,
|
||||
deliver_fn: Callable,
|
||||
) -> bool:
|
||||
"""
|
||||
Auto-dispatch error report to @drone when email delivery fails.
|
||||
|
||||
Args:
|
||||
to_branch: Intended recipient that failed
|
||||
subject: Original email subject
|
||||
error_msg: Error message from delivery failure
|
||||
deliver_fn: Callable to deliver email (deliver_email_to_branch)
|
||||
|
||||
Returns:
|
||||
True if error dispatched successfully, False otherwise.
|
||||
"""
|
||||
try:
|
||||
email_data = build_error_report(to_branch, subject, error_msg)
|
||||
deliver_fn("@drone", email_data)
|
||||
logger.info(f"[email] Error auto-dispatched to @drone for failed send to {to_branch}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"[email] Failed to dispatch send error to @drone: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def on_email_delivered(
|
||||
branch_path,
|
||||
new_count: int,
|
||||
opened_count: int,
|
||||
total: int,
|
||||
push_dashboard_fn: Optional[Callable] = None,
|
||||
update_central_fn: Optional[Callable] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Post-delivery callback: update dashboard and central.
|
||||
|
||||
Args:
|
||||
branch_path: Path to the branch that received email
|
||||
new_count: Number of new (unread) messages
|
||||
opened_count: Number of opened messages
|
||||
total: Total message count
|
||||
push_dashboard_fn: Callable for push_dashboard_update
|
||||
update_central_fn: Callable for update_central
|
||||
"""
|
||||
if push_dashboard_fn:
|
||||
try:
|
||||
push_dashboard_fn(branch_path)
|
||||
except Exception:
|
||||
pass # Dashboard update is best-effort
|
||||
if update_central_fn:
|
||||
try:
|
||||
update_central_fn()
|
||||
except Exception:
|
||||
pass # Central update is best-effort
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: footer.py - Email Footer Handler
|
||||
# Date: 2026-01-29
|
||||
# =================== AIPass ====================
|
||||
# Name: footer.py
|
||||
# Description: Email Footer Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-01-29): Created - auto-footer for all outgoing emails
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Can import Prax modules (service providers)
|
||||
# - Pure business logic only
|
||||
# Created: 2026-01-29
|
||||
# Modified: 2026-01-29
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -27,10 +18,10 @@ Independent handler - no module dependencies.
|
||||
STANDARD_FOOTER = """
|
||||
---
|
||||
⚠️ TASK CHECKLIST (before marking complete):
|
||||
□ SEED CHECK → drone @seed audit @branch (80%+)
|
||||
□ UPDATE MEMORIES → Your .local.json records this work
|
||||
□ SEEDGO CHECK → drone @seedgo audit @branch (80%+)
|
||||
□ UPDATE MEMORIES → Your .trinity/local.json records this work
|
||||
□ CLOSE FPLAN → drone @flow close <plan_id>
|
||||
□ CONFIRM → Reply with completion summary
|
||||
□ EMAIL SENDER → drone @ai_mail send @<sender> "Subject" "Summary"
|
||||
|
||||
Memories = Presence. No update = No learning.
|
||||
---"""
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: format.py - Email Formatting Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: format.py
|
||||
# Description: Email Formatting Handler
|
||||
# Version: 1.1.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.1.0 (2026-02-21): DPLAN-027 - Show branch alias in sender display
|
||||
# - v1.0.0 (2025-11-15): Created - email formatting and display utilities
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Uses Prax system_logger (FPLAN-0382)
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -28,7 +18,6 @@ from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
from aipass.cli.apps.modules import console
|
||||
|
||||
def _find_repo_root() -> Path:
|
||||
"""Walk up from this file to find AIPASS_REGISTRY.json (repo root)."""
|
||||
@@ -226,6 +215,7 @@ def truncate_text(text: str, max_length: int, suffix: str = "...") -> str:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aipass.cli.apps.modules import console
|
||||
console.print("\n" + "="*70)
|
||||
console.print("EMAIL FORMATTING HANDLER")
|
||||
console.print("="*70)
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: header.py
|
||||
# Description: Email Header Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-02-04
|
||||
# Modified: 2026-02-04
|
||||
# =============================================
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: header.py - Email Header Handler
|
||||
# Date: 2026-02-04
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.1.0 (2026-02-25): Add --no-memory-save conditional header for private branch dispatch
|
||||
# - v1.0.0 (2026-02-04): Created - dispatch header for critical reminders
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Can import Prax modules (service providers)
|
||||
# - Pure business logic only
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Email Header Handler
|
||||
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: inbox_cleanup.py - Inbox Cleanup Handler
|
||||
# Date: 2025-11-27
|
||||
# =================== AIPass ====================
|
||||
# Name: inbox_cleanup.py
|
||||
# Description: Inbox Cleanup Handler
|
||||
# Version: 3.3.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v3.3.0 (2026-02-25): FPLAN-0373 Phase 2 - use enriched push_dashboard_update from dashboard_sync
|
||||
# - v3.2.0 (2026-02-14): Add skip_post_ops param to mark_as_closed_and_archive for batch close perf
|
||||
# - v3.1.0 (2026-02-09): Add fcntl.flock inbox.json locking to prevent concurrent write corruption
|
||||
# - v3.0.0 (2026-02-04): Migrate to deleted/ directory (individual files like sent/)
|
||||
# - v2.1.0 (2026-02-04): Add auto-purge trigger after archiving to deleted
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Can import Prax modules (service providers)
|
||||
# - Can import AIPASS central services (dashboard)
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-27
|
||||
# Modified: 2025-11-27
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -49,9 +35,9 @@ def _get_inbox_lock():
|
||||
|
||||
|
||||
def _get_console() -> Any:
|
||||
"""Lazy import console."""
|
||||
from aipass.cli.apps.modules import console
|
||||
return console
|
||||
"""Lazy import console - only for __main__ block."""
|
||||
from rich.console import Console
|
||||
return Console()
|
||||
|
||||
|
||||
def _get_update_section() -> Any:
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: inbox_lock.py - Inbox File Lock Handler
|
||||
# Date: 2026-02-09
|
||||
# =================== AIPass ====================
|
||||
# Name: inbox_lock.py
|
||||
# Description: Inbox File Lock Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-09): Initial implementation - fcntl.flock based inbox.json locking
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Uses logging module for diagnostics
|
||||
# - Pure business logic only
|
||||
# Created: 2026-02-09
|
||||
# Modified: 2026-02-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: inbox_ops.py - Inbox Operations Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: inbox_ops.py
|
||||
# Description: Inbox Operations Handler
|
||||
# Version: 1.1.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.1.0 (2026-02-08): Add auto-migration for old inbox format {"inbox": []} → v2 schema
|
||||
# - v1.0.0 (2025-11-15): Extracted from email.py - inbox file I/O operations
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Can import Prax modules (service providers)
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -27,8 +17,6 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from aipass.cli.apps.modules import console
|
||||
|
||||
|
||||
|
||||
def load_inbox(inbox_file: Path) -> Dict:
|
||||
@@ -98,6 +86,7 @@ def load_inbox(inbox_file: Path) -> Dict:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aipass.cli.apps.modules import console
|
||||
console.print("\n" + "="*70)
|
||||
console.print("INBOX OPERATIONS HANDLER")
|
||||
console.print("="*70)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: inbox_resolve.py
|
||||
# Description: Inbox Resolution Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Inbox Resolution Handler
|
||||
|
||||
Resolves inbox paths and branch info for inbox viewing.
|
||||
Independent handler - no module or display dependencies.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Any, Callable, Tuple
|
||||
|
||||
|
||||
def resolve_inbox_target(
|
||||
args_first: Optional[str],
|
||||
repo_root: Path,
|
||||
get_branch_by_email_fn: Callable,
|
||||
get_current_user_fn: Callable,
|
||||
) -> Tuple[bool, Dict[str, Any]]:
|
||||
"""
|
||||
Resolve which inbox to display based on args.
|
||||
|
||||
Args:
|
||||
args_first: First argument (e.g., '@branch') or None
|
||||
repo_root: Repository root path
|
||||
get_branch_by_email_fn: Callable to look up branch by email
|
||||
get_current_user_fn: Callable to detect current user
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result_dict).
|
||||
result_dict contains:
|
||||
inbox_file: Path to inbox.json
|
||||
display_name: str for display
|
||||
target_branch: str | None (the explicit target, or None for current)
|
||||
error: str | None (set when success is False)
|
||||
"""
|
||||
target_branch = None
|
||||
if args_first and args_first.startswith("@"):
|
||||
target_branch = args_first
|
||||
|
||||
if target_branch:
|
||||
branch_info = get_branch_by_email_fn(target_branch)
|
||||
if not branch_info:
|
||||
return False, {"error": f"Unknown branch: {target_branch}"}
|
||||
|
||||
branch_path = Path(branch_info["path"])
|
||||
if not branch_path.is_absolute():
|
||||
branch_path = (repo_root / branch_path).resolve()
|
||||
mailbox_path = branch_path / ".ai_mail.local"
|
||||
display_name = branch_info.get("name", target_branch)
|
||||
else:
|
||||
user_info = get_current_user_fn()
|
||||
mailbox_path = Path(user_info["mailbox_path"])
|
||||
display_name = user_info.get("display_name", "")
|
||||
|
||||
inbox_file = mailbox_path / "inbox.json"
|
||||
|
||||
return True, {
|
||||
"inbox_file": inbox_file,
|
||||
"display_name": display_name,
|
||||
"target_branch": target_branch,
|
||||
"error": None,
|
||||
}
|
||||
@@ -1,19 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: lock_utils.py - Dispatch Lock Handler
|
||||
# Date: 2026-02-09
|
||||
# =================== AIPass ====================
|
||||
# Name: lock_utils.py
|
||||
# Description: Dispatch Lock Handler
|
||||
# Version: 1.1.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.1.0 (2026-02-10): Reduce stale lock timeout from 1800s to 600s (10 min)
|
||||
# - v1.0.0 (2026-02-09): Initial implementation - PID-based single instance lock per branch
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Uses logging module for diagnostics
|
||||
# - Pure business logic only
|
||||
# Created: 2026-02-09
|
||||
# Modified: 2026-02-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: purge.py - Sent/Deleted Auto-Purge Handler
|
||||
# Date: 2026-02-04
|
||||
# =================== AIPass ====================
|
||||
# Name: purge.py
|
||||
# Description: Sent/Deleted Auto-Purge Handler
|
||||
# Version: 2.0.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.0 (2026-02-04): Update deleted purge for deleted/ directory structure
|
||||
# - v1.0.0 (2026-02-04): Initial version - auto-purge sent/deleted folders
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Vectorization via subprocess (no direct Memory Bank imports)
|
||||
# - Uses Prax system_logger (FPLAN-0382)
|
||||
# Created: 2026-02-04
|
||||
# Modified: 2026-02-04
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: reply.py - Email Reply Handler
|
||||
# Date: 2025-11-30
|
||||
# =================== AIPass ====================
|
||||
# Name: reply.py
|
||||
# Description: Email Reply Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/email
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.2.0 (2026-01-31): Add reply chain validation - fail loud on identity mismatch
|
||||
# - v1.1.0 (2026-01-29): Add reply_to field support - replies go to reply_to address if set
|
||||
# - v1.0.0 (2025-11-30): Initial creation - reply to email + auto-close
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Can import Prax modules (service providers)
|
||||
# - Can import AIPASS central services (dashboard)
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-30
|
||||
# Modified: 2025-11-30
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: send.py
|
||||
# Description: Email Send Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Email Send Handler
|
||||
|
||||
Core send logic for email delivery workflows.
|
||||
Handles sender resolution, email creation, and delivery orchestration.
|
||||
Independent handler - no module or display dependencies.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, List, Dict, Any
|
||||
|
||||
from aipass.prax import logger
|
||||
# logger imported from aipass.prax
|
||||
|
||||
|
||||
def resolve_sender_info(
|
||||
from_branch: Optional[str],
|
||||
repo_root: Path,
|
||||
ai_mail_dir: Path,
|
||||
get_branch_by_email_fn,
|
||||
get_current_user_fn,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Resolve sender user_info from explicit branch or PWD detection.
|
||||
|
||||
Args:
|
||||
from_branch: Optional explicit sender branch (e.g., '@trigger').
|
||||
repo_root: Repository root path.
|
||||
ai_mail_dir: AI_Mail module directory.
|
||||
get_branch_by_email_fn: Callable to look up branch by email.
|
||||
get_current_user_fn: Callable to detect current user from PWD.
|
||||
|
||||
Returns:
|
||||
Dict with email_address, display_name, mailbox_path, timestamp_format.
|
||||
"""
|
||||
if from_branch:
|
||||
email_addr = f"@{from_branch.lstrip('@').lower()}"
|
||||
branch_info = get_branch_by_email_fn(email_addr)
|
||||
if branch_info:
|
||||
branch_path = Path(branch_info["path"])
|
||||
if not branch_path.is_absolute():
|
||||
branch_path = (repo_root / branch_path).resolve()
|
||||
return {
|
||||
"email_address": email_addr,
|
||||
"display_name": branch_info["name"],
|
||||
"mailbox_path": str(branch_path / ".ai_mail.local"),
|
||||
"timestamp_format": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
else:
|
||||
branch_name = from_branch.lstrip('@').upper()
|
||||
return {
|
||||
"email_address": email_addr,
|
||||
"display_name": branch_name,
|
||||
"mailbox_path": str(ai_mail_dir.parent / from_branch.lstrip('@').lower() / ".ai_mail.local"),
|
||||
"timestamp_format": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
else:
|
||||
return get_current_user_fn()
|
||||
|
||||
|
||||
def send_to_broadcast(
|
||||
subject: str,
|
||||
message: str,
|
||||
user_info: Dict[str, Any],
|
||||
auto_execute: bool,
|
||||
no_memory_save: bool,
|
||||
reply_to: Optional[str],
|
||||
dispatched_to: Optional[str],
|
||||
branches: List[Dict[str, Any]],
|
||||
create_email_file_fn,
|
||||
load_email_file_fn,
|
||||
deliver_email_to_branch_fn,
|
||||
on_delivered_callback,
|
||||
log_operation_fn,
|
||||
update_central_fn,
|
||||
) -> Tuple[bool, int, int, Optional[str]]:
|
||||
"""
|
||||
Execute broadcast send to all branches.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, success_count, total_count, error_msg).
|
||||
error_msg is set if the email file could not be loaded.
|
||||
"""
|
||||
email_file = create_email_file_fn("all", subject, message, user_info, reply_to=reply_to, dispatched_to=dispatched_to)
|
||||
email_data = load_email_file_fn(email_file)
|
||||
|
||||
if email_data is None:
|
||||
log_operation_fn("broadcast_failed", {"error": "Email file could not be loaded"})
|
||||
return False, 0, len(branches), "Email file could not be loaded"
|
||||
|
||||
results = [] # List of (branch_name, success, error_msg)
|
||||
for branch in branches:
|
||||
delivery_data = email_data.copy()
|
||||
delivery_data['to'] = branch['email']
|
||||
delivery_data['auto_execute'] = auto_execute
|
||||
if no_memory_save:
|
||||
delivery_data['no_memory_save'] = True
|
||||
|
||||
success, error_msg = deliver_email_to_branch_fn(branch['email'], delivery_data, on_delivered=on_delivered_callback)
|
||||
results.append((branch.get('name', branch['email']), success, error_msg))
|
||||
|
||||
success_count = sum(1 for _, s, _ in results if s)
|
||||
log_operation_fn("broadcast_sent", {"recipients": len(branches), "successful": success_count})
|
||||
|
||||
# Fire trigger event (best-effort)
|
||||
try:
|
||||
from aipass.trigger.apps.modules.core import trigger
|
||||
trigger.fire('email_broadcast_sent', recipients=len(branches), successful=success_count, subject=subject)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Update central (best-effort)
|
||||
try:
|
||||
if update_central_fn:
|
||||
update_central_fn()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return success_count > 0, success_count, len(branches), results
|
||||
|
||||
|
||||
def send_to_single(
|
||||
to_branch: str,
|
||||
subject: str,
|
||||
message: str,
|
||||
user_info: Dict[str, Any],
|
||||
auto_execute: bool,
|
||||
no_memory_save: bool,
|
||||
reply_to: Optional[str],
|
||||
dispatched_to: Optional[str],
|
||||
create_email_file_fn,
|
||||
load_email_file_fn,
|
||||
deliver_email_to_branch_fn,
|
||||
on_delivered_callback,
|
||||
log_operation_fn,
|
||||
update_central_fn,
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Execute single-recipient email send.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, error_msg). error_msg is None on success.
|
||||
"""
|
||||
email_file = create_email_file_fn(to_branch, subject, message, user_info, reply_to=reply_to, dispatched_to=dispatched_to)
|
||||
email_data = load_email_file_fn(email_file)
|
||||
|
||||
if email_data is None:
|
||||
log_operation_fn("email_failed", {"to": to_branch, "error": "Email file could not be loaded"})
|
||||
return False, "Email file could not be loaded"
|
||||
|
||||
email_data['auto_execute'] = auto_execute
|
||||
if dispatched_to:
|
||||
email_data['dispatched_to'] = dispatched_to
|
||||
if no_memory_save:
|
||||
email_data['no_memory_save'] = True
|
||||
|
||||
success, error_msg = deliver_email_to_branch_fn(to_branch, email_data, on_delivered=on_delivered_callback)
|
||||
|
||||
if success:
|
||||
log_operation_fn("email_sent", {"to": to_branch, "subject": subject, "auto_execute": auto_execute})
|
||||
|
||||
# Fire trigger event (best-effort)
|
||||
try:
|
||||
from aipass.trigger.apps.modules.core import trigger
|
||||
trigger.fire('email_sent', to=to_branch, subject=subject, auto_execute=auto_execute)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Update central (best-effort)
|
||||
try:
|
||||
if update_central_fn:
|
||||
update_central_fn()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return True, None
|
||||
else:
|
||||
log_operation_fn("email_failed", {"to": to_branch, "error": error_msg})
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def collect_interactive_input(branches: List[Dict[str, Any]]) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Collect send parameters from interactive user input.
|
||||
|
||||
Args:
|
||||
branches: List of available branch dicts with 'name' and 'email' keys.
|
||||
|
||||
Returns:
|
||||
Dict with 'to', 'subject', 'message' keys, or None if cancelled.
|
||||
"""
|
||||
try:
|
||||
selection = input(f"\nPick (1-{len(branches) + 1}): ").strip()
|
||||
idx = int(selection) - 1
|
||||
|
||||
if idx == len(branches):
|
||||
selected_email = "all"
|
||||
elif idx < 0 or idx >= len(branches):
|
||||
return None
|
||||
else:
|
||||
selected_email = branches[idx]["email"]
|
||||
except (ValueError, KeyboardInterrupt, EOFError):
|
||||
return None
|
||||
|
||||
try:
|
||||
subject = input("Subject: ").strip()
|
||||
if not subject:
|
||||
return None
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return None
|
||||
|
||||
try:
|
||||
message_lines = []
|
||||
while True:
|
||||
try:
|
||||
line = input()
|
||||
message_lines.append(line)
|
||||
except EOFError:
|
||||
break
|
||||
message = "\n".join(message_lines).strip()
|
||||
if not message:
|
||||
return None
|
||||
except KeyboardInterrupt:
|
||||
return None
|
||||
|
||||
try:
|
||||
confirm = input("\nSend? (y/n): ").strip().lower()
|
||||
if confirm != 'y':
|
||||
return None
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return None
|
||||
|
||||
return {
|
||||
"to": selected_email,
|
||||
"subject": subject,
|
||||
"message": message,
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: send_args.py
|
||||
# Description: Email Send Argument Parsing Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Email Send Argument Parsing Handler
|
||||
|
||||
Parses send command arguments into structured data.
|
||||
Independent handler - no module or display dependencies.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
|
||||
def parse_send_args(args: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse send command arguments into structured result.
|
||||
|
||||
Args:
|
||||
args: Raw argument list from CLI
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
auto_execute: bool
|
||||
no_memory_save: bool
|
||||
reply_to: str | None
|
||||
recipients: List[str]
|
||||
subject: str | None
|
||||
message: str | None
|
||||
mode: 'direct' | 'interactive' | 'error'
|
||||
error: str | None (set when mode=='error')
|
||||
"""
|
||||
working_args = list(args)
|
||||
|
||||
# Extract --dispatch / --auto-execute
|
||||
auto_execute = '--dispatch' in working_args or '--auto-execute' in working_args
|
||||
working_args = [a for a in working_args if a not in ('--dispatch', '--auto-execute')]
|
||||
|
||||
# Extract --no-memory-save
|
||||
no_memory_save = '--no-memory-save' in working_args
|
||||
working_args = [a for a in working_args if a != '--no-memory-save']
|
||||
|
||||
# Extract --reply-to
|
||||
reply_to = None
|
||||
if '--reply-to' in working_args:
|
||||
idx = working_args.index('--reply-to')
|
||||
if idx + 1 < len(working_args):
|
||||
reply_to = working_args[idx + 1]
|
||||
working_args = working_args[:idx] + working_args[idx + 2:]
|
||||
else:
|
||||
return {
|
||||
"auto_execute": auto_execute,
|
||||
"no_memory_save": no_memory_save,
|
||||
"reply_to": None,
|
||||
"recipients": [],
|
||||
"subject": None,
|
||||
"message": None,
|
||||
"mode": "error",
|
||||
"error": "--reply-to requires a branch address (e.g., --reply-to @dev_central)",
|
||||
}
|
||||
|
||||
# Separate recipients from subject/message
|
||||
recipients = []
|
||||
rest = []
|
||||
for a in working_args:
|
||||
if a.startswith('@') and not rest:
|
||||
recipients.append(a)
|
||||
elif a.startswith('/') and not rest:
|
||||
recipients.append(a)
|
||||
else:
|
||||
rest.append(a)
|
||||
|
||||
# Determine mode
|
||||
if recipients and len(rest) >= 2:
|
||||
mode = "direct"
|
||||
subject = rest[0]
|
||||
message = rest[1]
|
||||
elif not recipients and not rest:
|
||||
mode = "interactive"
|
||||
subject = None
|
||||
message = None
|
||||
else:
|
||||
mode = "error"
|
||||
subject = rest[0] if rest else None
|
||||
message = rest[1] if len(rest) >= 2 else None
|
||||
|
||||
return {
|
||||
"auto_execute": auto_execute,
|
||||
"no_memory_save": no_memory_save,
|
||||
"reply_to": reply_to,
|
||||
"recipients": recipients,
|
||||
"subject": subject,
|
||||
"message": message,
|
||||
"mode": mode,
|
||||
"error": None if mode != "error" else "Usage: send @recipient [subject] [message]",
|
||||
}
|
||||
|
||||
|
||||
def resolve_dispatch_target(
|
||||
branch: str,
|
||||
auto_execute: bool,
|
||||
get_branch_info_fn=None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Resolve dispatch target address for a recipient.
|
||||
|
||||
Args:
|
||||
branch: Recipient address (e.g., '@flow' or '/path/to/branch')
|
||||
auto_execute: Whether dispatch was requested
|
||||
get_branch_info_fn: Callable to look up branch info from registry by path
|
||||
|
||||
Returns:
|
||||
Dispatch target address string, or None if no dispatch.
|
||||
"""
|
||||
if not auto_execute:
|
||||
return None
|
||||
|
||||
if branch.startswith('/') or branch.startswith('~'):
|
||||
if get_branch_info_fn:
|
||||
branch_info = get_branch_info_fn(Path(branch))
|
||||
if branch_info:
|
||||
return branch_info.get("email", f"@{Path(branch).name.lower()}")
|
||||
return f"@{Path(branch).name.lower()}"
|
||||
|
||||
return branch
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: json_handler.py - JSON Handler (Canonical Path)
|
||||
# Date: 2026-02-28
|
||||
# =================== AIPass ====================
|
||||
# Name: json_handler.py
|
||||
# Description: JSON Handler (Canonical Path)
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/json
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-28): Re-export from json_utils for Seed architecture compliance
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Re-exports json_handler from json_utils/ (canonical implementation)
|
||||
# - Satisfies Seed architecture standard for apps/handlers/json/ path
|
||||
# Created: 2026-02-28
|
||||
# Modified: 2026-02-28
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: json_handler.py - JSON Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: json_handler.py
|
||||
# Description: JSON Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/json_utils
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Initial creation - auto-creating self-healing JSON
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - BYPASS: Direct json.load/json.dump required - this IS the json handler
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: data_ops.py - Error Monitor Data Operations Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: data_ops.py
|
||||
# Description: Error Monitor Data Operations Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/monitoring
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Extracted from error_monitor.py
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: errors.py - Error Detection Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: errors.py
|
||||
# Description: Error Detection Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/monitoring
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Extracted from ai_mail_error_monitor.py
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -34,8 +26,6 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Tuple
|
||||
|
||||
from aipass.cli.apps.modules import console
|
||||
|
||||
|
||||
def _find_repo_root() -> Path:
|
||||
"""Walk up from this file to find AIPASS_REGISTRY.json (repo root)."""
|
||||
@@ -251,6 +241,7 @@ def validate_error_data_entry(error_info: Dict) -> bool:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aipass.cli.apps.modules import console
|
||||
console.print("\n" + "="*70)
|
||||
console.print("ERROR DETECTION HANDLER")
|
||||
console.print("="*70)
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: memory.py - Memory Health Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: memory.py
|
||||
# Description: Memory Health Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/monitoring
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Extracted from ai_mail_local_memory_monitor.py
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -31,7 +23,6 @@ Architecture:
|
||||
# =============================================
|
||||
from pathlib import Path
|
||||
|
||||
from aipass.cli.apps.modules import console
|
||||
|
||||
# =============================================
|
||||
# CONSTANTS
|
||||
@@ -189,6 +180,7 @@ def validate_thresholds(green_max: int, yellow_min: int, yellow_max: int, red_mi
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aipass.cli.apps.modules import console
|
||||
console.print("\n" + "="*70)
|
||||
console.print("MEMORY HEALTH HANDLER")
|
||||
console.print("="*70)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: notify.py
|
||||
# Description: Desktop Notification Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Desktop Notification Handler
|
||||
|
||||
Sends persistent, stacking desktop notifications via D-Bus.
|
||||
GNOME's Portal mode strips persistence hints from notify-send,
|
||||
so we use the dbus module directly with unique app names per
|
||||
notification source to ensure they stack in the notification center.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
def send_notification(title: str, body: str, source: str = "ai_mail",
|
||||
icon: str = "dialog-information") -> bool:
|
||||
"""Send a persistent desktop notification.
|
||||
|
||||
Args:
|
||||
title: Notification title (e.g. "Email from @spawn")
|
||||
body: Notification body text
|
||||
source: Branch/context name used as app identity for stacking.
|
||||
Each unique source gets its own slot in the notification center.
|
||||
icon: Icon name (dialog-information, dialog-warning, etc.)
|
||||
|
||||
Returns:
|
||||
True if sent, False on failure
|
||||
"""
|
||||
# Primary: dbus direct (bypasses Portal, supports stacking)
|
||||
if _send_via_dbus(title, body, source, icon):
|
||||
return True
|
||||
|
||||
# Fallback: notify-send (works on non-GNOME, macOS via homebrew, etc.)
|
||||
return _send_via_notify_send(title, body, icon)
|
||||
|
||||
|
||||
def _send_via_dbus(title: str, body: str, source: str,
|
||||
icon: str) -> bool:
|
||||
"""Send notification via D-Bus using system python."""
|
||||
try:
|
||||
# Use system python which has dbus module (venv python may not)
|
||||
result = subprocess.run(
|
||||
["/usr/bin/python3", "-c", _DBUS_SCRIPT,
|
||||
source, icon, title, body],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def _send_via_notify_send(title: str, body: str, icon: str) -> bool:
|
||||
"""Fallback: send via notify-send."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["notify-send", "-i", icon, title, body],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
return True
|
||||
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
# Inline script executed by system python (which has dbus module).
|
||||
# Kept as a string to avoid importing dbus in the venv.
|
||||
_DBUS_SCRIPT = """\
|
||||
import sys, dbus
|
||||
source, icon, title, body = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
||||
bus = dbus.SessionBus()
|
||||
proxy = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
|
||||
iface = dbus.Interface(proxy, 'org.freedesktop.Notifications')
|
||||
iface.Notify(source, 0, icon, title, body, [], {'urgency': dbus.Byte(1)}, -1)
|
||||
"""
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: load.py - Registry Loading Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: load.py
|
||||
# Description: Registry Loading Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/registry
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Initial handler - extracted from local_memory_monitor module
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Business logic implementation
|
||||
# - Pure functions where possible
|
||||
# - Clear error handling
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: read.py - Registry Read Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: read.py
|
||||
# Description: Registry Read Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/registry
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Extracted from ai_mail_cli.py
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers are INDEPENDENT (no cross-domain imports)
|
||||
# - Use: from prax.apps.modules.logger import system_logger as logger
|
||||
# - Keep handlers <300 lines each
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -33,7 +24,6 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
|
||||
from aipass.cli.apps.modules import console
|
||||
|
||||
# Constants
|
||||
MODULE_NAME = "registry.read"
|
||||
@@ -172,6 +162,7 @@ def get_branch_path_map() -> Dict[str, str]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aipass.cli.apps.modules import console
|
||||
console.print("\n" + "="*70)
|
||||
console.print("AI_MAIL HANDLER: registry/read.py")
|
||||
console.print("="*70)
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: update.py - Registry Update Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: update.py
|
||||
# Description: Registry Update Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/registry
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Extracted from ai_mail_branch_ping.py
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers are INDEPENDENT (no cross-domain imports)
|
||||
# - Use: from prax.apps.modules.logger import system_logger as logger
|
||||
# - Keep handlers <300 lines each
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -34,7 +25,6 @@ from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from aipass.cli.apps.modules import console
|
||||
|
||||
# Constants
|
||||
MODULE_NAME = "registry.update"
|
||||
@@ -247,6 +237,7 @@ def get_branch_context() -> Tuple[str, Path]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aipass.cli.apps.modules import console
|
||||
console.print("\n" + "="*70)
|
||||
console.print("AI_MAIL HANDLER: registry/update.py")
|
||||
console.print("="*70)
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: validate.py - Registry Validation Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: validate.py
|
||||
# Description: Registry Validation Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/registry
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Extracted from ai_mail_cli.py
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers are INDEPENDENT (no cross-domain imports)
|
||||
# - Use: from prax.apps.modules.logger import system_logger as logger
|
||||
# - Keep handlers <300 lines each
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -32,7 +23,6 @@ Handler Independence:
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
from aipass.cli.apps.modules import console
|
||||
|
||||
# Constants
|
||||
MODULE_NAME = "registry.validate"
|
||||
@@ -216,6 +206,7 @@ def get_duplicate_paths(branches: List[Dict]) -> List[str]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aipass.cli.apps.modules import console
|
||||
console.print("\n" + "="*70)
|
||||
console.print("AI_MAIL HANDLER: registry/validate.py")
|
||||
console.print("="*70)
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: error_handler.py - Error Detected Event Consumer
|
||||
# Date: 2026-02-02
|
||||
# =================== AIPass ====================
|
||||
# Name: error_handler.py
|
||||
# Description: Error Detected Event Consumer
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/trigger
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-02): Created - FPLAN-0284 Phase 2
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follows AIPass Seed standards
|
||||
# - NO console.print() - handlers return data to modules
|
||||
# - NO logger calls in handler (causes recursion with trigger)
|
||||
# - Silent failure pattern - catch all exceptions
|
||||
# - Responds to error_detected events from Trigger's log_watcher
|
||||
# Created: 2026-02-02
|
||||
# Modified: 2026-02-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# =============================================
|
||||
# META DATA HEADER
|
||||
# Name: branch_detection.py - Branch Auto-Detection Handler
|
||||
# Date: 2025-11-18
|
||||
# =================== AIPass ====================
|
||||
# Name: branch_detection.py
|
||||
# Description: Branch Auto-Detection Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/users
|
||||
#
|
||||
# CHANGELOG:
|
||||
# - v1.0.0 (2025-11-18): Initial creation - PWD/CWD branch detection
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Can import Prax modules (service providers)
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-18
|
||||
# Modified: 2025-11-18
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# =============================================
|
||||
# META DATA HEADER
|
||||
# Name: config_generator.py - Local Config Auto-Generation Handler
|
||||
# Date: 2025-11-18
|
||||
# =================== AIPass ====================
|
||||
# Name: config_generator.py
|
||||
# Description: Local Config Auto-Generation Handler
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/handlers/users
|
||||
#
|
||||
# CHANGELOG:
|
||||
# - v1.0.0 (2025-11-18): Initial creation - auto-generate branch configs
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler independence: NO cross-domain imports
|
||||
# - Can import Prax modules (service providers)
|
||||
# - Pure business logic only
|
||||
# Created: 2025-11-18
|
||||
# Modified: 2025-11-18
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
|
||||
# =============================================
|
||||
# META DATA HEADER
|
||||
# Name: load.py - User Config Loading Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: load.py
|
||||
# Description: User Config Loading Handler
|
||||
# Version: 1.2.0
|
||||
# Category: ai_mail/handlers/users
|
||||
#
|
||||
# CHANGELOG:
|
||||
# - v1.2.0 (2025-11-18): Added per-branch config support with PWD detection
|
||||
# - v1.1.0 (2025-11-15): Renamed domain config -> users for business purpose naming
|
||||
# - v1.0.0 (2025-11-15): Extracted from ai_mail_cli.py and ai_mail_local_memory_monitor.py
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
|
||||
# =============================================
|
||||
# META DATA HEADER
|
||||
# Name: user.py - User Info Handler
|
||||
# Date: 2025-11-30
|
||||
# =================== AIPass ====================
|
||||
# Name: user.py
|
||||
# Description: User Info Handler
|
||||
# Version: 2.0.0
|
||||
# Category: ai_mail/handlers/users
|
||||
#
|
||||
# CHANGELOG:
|
||||
# - v2.0.0 (2025-11-30): Removed all fallbacks - fail hard if branch detection fails
|
||||
# - v1.1.0 (2025-11-15): Renamed domain config -> users for business purpose naming
|
||||
# - v1.0.0 (2025-11-15): Extracted from ai_mail_cli.py
|
||||
# Created: 2025-11-30
|
||||
# Modified: 2025-11-30
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: branch_ping.py - Branch Ping Orchestration Module
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: branch_ping.py
|
||||
# Description: Branch Ping Orchestration Module
|
||||
# Version: 1.0.0
|
||||
# Category: ai_mail/modules
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Initial orchestration module for branch ping
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Modules ORCHESTRATE (no business logic)
|
||||
# - Delegate to handlers for all operations
|
||||
# - Use json_handler.log_operation()
|
||||
# - Keep modules 110-155 lines
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -168,7 +158,7 @@ EXAMPLES:
|
||||
drone ai_mail branch_ping thresholds
|
||||
"""
|
||||
)
|
||||
parser.print_help()
|
||||
console.print(parser.format_help())
|
||||
|
||||
|
||||
def handle_command(command: str, args: List[str]) -> bool:
|
||||
@@ -193,6 +183,26 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def print_introspection():
|
||||
"""Display module introspection info."""
|
||||
console.print()
|
||||
console.print("branch_ping Module")
|
||||
console.print("Orchestrates branch memory health monitoring: ping, status, registry, and thresholds.")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/monitoring/")
|
||||
console.print(" - memory.py (count_file_lines — count lines in a memory file)")
|
||||
console.print(" - memory.py (get_status_from_count — derive health status from line count)")
|
||||
console.print(" handlers/registry/")
|
||||
console.print(" - update.py (ping_registry — update memory health registry for a branch)")
|
||||
console.print(" - update.py (get_branch_context — resolve current branch name and directory)")
|
||||
console.print(" - update.py (update_json_memory_health — write health metadata into memory file)")
|
||||
console.print(" - load.py (load_registry — load the memory health registry file)")
|
||||
console.print(" handlers/json_utils/")
|
||||
console.print(" - json_handler.py (log_operation — log structured operation to JSON)")
|
||||
console.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Handle --help flag
|
||||
if len(sys.argv) > 1 and sys.argv[1] in ['--help', '-h', 'help']:
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: dispatch.py - Dispatch Module
|
||||
# Date: 2026-02-02
|
||||
# =================== AIPass ====================
|
||||
# Name: dispatch.py
|
||||
# Description: Dispatch Module
|
||||
# Version: 3.0.0
|
||||
# Category: ai_mail/modules
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v3.0.0 (2026-02-20): Add wake subcommand - manual branch spawn without daemon
|
||||
# - v2.0.0 (2026-02-17): Add daemon subcommand, move status logic to handler
|
||||
# - v1.0.0 (2026-02-02): Initial version - dispatch status tracking
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Orchestration only - delegates to handlers
|
||||
# - Uses json_handler.log_operation()
|
||||
# Created: 2026-02-02
|
||||
# Modified: 2026-02-02
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -215,6 +205,22 @@ def _orchestrate_daemon() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def print_introspection():
|
||||
"""Display module introspection info."""
|
||||
console.print()
|
||||
console.print("dispatch Module")
|
||||
console.print("Orchestrates dispatch commands: status tracking, daemon management, and manual branch wake.")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/dispatch/")
|
||||
console.print(" - status.py (load_dispatch_log — load dispatch log entries)")
|
||||
console.print(" - status.py (check_pid_status — check if a spawned process is still running)")
|
||||
console.print(" - status.py (calculate_age — calculate age string from timestamp)")
|
||||
console.print(" - wake.py (wake_branch — manually wake a branch by spawning an agent)")
|
||||
console.print(" - daemon.py (run_daemon — start the continuous dispatch daemon)")
|
||||
console.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) == 1:
|
||||
print_help()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
|
||||
**Purpose:** LLM API access layer with provider abstraction, key management, model routing, and usage tracking.
|
||||
**Module:** `aipass.api`
|
||||
**Last Updated:** 2026-03-08
|
||||
|
||||
---
|
||||
|
||||
@@ -20,7 +21,7 @@
|
||||
|
||||
---
|
||||
|
||||
## CLI
|
||||
## Commands / Usage
|
||||
|
||||
```bash
|
||||
drone @api get-key # Retrieve API key for provider
|
||||
@@ -30,8 +31,11 @@ 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
|
||||
drone @api --version # Show version
|
||||
```
|
||||
|
||||
Running `drone @api` with no arguments displays module introspection (discovered modules and status).
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
@@ -76,3 +80,7 @@ api/
|
||||
### Provides To
|
||||
- All modules -- LLM API access for any branch that needs model inference
|
||||
- System-wide API key management and credential validation
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2026-03-08*
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: main.py - api Branch Orchestrator
|
||||
# Date: 2025-11-08
|
||||
# =================== AIPass ====================
|
||||
# Name: api.py
|
||||
# Description: Entry point CLI for drone @api — LLM client via OpenRouter
|
||||
# Version: 1.0.0
|
||||
# Category: api/entry_point
|
||||
# CODE STANDARDS: Seed v1.0.0
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-08): Initial version - modular architecture
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: env.py - .env file operations
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: env.py
|
||||
# Description: .env file operations
|
||||
# Version: 0.2.0
|
||||
# Category: api/handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v0.2.0 (2025-11-16): Extracted from api_connect.py - complete implementation
|
||||
# - v0.1.0 (2025-11-15): Initial handler stub
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler layer (standalone functions)
|
||||
# - Uses CLI service for output
|
||||
# - Under 300 lines
|
||||
# ==============================================
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2025-11-16
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
.env File Handler
|
||||
@@ -36,8 +26,8 @@ import sys
|
||||
# Standard library
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
# CLI services
|
||||
from aipass.cli.apps.modules import console
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
|
||||
# ==============================================
|
||||
@@ -172,7 +162,7 @@ def create_env_template(provider: str = "openrouter", target_path: Optional[Path
|
||||
# Don't overwrite existing file
|
||||
if env_path.exists():
|
||||
# .env file already exists
|
||||
console.print(f"[yellow]ℹ[/yellow] .env file already exists at {env_path}")
|
||||
logger.info(f".env file already exists at {env_path}")
|
||||
return True
|
||||
|
||||
# Template content based on provider
|
||||
@@ -208,12 +198,12 @@ OPENAI_API_KEY=sk-your-openai-key-here
|
||||
f.write(env_template)
|
||||
|
||||
# Created .env template
|
||||
console.print(f"[green]✓[/green] Created .env template at {env_path}")
|
||||
logger.info(f"Created .env template at {env_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# Failed to create .env template
|
||||
console.print(f"[red]✗[/red] Failed to create .env template: {e}")
|
||||
logger.error(f"Failed to create .env template: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -267,12 +257,12 @@ def create_custom_env_template(variables: Dict[str, str], target_path: Path,
|
||||
f.write(content)
|
||||
|
||||
# Created custom .env template
|
||||
console.print(f"[green]✓[/green] Created custom .env template at {target_path}")
|
||||
logger.info(f"Created custom .env template at {target_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# Failed to create custom .env template
|
||||
console.print(f"[red]✗[/red] Failed to create custom .env template: {e}")
|
||||
logger.error(f"Failed to create custom .env template: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: keys.py - API Key Management Handler
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: keys.py
|
||||
# Description: API Key Management Handler
|
||||
# Version: 2.0.0
|
||||
# Category: api/handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.0 (2025-11-16): Extracted from api_connect.py - complete implementation
|
||||
# - v1.0.0 (2025-11-15): Initial handler stub
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler layer (standalone functions)
|
||||
# - Uses CLI service for output
|
||||
# - Uses prax logger for system logs
|
||||
# - Under 300 lines
|
||||
# ==============================================
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2025-11-16
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
API Key Management Handler
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: provider.py - Provider Configuration Handler
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: provider.py
|
||||
# Description: Provider Configuration Handler
|
||||
# Version: 2.0.0
|
||||
# Category: api/handlers/config
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.0 (2025-11-16): Complete extraction from api_connect.py
|
||||
# - v1.0.0 (2025-11-15): Initial handler stub
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2025-11-16
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -36,16 +31,8 @@ from typing import Dict, Any, Optional
|
||||
# Internal handlers
|
||||
from aipass.api.apps.handlers.json.json_handler import load_json, save_json
|
||||
|
||||
# Console for user feedback
|
||||
try:
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
except ImportError:
|
||||
# Fallback console if rich not available
|
||||
class SimpleConsole:
|
||||
def print(self, *args, **kwargs):
|
||||
print(*args)
|
||||
console = SimpleConsole()
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
# =============================================
|
||||
# CONSTANTS
|
||||
@@ -223,12 +210,12 @@ def update_provider_config(provider: str, updates: Dict[str, Any]) -> bool:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Updated config for provider
|
||||
console.print(f"[green]✓[/green] Provider config updated: {provider}")
|
||||
logger.info(f"Provider config updated: {provider}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# Failed to update provider config
|
||||
console.print(f"[red]✗[/red] Failed to update provider config: {e}")
|
||||
logger.error(f"Failed to update provider config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -345,7 +332,7 @@ def _create_default_config() -> bool:
|
||||
json.dump(default_config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Created default config
|
||||
console.print(f"[green]✓[/green] Created default config: {config_path}")
|
||||
logger.info(f"Created default config: {config_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: json_handler.py - JSON Auto-Creating Handler
|
||||
# Date: 2025-11-21
|
||||
# =================== AIPass ====================
|
||||
# Name: json_handler.py
|
||||
# Description: JSON Auto-Creating Handler
|
||||
# Version: 1.1.0
|
||||
# Category: api/handlers/json
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.1.0 (2025-11-21): Refactored to comply with error handling
|
||||
# - v1.0.0 (2025-11-13): Initial JSON auto-creation system
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Pure functions with proper error raising
|
||||
# - No Prax imports (handler tier 3)
|
||||
# Created: 2025-11-21
|
||||
# Modified: 2025-11-21
|
||||
# =============================================
|
||||
|
||||
import json
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
|
||||
# =============================================
|
||||
# META DATA HEADER
|
||||
# =================== AIPass ====================
|
||||
# Name: caller.py
|
||||
# Date: 2025-11-16
|
||||
# Description: OpenRouter Caller Detection Handler
|
||||
# Version: 1.0.0
|
||||
# Category: api/handlers
|
||||
#
|
||||
# CHANGELOG:
|
||||
# - v1.0.0 (2025-11-16): Initial caller detection handler extracted from archive
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2025-11-16
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: client.py - OpenRouter Client Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: client.py
|
||||
# Description: OpenRouter Client Handler
|
||||
# Version: 3.0.0
|
||||
# Category: api/handlers/openrouter
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v3.0.0 (2026-02-20): Fallback model chain in get_response() + retry logic in make_api_request()
|
||||
# - v2.0.0 (2025-11-16): Complete extraction from archive - client creation, API requests, response handling
|
||||
# - v1.0.0 (2025-11-15): Initial handler stub
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -31,7 +25,7 @@ Configuration:
|
||||
- Connection pooling via client caching
|
||||
|
||||
Standards:
|
||||
- Uses console.print() for user output (NO print())
|
||||
- Uses prax logger for output (NO print() or console.print())
|
||||
- Uses logger.info() for system logging
|
||||
- Integrates with auth/keys, caller detection, usage tracking handlers
|
||||
- Standalone functions (no classes)
|
||||
@@ -46,8 +40,8 @@ from pathlib import Path
|
||||
import time
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
# AIPASS imports
|
||||
from aipass.cli.apps.modules import console
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
# OpenAI SDK for OpenRouter compatibility
|
||||
try:
|
||||
@@ -104,12 +98,12 @@ def create_client(api_key: str, base_url: str = OPENROUTER_BASE_URL, timeout: in
|
||||
"""
|
||||
if not OPENAI_AVAILABLE:
|
||||
# logger.error("OpenAI SDK not installed - cannot create client")
|
||||
console.print("[red]Error: OpenAI SDK not installed. Run: pip install openai[/red]")
|
||||
logger.error("OpenAI SDK not installed. Run: pip install openai")
|
||||
return None
|
||||
|
||||
if not api_key:
|
||||
# logger.error("Cannot create client - no API key provided")
|
||||
console.print("[red]Error: API key required for client creation[/red]")
|
||||
logger.error("API key required for client creation")
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -126,7 +120,7 @@ def create_client(api_key: str, base_url: str = OPENROUTER_BASE_URL, timeout: in
|
||||
|
||||
except Exception as e:
|
||||
# logger.error(f"Failed to create OpenRouter client: {e}")
|
||||
console.print(f"[red]Error creating OpenRouter client: {e}[/red]")
|
||||
logger.error(f"Error creating OpenRouter client: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -208,16 +202,16 @@ def make_api_request(client: OpenAI, messages: List[Dict], model: str, retries:
|
||||
try:
|
||||
response = client.chat.completions.create(**api_params)
|
||||
if attempt > 0:
|
||||
print(f"[INFO] API request succeeded on retry {attempt} for model {model}")
|
||||
logger.info(f"API request succeeded on retry {attempt} for model {model}")
|
||||
return response
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt < retries:
|
||||
delay = 1.0 * (attempt + 1) # 1s, 2s, ...
|
||||
print(f"[INFO] API request failed for {model} (attempt {attempt + 1}/{1 + retries}): {e} — retrying in {delay:.0f}s")
|
||||
logger.info(f"API request failed for {model} (attempt {attempt + 1}/{1 + retries}): {e} — retrying in {delay:.0f}s")
|
||||
time.sleep(delay)
|
||||
|
||||
print(f"[INFO] API request failed for {model} after {1 + retries} attempts: {last_error}")
|
||||
logger.error(f"API request failed for {model} after {1 + retries} attempts: {last_error}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -318,15 +312,15 @@ def get_response(prompt: str, caller: Optional[str] = None, model: Optional[str]
|
||||
# Step 2: Require model from caller - no defaults
|
||||
if not model:
|
||||
# logger.error("No model specified - caller must provide model from their branch config")
|
||||
console.print("[red]Error: No model specified.[/red]")
|
||||
console.print("[yellow]Callers must provide their own model via branch config (e.g., flow_json/openrouter_config.json)[/yellow]")
|
||||
logger.error("No model specified.")
|
||||
logger.warning("Callers must provide their own model via branch config (e.g., flow_json/openrouter_config.json)")
|
||||
return None
|
||||
|
||||
# Step 3: Get API key
|
||||
api_key = get_api_key("openrouter")
|
||||
if not api_key:
|
||||
# logger.error("Cannot get response - no API key available")
|
||||
console.print("[red]Error: No OpenRouter API key available[/red]")
|
||||
logger.error("No OpenRouter API key available")
|
||||
return None
|
||||
|
||||
# Step 4: Get or create client
|
||||
@@ -376,7 +370,7 @@ def clear_client_cache() -> None:
|
||||
count = len(_client_cache)
|
||||
_client_cache.clear()
|
||||
# logger.info(f"Cleared {count} cached clients")
|
||||
console.print(f"[green]Cleared {count} cached OpenRouter clients[/green]")
|
||||
logger.info(f"Cleared {count} cached OpenRouter clients")
|
||||
|
||||
|
||||
def get_cache_stats() -> Dict[str, Any]:
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: models.py - OpenRouter Model Management
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: models.py
|
||||
# Description: OpenRouter Model Management
|
||||
# Version: 1.0.0
|
||||
# Category: api/handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-16): Complete handler - model fetching, filtering, pricing
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2025-11-16
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -29,8 +25,8 @@ from pathlib import Path
|
||||
# Standard library imports
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# Console import (NO print())
|
||||
from aipass.cli.apps.modules import console
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
# Third-party imports
|
||||
import requests
|
||||
@@ -67,7 +63,7 @@ def get_available_models(api_key: Optional[str] = None) -> List[Dict]:
|
||||
|
||||
Example:
|
||||
>>> models = get_available_models()
|
||||
>>> console.print(f"Found {len(models)} models")
|
||||
>>> logger.info(f"Found {len(models)} models")
|
||||
"""
|
||||
try:
|
||||
# Get API key if not provided
|
||||
@@ -76,7 +72,7 @@ def get_available_models(api_key: Optional[str] = None) -> List[Dict]:
|
||||
|
||||
if not api_key:
|
||||
# logger.info(f"[{MODULE_NAME}] No API key available for OpenRouter")
|
||||
console.print("[yellow]No OpenRouter API key found[/yellow]")
|
||||
logger.warning("No OpenRouter API key found")
|
||||
return []
|
||||
|
||||
# Fetch models from API
|
||||
@@ -91,7 +87,7 @@ def get_available_models(api_key: Optional[str] = None) -> List[Dict]:
|
||||
|
||||
except Exception as e:
|
||||
# logger.info(f"[{MODULE_NAME}] Failed to get available models: {e}")
|
||||
console.print(f"[red]Error fetching models: {e}[/red]")
|
||||
logger.error(f"Error fetching models: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@@ -111,7 +107,7 @@ def get_free_models(api_key: Optional[str] = None) -> List[Dict]:
|
||||
Example:
|
||||
>>> free_models = get_free_models()
|
||||
>>> for model in free_models:
|
||||
... console.print(f"Free: {model['id']}")
|
||||
... logger.info(f"Free: {model['id']}")
|
||||
"""
|
||||
try:
|
||||
# Get all models first
|
||||
@@ -128,7 +124,7 @@ def get_free_models(api_key: Optional[str] = None) -> List[Dict]:
|
||||
|
||||
except Exception as e:
|
||||
# logger.info(f"[{MODULE_NAME}] Failed to get free models: {e}")
|
||||
console.print(f"[red]Error fetching free models: {e}[/red]")
|
||||
logger.error(f"Error fetching free models: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@@ -166,7 +162,7 @@ def fetch_models_from_api(api_key: str) -> List[Dict]:
|
||||
# Check response status
|
||||
if response.status_code != 200:
|
||||
# logger.info(f"[{MODULE_NAME}] API request failed with status {response.status_code}")
|
||||
console.print(f"[red]OpenRouter API error: {response.status_code}[/red]")
|
||||
logger.error(f"OpenRouter API error: {response.status_code}")
|
||||
return []
|
||||
|
||||
# Parse JSON response
|
||||
@@ -183,22 +179,22 @@ def fetch_models_from_api(api_key: str) -> List[Dict]:
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
# logger.info(f"[{MODULE_NAME}] API request timeout after {DEFAULT_TIMEOUT}s")
|
||||
console.print(f"[red]Request timeout - OpenRouter API not responding[/red]")
|
||||
logger.error("Request timeout - OpenRouter API not responding")
|
||||
return []
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
# logger.info(f"[{MODULE_NAME}] Network error: {e}")
|
||||
console.print(f"[red]Network error: {e}[/red]")
|
||||
logger.error(f"Network error: {e}")
|
||||
return []
|
||||
|
||||
except ValueError as e:
|
||||
# logger.info(f"[{MODULE_NAME}] JSON parse error: {e}")
|
||||
console.print(f"[red]Invalid JSON response from API[/red]")
|
||||
logger.error("Invalid JSON response from API")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
# logger.info(f"[{MODULE_NAME}] Unexpected error fetching models: {e}")
|
||||
console.print(f"[red]Error: {e}[/red]")
|
||||
logger.error(f"Error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@@ -291,7 +287,7 @@ def extract_model_metadata(model: Dict) -> Dict:
|
||||
Example:
|
||||
>>> model = get_model_by_id("meta-llama/llama-3.3-70b-instruct:free")
|
||||
>>> meta = extract_model_metadata(model)
|
||||
>>> console.print(f"Context: {meta['context_length']}")
|
||||
>>> logger.info(f"Context: {meta['context_length']}")
|
||||
"""
|
||||
try:
|
||||
pricing = model.get("pricing", {})
|
||||
@@ -341,27 +337,27 @@ def list_model_ids(models: List[Dict]) -> List[str]:
|
||||
|
||||
def display_models(models: List[Dict], show_pricing: bool = True) -> None:
|
||||
"""
|
||||
Display models in formatted output using console
|
||||
Display models in formatted output using logger
|
||||
|
||||
Args:
|
||||
models: List of model dictionaries to display
|
||||
show_pricing: Whether to show pricing information
|
||||
"""
|
||||
if not models:
|
||||
console.print("[yellow]No models to display[/yellow]")
|
||||
logger.info("No models to display")
|
||||
return
|
||||
|
||||
console.print(f"\n[bold]Found {len(models)} models:[/bold]\n")
|
||||
logger.info(f"Found {len(models)} models:")
|
||||
|
||||
for i, model in enumerate(models, 1):
|
||||
model_id = model.get("id", "unknown")
|
||||
name = model.get("name", "")
|
||||
context = model.get("context_length", 0)
|
||||
|
||||
console.print(f"[cyan]{i}. {model_id}[/cyan]")
|
||||
logger.info(f"{i}. {model_id}")
|
||||
if name:
|
||||
console.print(f" Name: {name}")
|
||||
console.print(f" Context: {context:,} tokens")
|
||||
logger.info(f" Name: {name}")
|
||||
logger.info(f" Context: {context:,} tokens")
|
||||
|
||||
if show_pricing:
|
||||
pricing = model.get("pricing", {})
|
||||
@@ -369,11 +365,9 @@ def display_models(models: List[Dict], show_pricing: bool = True) -> None:
|
||||
completion_cost = pricing.get("completion", "0")
|
||||
|
||||
if prompt_cost == "0" and completion_cost == "0":
|
||||
console.print(" [green]FREE[/green]")
|
||||
logger.info(" FREE")
|
||||
else:
|
||||
console.print(f" Prompt: ${prompt_cost} / Completion: ${completion_cost}")
|
||||
|
||||
console.print()
|
||||
logger.info(f" Prompt: ${prompt_cost} / Completion: ${completion_cost}")
|
||||
|
||||
|
||||
def display_free_models_summary(api_key: Optional[str] = None) -> None:
|
||||
@@ -386,20 +380,20 @@ def display_free_models_summary(api_key: Optional[str] = None) -> None:
|
||||
Args:
|
||||
api_key: Optional OpenRouter API key
|
||||
"""
|
||||
console.print("[bold]Searching for FREE models on OpenRouter...[/bold]")
|
||||
console.print("=" * 60)
|
||||
logger.info("Searching for FREE models on OpenRouter...")
|
||||
logger.info("=" * 60)
|
||||
|
||||
free_models = get_free_models(api_key)
|
||||
|
||||
if not free_models:
|
||||
console.print("[yellow]No free models found[/yellow]")
|
||||
console.print("OpenRouter may have changed pricing.")
|
||||
console.print("Alternative: Use very cheap models like openai/gpt-4o-mini")
|
||||
logger.info("No free models found")
|
||||
logger.info("OpenRouter may have changed pricing.")
|
||||
logger.info("Alternative: Use very cheap models like openai/gpt-4o-mini")
|
||||
return
|
||||
|
||||
display_models(free_models, show_pricing=False)
|
||||
|
||||
console.print("\n[bold]RECOMMENDED FREE MODELS TO TRY:[/bold]")
|
||||
console.print("-" * 60)
|
||||
logger.info("RECOMMENDED FREE MODELS TO TRY:")
|
||||
logger.info("-" * 60)
|
||||
for model in free_models[:5]: # Top 5
|
||||
console.print(f" - {model.get('id', '')}")
|
||||
logger.info(f" - {model.get('id', '')}")
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: provision.py - Caller Auto-Provisioning Handler
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: provision.py
|
||||
# Description: Caller Auto-Provisioning Handler
|
||||
# Version: 1.0.0
|
||||
# Category: api/handlers/openrouter
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-16): Initial handler - auto-provision caller configs
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2025-11-16
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -21,7 +17,7 @@ Business logic for provisioning OpenRouter API configs:
|
||||
- Ensure caller has complete 3-file JSON structure
|
||||
|
||||
COMPLIANT STANDARDS:
|
||||
- Uses console.print() for output (NO print())
|
||||
- Uses prax logger for output (NO print() or console.print())
|
||||
- Uses prax logger for operations
|
||||
- Standalone functions (no class dependencies)
|
||||
- Imports from caller handler for detection logic
|
||||
@@ -36,7 +32,7 @@ import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
|
||||
from aipass.cli.apps.modules import console
|
||||
from aipass.prax import logger
|
||||
|
||||
from aipass.api.apps.handlers.openrouter.caller import detect_caller_from_stack
|
||||
|
||||
@@ -168,12 +164,12 @@ def provision_json_folder(json_folder: Path) -> bool:
|
||||
|
||||
json_folder.mkdir(parents=True, exist_ok=True)
|
||||
# logger.info(f"Created JSON folder: {json_folder}")
|
||||
console.print(f"[green]Created JSON folder:[/green] {json_folder}")
|
||||
logger.info(f"Created JSON folder: {json_folder}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
# logger.error(f"Failed to create JSON folder {json_folder}: {e}")
|
||||
console.print(f"[red]Error:[/red] Failed to create JSON folder: {e}")
|
||||
logger.error(f"Failed to create JSON folder: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -207,7 +203,7 @@ def create_caller_config(caller: str, json_folder: Path) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
# logger.info(f"Created API config for {caller}: {config_file}")
|
||||
console.print(f"[green]Created config:[/green] {config_file.name}")
|
||||
logger.info(f"Created config: {config_file.name}")
|
||||
|
||||
# Create data file
|
||||
data_file = json_folder / "openrouter_skill_data.json"
|
||||
@@ -215,7 +211,7 @@ def create_caller_config(caller: str, json_folder: Path) -> Dict[str, Any]:
|
||||
|
||||
if write_json(data_file, data):
|
||||
# logger.info(f"Created data file for {caller}: {data_file}")
|
||||
console.print(f"[green]Created data:[/green] {data_file.name}")
|
||||
logger.info(f"Created data: {data_file.name}")
|
||||
|
||||
# Create log file
|
||||
log_file = json_folder / "openrouter_skill_log.json"
|
||||
@@ -223,16 +219,16 @@ def create_caller_config(caller: str, json_folder: Path) -> Dict[str, Any]:
|
||||
|
||||
if write_json(log_file, log_data):
|
||||
# logger.info(f"Created log file for {caller}: {log_file}")
|
||||
console.print(f"[green]Created log:[/green] {log_file.name}")
|
||||
logger.info(f"Created log: {log_file.name}")
|
||||
|
||||
console.print(f"[cyan]Info:[/cyan] Auto-provisioned OpenRouter config for '{caller}'")
|
||||
console.print(f"[yellow]Note:[/yellow] Reload config and retry request")
|
||||
logger.info(f"Auto-provisioned OpenRouter config for '{caller}'")
|
||||
logger.warning("Reload config and retry request")
|
||||
|
||||
return config
|
||||
|
||||
except Exception as e:
|
||||
# logger.error(f"Failed to create config for {caller}: {e}")
|
||||
console.print(f"[red]Error:[/red] Config creation failed: {e}")
|
||||
logger.error(f"Config creation failed: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
@@ -259,7 +255,7 @@ def ensure_caller_config(caller: str | None = None) -> Dict[str, Any]:
|
||||
# logger.info(f"Auto-detected caller: {caller}")
|
||||
else:
|
||||
# logger.warning("Could not detect caller from stack")
|
||||
console.print("[yellow]Warning:[/yellow] Could not detect caller module")
|
||||
logger.warning("Could not detect caller module")
|
||||
return {}
|
||||
|
||||
# Get JSON folder path if not already detected
|
||||
@@ -267,7 +263,7 @@ def ensure_caller_config(caller: str | None = None) -> Dict[str, Any]:
|
||||
_, json_folder = detect_caller_from_stack()
|
||||
if not json_folder:
|
||||
# logger.error(f"Could not determine JSON folder for {caller}")
|
||||
console.print(f"[red]Error:[/red] Could not find JSON folder for '{caller}'")
|
||||
logger.error(f"Could not find JSON folder for '{caller}'")
|
||||
return {}
|
||||
|
||||
# Check if config already exists
|
||||
@@ -280,14 +276,14 @@ def ensure_caller_config(caller: str | None = None) -> Dict[str, Any]:
|
||||
return config
|
||||
else:
|
||||
# logger.warning(f"Config file corrupted for {caller}, regenerating")
|
||||
console.print(f"[yellow]Warning:[/yellow] Corrupted config, regenerating...")
|
||||
logger.warning("Corrupted config, regenerating...")
|
||||
|
||||
# Create new config
|
||||
return create_caller_config(caller, json_folder)
|
||||
|
||||
except Exception as e:
|
||||
# logger.error(f"Failed to ensure config for {caller}: {e}")
|
||||
console.print(f"[red]Error:[/red] Config provisioning failed: {e}")
|
||||
logger.error(f"Config provisioning failed: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: aggregation.py - Usage Aggregation Handler
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: aggregation.py
|
||||
# Description: Usage Aggregation Handler
|
||||
# Version: 1.0.0
|
||||
# Category: api/handlers/usage
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Initial handler - usage stats aggregation
|
||||
# - v1.1.0 (2025-11-16): Extracted aggregation logic from archive
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: cleanup.py - Usage data retention and cleanup
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: cleanup.py
|
||||
# Description: Usage data retention and cleanup
|
||||
# Version: 0.1.0
|
||||
# Category: api/handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v0.1.0 (2025-11-16): Extracted from api_usage.py
|
||||
# - v1.0.0 (2025-11-15): Initial handler - old data cleanup
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler layer (standalone functions)
|
||||
# - Uses CLI service for output
|
||||
# - Under 300 lines
|
||||
# ==============================================
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2025-11-16
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Usage Data Cleanup Handler
|
||||
@@ -32,8 +22,8 @@ import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
# CLI services
|
||||
from aipass.cli.apps.modules import console, success, warning
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
|
||||
def _read_json(file_path: Path) -> Optional[Dict]:
|
||||
@@ -99,13 +89,13 @@ def cleanup_old_data(data_file_path: Path, retention_days: int = 30) -> int:
|
||||
|
||||
_write_json(data_file_path, data)
|
||||
# logger.info(f"Cleaned up {len(old_generations)} generation entries")
|
||||
success(f"Cleaned up {len(old_generations)} generation entries older than {retention_days} days")
|
||||
logger.info(f"Cleaned up {len(old_generations)} generation entries older than {retention_days} days")
|
||||
|
||||
return len(old_generations)
|
||||
|
||||
except Exception as e:
|
||||
# logger.error(f"Cleanup failed: {e}")
|
||||
console.print(f"[red]Cleanup failed: {e}[/red]")
|
||||
logger.error(f"Cleanup failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@@ -162,13 +152,13 @@ def cleanup_daily_totals(data_file_path: Path, retention_days: int = 90) -> int:
|
||||
|
||||
_write_json(data_file_path, data)
|
||||
# logger.info(f"Cleaned up {len(old_dates)} daily total entries")
|
||||
success(f"Cleaned up {len(old_dates)} daily total entries older than {retention_days} days")
|
||||
logger.info(f"Cleaned up {len(old_dates)} daily total entries older than {retention_days} days")
|
||||
|
||||
return len(old_dates)
|
||||
|
||||
except Exception as e:
|
||||
# logger.error(f"Daily totals cleanup failed: {e}")
|
||||
console.print(f"[red]Daily totals cleanup failed: {e}[/red]")
|
||||
logger.error(f"Daily totals cleanup failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@@ -190,7 +180,7 @@ def auto_cleanup(data_file_path: Path, config: Optional[Dict] = None) -> Dict[st
|
||||
|
||||
except Exception as e:
|
||||
# logger.error(f"Auto cleanup failed: {e}")
|
||||
console.print(f"[red]Auto cleanup failed: {e}[/red]")
|
||||
logger.error(f"Auto cleanup failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: tracking.py - Usage Tracking Handler
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: tracking.py
|
||||
# Description: Usage Tracking Handler
|
||||
# Version: 1.0.0
|
||||
# Category: api/handlers/usage
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-16): Extracted tracking logic from archive
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2025-11-16
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: api_key.py - API Key Management Module
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: api_key.py
|
||||
# Description: API Key Management Module
|
||||
# Version: 1.0.0
|
||||
# Category: api/modules
|
||||
# CODE STANDARDS: Seed v1.0.0
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Initial module - orchestrates key management
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: openrouter_client.py - OpenRouter Client Module
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: openrouter_client.py
|
||||
# Description: OpenRouter Client Module
|
||||
# Version: 1.0.0
|
||||
# Category: api/modules
|
||||
# CODE STANDARDS: Seed v1.0.0
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Initial module - orchestrates OpenRouter operations
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -237,7 +232,7 @@ def get_response(prompt: str, caller: str | None = None, model: str | None = Non
|
||||
>>> from aipass.api.apps.modules.openrouter_client import get_response
|
||||
>>> response = get_response("Hello", caller="flow", model="anthropic/claude-3.5-sonnet")
|
||||
>>> if response:
|
||||
... print(response['content'])
|
||||
... console.print(response['content'])
|
||||
"""
|
||||
return client.get_response(prompt, caller, model, **kwargs)
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: usage_tracker.py - Usage Tracking Module
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: usage_tracker.py
|
||||
# Description: Usage Tracking Module
|
||||
# Version: 1.0.0
|
||||
# Category: api/modules
|
||||
# CODE STANDARDS: Seed v1.0.0
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-15): Initial module - orchestrates usage tracking
|
||||
# Created: 2025-11-15
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
+116
-2
@@ -3,13 +3,127 @@
|
||||
**Purpose:** Multi-mode backup with Google Drive integration
|
||||
**Module:** `aipass.backup`
|
||||
**Created:** 2026-03-07
|
||||
**Citizen Class:** birthright
|
||||
**Citizen Class:** builder
|
||||
**Last Updated:** 2026-03-08
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Birthright citizen — minimal presence with identity and memory.
|
||||
Builder citizen — full 3-layer architecture with identity and memory.
|
||||
|
||||
Provides automated file protection through snapshot backups, versioned backups,
|
||||
and Google Drive synchronization. The entry point routes commands to four
|
||||
specialized modules: `backup_core`, `google_drive_sync`, `integrations`, and
|
||||
`reauth_drive`.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
backup/
|
||||
├── __init__.py
|
||||
├── README.md
|
||||
├── apps/
|
||||
│ ├── backup.py # Entry point (CLI) — drone @backup
|
||||
│ ├── modules/
|
||||
│ │ ├── backup_core.py # Core backup operations (snapshot, versioned)
|
||||
│ │ ├── google_drive_sync.py # Google Drive sync orchestration
|
||||
│ │ ├── integrations.py # Cross-module integration commands
|
||||
│ │ └── reauth_drive.py # Google Drive re-authentication
|
||||
│ ├── handlers/
|
||||
│ │ ├── config/
|
||||
│ │ │ └── config_handler.py # Configuration management
|
||||
│ │ ├── diff/
|
||||
│ │ │ ├── diff_generator.py # Diff generation between backups
|
||||
│ │ │ ├── version_manager.py # Version tracking
|
||||
│ │ │ └── vscode_integration.py # VS Code diff viewer integration
|
||||
│ │ ├── json/
|
||||
│ │ │ ├── backup_info_handler.py # Backup info JSON read/write
|
||||
│ │ │ ├── backup_metadata_builder.py # Metadata construction
|
||||
│ │ │ ├── changelog_handler.py # Changelog JSON management
|
||||
│ │ │ ├── drive_sync_json.py # Drive sync state tracking
|
||||
│ │ │ ├── json_handler.py # Generic JSON utilities
|
||||
│ │ │ └── statistics_handler.py # Backup statistics
|
||||
│ │ ├── models/
|
||||
│ │ │ └── backup_models.py # Data models for backup objects
|
||||
│ │ ├── operations/
|
||||
│ │ │ ├── drive_sync_client.py # Google Drive API client
|
||||
│ │ │ ├── drive_sync_ops.py # Drive sync implementation
|
||||
│ │ │ ├── file_cleanup.py # Old backup cleanup
|
||||
│ │ │ ├── file_operations.py # File copy/move operations
|
||||
│ │ │ ├── file_scanner.py # File discovery and filtering
|
||||
│ │ │ ├── integration_ops.py # Integration operation logic
|
||||
│ │ │ └── path_builder.py # Backup path construction
|
||||
│ │ ├── reporting/
|
||||
│ │ │ └── report_formatter.py # Backup report formatting
|
||||
│ │ └── utils/
|
||||
│ │ ├── backup_timestamps.py # Timestamp utilities
|
||||
│ │ ├── reauth_handler.py # Re-auth implementation
|
||||
│ │ └── system_utils.py # System-level utilities
|
||||
│ ├── extensions/ # Extension point (placeholder)
|
||||
│ ├── json_templates/ # JSON template files
|
||||
│ └── plugins/ # Plugin point (placeholder)
|
||||
├── backup_json/ # JSON tracking data
|
||||
├── artifacts/ # Backup artifacts
|
||||
├── docs/ # Documentation
|
||||
├── tests/ # Test suite
|
||||
└── tools/ # Branch verification utilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands / Usage
|
||||
|
||||
```bash
|
||||
drone @backup # Introspection — list discovered modules
|
||||
drone @backup --help # Show full help
|
||||
drone @backup --version # Show version
|
||||
drone @backup --all # Full backup cycle: snapshot -> versioned -> drive-sync
|
||||
drone @backup snapshot # Create a system snapshot backup
|
||||
drone @backup versioned # Create a versioned backup
|
||||
drone @backup drive-test # Test Google Drive connectivity
|
||||
drone @backup drive-sync # Sync backups to Google Drive
|
||||
drone @backup drive-sync --test # Run a small test sync to verify integration
|
||||
drone @backup drive-stats # Show Drive file tracker statistics
|
||||
drone @backup drive-clear-tracker # Clear Drive file tracker cache
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--verbose`, `-v` | Extra diagnostic output |
|
||||
| `--dry-run` | Preview what would happen, execute nothing |
|
||||
| `--note NOTE` | Add a backup note/description |
|
||||
| `--project NAME` | Project name for Drive sync (default: AIPass) |
|
||||
| `--force` | Force sync all files (ignore change tracker) |
|
||||
| `--limit N` | Limit drive-sync to first N files |
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Depends On
|
||||
- `rich` — Console output and formatting
|
||||
- Python stdlib (`sys`, `argparse`, `logging`, `pathlib`)
|
||||
|
||||
### Provides To
|
||||
- All modules — automated file protection, snapshot and versioned backups
|
||||
- Google Drive — cloud backup synchronization
|
||||
- Other branches — backup artifacts via `backup_json/` and `artifacts/`
|
||||
|
||||
---
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `backup_core` | Core backup operations — snapshot and versioned backup creation |
|
||||
| `google_drive_sync` | Google Drive synchronization — upload, track, and manage cloud backups |
|
||||
| `integrations` | Cross-module integration commands and coordination |
|
||||
| `reauth_drive` | Google Drive re-authentication when credentials expire |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: backup.py - Backup System Entry Point
|
||||
# Date: 2025-11-22
|
||||
# Version: 1.3.0
|
||||
# Category: core/backup
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.3.0 (2026-03-06): Adapted for AIPass public repo
|
||||
# * Removed shebang, sys.path manipulation, prax/cli imports
|
||||
# * Replaced get_modules() with explicit relative imports
|
||||
# * Uses standard logging and rich console
|
||||
# - v1.2.0 (2026-02-22): Seed CLI flags standard: --version, backup all command
|
||||
# - v1.1.0 (2025-11-22): Fixed META header - correct filename, category, and standards
|
||||
# - v1.0.0 (2025-11-08): Initial version - modular architecture
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers implement logic, modules orchestrate
|
||||
# =================== AIPass ====================
|
||||
# Name: backup.py
|
||||
# Description: Entry point CLI for drone @backup
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -28,14 +16,11 @@ Main handles routing, modules implement functionality.
|
||||
# Standard library imports
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, List
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console = Console()
|
||||
from aipass.cli.apps.modules import console
|
||||
from aipass.prax import logger
|
||||
|
||||
# Explicit module imports (replaces dynamic discover_modules)
|
||||
from aipass.backup.apps.modules import backup_core, google_drive_sync, integrations, reauth_drive
|
||||
|
||||
@@ -1,29 +1,9 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: config_handler.py - Backup system configuration and patterns
|
||||
# Date: 2025-11-23
|
||||
# =================== AIPass ====================
|
||||
# Name: config_handler.py
|
||||
# Description: Backup system configuration and patterns
|
||||
# Version: 2.0.2
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.2 (2026-03-06): Adapted for AIPass public repo
|
||||
# * Removed shebang and sys.path manipulation
|
||||
# * Cleared CLI_TRACKING_PATTERNS (hardcoded internal paths removed)
|
||||
# - v2.0.1 (2025-11-23): Added ignore patterns for log/data JSON files
|
||||
# * Enabled *_data.json pattern to ignore backup_core_data.json
|
||||
# * Enabled *_log.json pattern to ignore backup_core_log.json, drone_log.json
|
||||
# * Enabled snapshot_backup.json pattern
|
||||
# * Prevents frequent-change files from being backed up every run
|
||||
# - v2.0.0 (2025-11-16): Seed standard extraction from archive
|
||||
# * All configuration constants and patterns
|
||||
# * Helper functions for pattern matching and filtering
|
||||
# * Pure configuration - no complex logic
|
||||
# - v1.0.0 (2025-10-14): Initial extraction from backup.py
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-23
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: diff_generator.py - Unified diff generation with binary detection
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: diff_generator.py
|
||||
# Description: Unified diff generation with binary detection
|
||||
# Version: 2.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.0 (2025-11-16): Extracted from backup_diff.py
|
||||
# * Diff generation functionality
|
||||
# * Binary file detection
|
||||
# * Unified diff content generation
|
||||
# * Updated imports to new handler locations
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: version_manager.py - Version discovery and management
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: version_manager.py
|
||||
# Description: Version discovery and management
|
||||
# Version: 2.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.0 (2025-11-16): Extracted from backup_diff.py
|
||||
# * Version file discovery functionality
|
||||
# * Versioned file listing
|
||||
# * Support for file-organized structure
|
||||
# * Updated imports to new handler locations
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -30,11 +17,11 @@ Supports version listing and filtering.
|
||||
# IMPORTS
|
||||
# =============================================
|
||||
|
||||
import logging
|
||||
from aipass.prax import logger
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# logger imported from aipass.prax
|
||||
|
||||
# Import from handlers
|
||||
from aipass.backup.apps.handlers.utils.system_utils import safe_print
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: vscode_integration.py - VS Code diff viewer integration
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: vscode_integration.py
|
||||
# Description: VS Code diff viewer integration
|
||||
# Version: 2.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.0 (2025-11-16): Extracted from backup_diff.py
|
||||
# * VS Code diff viewer integration
|
||||
# * File comparison logic
|
||||
# * Baseline vs current comparison
|
||||
# * Updated imports to new handler locations
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -33,10 +20,10 @@ Supports baseline vs current and version-to-version comparisons.
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
from aipass.prax import logger
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# logger imported from aipass.prax
|
||||
|
||||
# Import from handlers
|
||||
from aipass.backup.apps.handlers.utils.system_utils import safe_print
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: backup_info_handler.py - Backup state and statistics
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: backup_info_handler.py
|
||||
# Description: Backup state and statistics
|
||||
# Version: 1.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-16): Initial extraction from backup_core.py
|
||||
# * load_backup_info() - Load mode-specific state
|
||||
# * save_backup_info() - Persist backup state
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: backup_metadata_builder.py - Backup metadata construction
|
||||
# Date: 2025-11-18
|
||||
# =================== AIPass ====================
|
||||
# Name: backup_metadata_builder.py
|
||||
# Description: Backup metadata construction
|
||||
# Version: 1.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-18): Extracted from backup_core.py
|
||||
# * Extracted backup_info creation logic
|
||||
# * Handles mode-specific metadata formatting
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-18
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: changelog_handler.py - Backup changelog management
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: changelog_handler.py
|
||||
# Description: Backup changelog management
|
||||
# Version: 1.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-16): Initial extraction from backup_core.py
|
||||
# * load_changelog() - Load persistent changelog
|
||||
# * save_changelog_entry() - Add new entry
|
||||
# * display_previous_comments() - Show history
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -35,17 +23,12 @@ Functions:
|
||||
# =============================================
|
||||
|
||||
import json
|
||||
import logging
|
||||
from aipass.prax import logger
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
# Initialize console for output
|
||||
console = Console()
|
||||
# logger imported from aipass.prax
|
||||
|
||||
# =============================================
|
||||
# CHANGELOG OPERATIONS
|
||||
@@ -65,8 +48,7 @@ def load_changelog(changelog_file: Path) -> Dict:
|
||||
with open(changelog_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error loading changelog: {e}[/red]")
|
||||
logger.warning(f"Error loading changelog: {e}")
|
||||
logger.error(f"Error loading changelog: {e}")
|
||||
return {"entries": []}
|
||||
|
||||
|
||||
@@ -98,7 +80,6 @@ def save_changelog_entry(changelog_file: Path, note: str, mode: str,
|
||||
logger.info(f"[changelog_handler] Saved changelog entry: {note[:50]}")
|
||||
return True
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error saving changelog entry: {e}[/red]")
|
||||
logger.error(f"[changelog_handler] Error saving changelog: {e}")
|
||||
return False
|
||||
|
||||
@@ -115,14 +96,10 @@ def display_previous_comments(changelog_file: Path, mode_name: str):
|
||||
entries = changelog.get("entries", [])
|
||||
|
||||
if not entries:
|
||||
console.print(f"[yellow]No previous {mode_name} backup comments found.[/yellow]")
|
||||
logger.warning(f"No previous {mode_name} backup comments found.")
|
||||
return
|
||||
|
||||
from rich.panel import Panel
|
||||
console.print()
|
||||
console.print(Panel(f"PREVIOUS {mode_name.upper()} BACKUP COMMENTS",
|
||||
style="bold cyan",
|
||||
border_style="cyan"))
|
||||
logger.info(f"PREVIOUS {mode_name.upper()} BACKUP COMMENTS")
|
||||
|
||||
# Show last 10 entries (most recent first)
|
||||
recent_entries = entries[-10:]
|
||||
@@ -133,21 +110,20 @@ def display_previous_comments(changelog_file: Path, mode_name: str):
|
||||
mode_info = entry.get('mode', 'unknown')
|
||||
# Handle encoding issues in notes
|
||||
note = str(entry['note']).encode('ascii', errors='replace').decode('ascii')
|
||||
console.print(f"[bright_blue]{i:2d}.[/bright_blue] [{formatted_time}] [green][{mode_info}][/green] {note}")
|
||||
logger.info(f"{i:2d}. [{formatted_time}] [{mode_info}] {note}")
|
||||
except Exception as e:
|
||||
console.print(f"[bright_blue]{i:2d}.[/bright_blue] [red][ERROR] Failed to display entry: {e}[/red]")
|
||||
logger.error(f"{i:2d}. [ERROR] Failed to display entry: {e}")
|
||||
|
||||
if len(entries) > 10:
|
||||
console.print(f"\n[dim]... and {len(entries) - 10} older entries[/dim]")
|
||||
console.print()
|
||||
logger.info(f"... and {len(entries) - 10} older entries")
|
||||
except FileNotFoundError:
|
||||
console.print(f"[yellow]No previous {mode_name} backup comments found.[/yellow]")
|
||||
logger.warning(f"No previous {mode_name} backup comments found.")
|
||||
except PermissionError as e:
|
||||
console.print(f"[yellow]Warning: Cannot read backup history - permission denied: {e}[/yellow]")
|
||||
console.print("[yellow]Continuing with backup...[/yellow]")
|
||||
logger.warning(f"Cannot read backup history - permission denied: {e}")
|
||||
logger.warning("Continuing with backup...")
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Warning: Error displaying comments: {e}[/yellow]")
|
||||
console.print("[yellow]Continuing with backup...[/yellow]")
|
||||
logger.warning(f"Error displaying comments: {e}")
|
||||
logger.warning("Continuing with backup...")
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: drive_sync_json.py - Google Drive Sync JSON Operations Handler
|
||||
# Date: 2026-02-20
|
||||
# =================== AIPass ====================
|
||||
# Name: drive_sync_json.py
|
||||
# Description: Google Drive Sync JSON Operations Handler
|
||||
# Version: 1.0.0
|
||||
# Category: backup_system/handlers/json
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-20): Extracted from google_drive_sync module
|
||||
# * Moved JSON I/O functions to handler layer
|
||||
# * Follows seed 3-layer architecture standards
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers must be independent and transportable
|
||||
# - No prax imports (handler tier)
|
||||
# - Pure file I/O operations
|
||||
# Created: 2026-02-20
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: json_handler.py - JSON Auto-Creating Handler
|
||||
# Date: 2025-11-22
|
||||
# =================== AIPass ====================
|
||||
# Name: json_handler.py
|
||||
# Description: JSON Auto-Creating Handler
|
||||
# Version: 3.1.0
|
||||
# Category: backup_system/handlers/json
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v3.1.0 (2026-01-31): Refactored to comply with error handling standards
|
||||
# - v3.0.0 (2025-11-22): Updated to match Cortex template with all features
|
||||
# - v2.0.0 (2025-11-16): Migrated to seed 3-layer architecture
|
||||
# - v1.0.0 (2025-10-14): Initial implementation with auto-detection
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Pure functions with proper error raising
|
||||
# - No Prax imports (handler tier 3)
|
||||
# Created: 2025-11-22
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
import json
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: statistics_handler.py - Backup statistics tracking
|
||||
# Date: 2025-11-18
|
||||
# =================== AIPass ====================
|
||||
# Name: statistics_handler.py
|
||||
# Description: Backup statistics tracking
|
||||
# Version: 1.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-18): Extracted from backup_core.py
|
||||
# * Extracted update_data_file() statistics tracking
|
||||
# * Handles runtime state and statistics updates
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-18
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -27,11 +16,11 @@ Manages backup operation statistics and runtime state persistence.
|
||||
# IMPORTS
|
||||
# =============================================
|
||||
|
||||
import logging
|
||||
from aipass.prax import logger
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# logger imported from aipass.prax
|
||||
|
||||
# Import handlers
|
||||
from aipass.backup.apps.handlers.json.json_handler import load_json, save_json
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: backup_models.py - Backup system data models and structures
|
||||
# Date: 2025-11-16
|
||||
# =================== AIPass ====================
|
||||
# Name: backup_models.py
|
||||
# Description: Backup system data models and structures
|
||||
# Version: 2.0.1
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.1 (2026-03-06): Adapted for AIPass public repo
|
||||
# * Removed shebang and sys.path manipulation
|
||||
# - v2.0.0 (2025-11-16): Migrated to seed 3-layer architecture
|
||||
# - v1.0.0 (2025-10-14): Initial extraction from backup.py
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-16
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: drive_sync_client.py - Google Drive Sync Client Handler
|
||||
# Date: 2026-02-20
|
||||
# =================== AIPass ====================
|
||||
# Name: drive_sync_client.py
|
||||
# Description: Google Drive Sync Client Handler
|
||||
# Version: 1.0.0
|
||||
# Category: backup_system/handlers/operations
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-20): Extracted GoogleDriveSync class from module
|
||||
# * Moved implementation to handler layer per seed 3-layer architecture
|
||||
# * Module now delegates to this handler for all Drive operations
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers must be independent and transportable
|
||||
# - No prax imports in class methods (handler tier)
|
||||
# - Google API interactions isolated here
|
||||
# Created: 2026-02-20
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: drive_sync_ops.py - Google Drive Sync Business Operations Handler
|
||||
# Date: 2026-02-20
|
||||
# =================== AIPass ====================
|
||||
# Name: drive_sync_ops.py
|
||||
# Description: Google Drive Sync Business Operations Handler
|
||||
# Version: 1.0.0
|
||||
# Category: backup_system/handlers/operations
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-20): Extracted from google_drive_sync module
|
||||
# * Moved get_status, clear_file_tracker, show_file_tracker_stats,
|
||||
# test_drive_sync to handler layer
|
||||
# * Follows seed 3-layer architecture standards
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers must be independent and transportable
|
||||
# - No prax imports (handler tier)
|
||||
# - Delegates to json handler for file operations
|
||||
# Created: 2026-02-20
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,29 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: file_cleanup.py - File deletion operations
|
||||
# Date: 2025-11-23
|
||||
# =================== AIPass ====================
|
||||
# Name: file_cleanup.py
|
||||
# Description: File deletion operations
|
||||
# Version: 1.0.2
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.2 (2025-11-23): Added dry-run support for cleanup operations
|
||||
# * Added dry_run parameter to cleanup_deleted_files()
|
||||
# * Shows "Would delete" messages instead of actually deleting in dry-run
|
||||
# * All three passes (directories, files, empty dirs) respect dry-run mode
|
||||
# - v1.0.1 (2025-11-23): CRITICAL BUG FIX - respect exceptions in empty dir cleanup
|
||||
# * Third pass now checks should_ignore() before removing empty directories
|
||||
# * Prevents deletion of empty template directories
|
||||
# * Added source_dir mapping to check exceptions properly
|
||||
# * Only removes empty dirs if source doesn't exist OR should be ignored
|
||||
# - v1.0.0 (2025-11-18): Extracted from backup_core.py
|
||||
# * Extracted file deletion logic for dynamic mode
|
||||
# * Handles cleanup of deleted source files
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-23
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -36,11 +16,11 @@ Handles cleanup of backup files when source files are deleted.
|
||||
# IMPORTS
|
||||
# =============================================
|
||||
|
||||
import logging
|
||||
from aipass.prax import logger
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# logger imported from aipass.prax
|
||||
|
||||
from aipass.backup.apps.handlers.utils.system_utils import temporarily_writable, safe_print
|
||||
from aipass.backup.apps.handlers.models.backup_models import BackupResult
|
||||
|
||||
@@ -1,40 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: file_operations.py - Core backup file operations
|
||||
# Date: 2025-11-23
|
||||
# =================== AIPass ====================
|
||||
# Name: file_operations.py
|
||||
# Description: Core backup file operations
|
||||
# Version: 2.0.4
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.0.4 (2025-11-23): Fix baseline snapshot messages for VS Code clickability
|
||||
# * Changed baseline snapshot message from {baseline_name} to {baseline_path}
|
||||
# * Users can now Ctrl+click on baseline paths to jump directly to files
|
||||
# * Shows full path (e.g., /home/aipass/backups/project/README-baseline-2025-11-23.md)
|
||||
# - v2.0.3 (2025-11-23): CRITICAL performance fix for versioned backup
|
||||
# * Skip copying unchanged files (only copy if mtime differs)
|
||||
# * Fixes 5s -> <1s regression (was copying all 5000+ files every time)
|
||||
# * Added file_changed flag to track copy necessity
|
||||
# * Only copy if is_new_file or file_changed
|
||||
# - v2.0.2 (2025-11-23): Added per-file output to snapshot mode
|
||||
# * Snapshot backup now shows each file as it's copied
|
||||
# * Displays "Copied (new)" or "Copied (updated)" for each file
|
||||
# * Matches the verbosity of versioned backup mode
|
||||
# - v2.0.1 (2025-11-23): Fixed automatic VS Code diff opening
|
||||
# * Removed automatic VS Code integration from copy_versioned_file()
|
||||
# * Diffs are now created silently without opening in editor
|
||||
# * VS Code integration still available via separate command
|
||||
# - v2.0.0 (2025-11-16): Extraction from backup_operations.py
|
||||
# * Extracted core file operations (lines 49-271)
|
||||
# * copy_file_with_structure for snapshot mode
|
||||
# * copy_versioned_file with baseline snapshots and diff generation
|
||||
# * Extensive Linux permission handling with temporarily_writable()
|
||||
# * Retry logic for filesystem errors
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-23
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -52,12 +21,12 @@ Functions:
|
||||
# IMPORTS
|
||||
# =============================================
|
||||
|
||||
import logging
|
||||
from aipass.prax import logger
|
||||
import shutil
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# logger imported from aipass.prax
|
||||
|
||||
# Import from handlers modules
|
||||
from aipass.backup.apps.handlers.utils.system_utils import temporarily_writable, safe_print
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: file_scanner.py - File system scanning operations
|
||||
# Date: 2025-11-18
|
||||
# =================== AIPass ====================
|
||||
# Name: file_scanner.py
|
||||
# Description: File system scanning operations
|
||||
# Version: 1.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-18): Extracted from backup_core.py
|
||||
# * Extracted file scanning logic (os.walk loops)
|
||||
# * Handles file discovery with ignore patterns
|
||||
# * Returns file list with metadata
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-18
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: integration_ops.py - Backup Integration Operations Handler
|
||||
# Date: 2026-02-20
|
||||
# =================== AIPass ====================
|
||||
# Name: integration_ops.py
|
||||
# Description: Backup Integration Operations Handler
|
||||
# Version: 1.0.0
|
||||
# Category: backup_system/handlers/operations
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-20): Extracted from integrations module
|
||||
# * Moved sync_to_drive() and set_backup_readonly() to handler layer
|
||||
# * Follows seed 3-layer architecture standards
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers must be independent and transportable
|
||||
# - No prax imports (handler tier)
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2026-02-20
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: path_builder.py - Backup path construction
|
||||
# Date: 2025-11-18
|
||||
# =================== AIPass ====================
|
||||
# Name: path_builder.py
|
||||
# Description: Backup path construction
|
||||
# Version: 1.0.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2025-11-18): Extracted from backup_core.py
|
||||
# * Extracted path construction logic for versioned mode
|
||||
# * Handles long filename hashing
|
||||
# * Mode-specific path building
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-18
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: sync_test_ops.py
|
||||
# Description: File operations for Drive sync test
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Sync Test File Operations Handler
|
||||
|
||||
Handles file system operations for the Drive sync test workflow:
|
||||
- Creating test directory and test files
|
||||
- Cleaning up test directory after test completes
|
||||
"""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from aipass.prax import logger
|
||||
|
||||
|
||||
def create_sync_test_files(backup_root: Path) -> dict:
|
||||
"""Create test directory and test files for Drive sync verification.
|
||||
|
||||
Args:
|
||||
backup_root: Root backup directory (e.g., src/aipass/backup/)
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
- success (bool): Whether files were created
|
||||
- test_dir (Path): Path to test directory
|
||||
- file_count (int): Number of test files created
|
||||
- error (str|None): Error message if failed
|
||||
"""
|
||||
try:
|
||||
test_dir = backup_root / "backups" / "_sync_test"
|
||||
test_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
test_files = {
|
||||
"test_file_1.txt": "Hello from AIPass backup test",
|
||||
"test_file_2.json": '{"test": true, "timestamp": "' + datetime.now().isoformat() + '"}',
|
||||
"subdir/nested_file.txt": "Nested directory test",
|
||||
"subdir/deep/deeper_file.md": "# Deep nested test\nVerifying folder structure",
|
||||
}
|
||||
|
||||
for name, content in test_files.items():
|
||||
path = test_dir / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
|
||||
logger.info(f"[sync_test_ops] Created {len(test_files)} test files in {test_dir}")
|
||||
return {
|
||||
"success": True,
|
||||
"test_dir": test_dir,
|
||||
"file_count": len(test_files),
|
||||
"error": None,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[sync_test_ops] Failed to create test files: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"test_dir": None,
|
||||
"file_count": 0,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
|
||||
def cleanup_sync_test_dir(test_dir: Path) -> dict:
|
||||
"""Remove the sync test directory and all contents.
|
||||
|
||||
Args:
|
||||
test_dir: Path to test directory to remove
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
- success (bool): Whether cleanup succeeded
|
||||
- error (str|None): Error message if failed
|
||||
"""
|
||||
try:
|
||||
shutil.rmtree(test_dir, ignore_errors=True)
|
||||
logger.info(f"[sync_test_ops] Cleaned up test directory: {test_dir}")
|
||||
return {"success": True, "error": None}
|
||||
except Exception as e:
|
||||
logger.error(f"[sync_test_ops] Failed to cleanup test directory: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
@@ -1,25 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: report_formatter.py - Backup result reporting
|
||||
# Date: 2025-11-23
|
||||
# =================== AIPass ====================
|
||||
# Name: report_formatter.py
|
||||
# Description: Backup result reporting
|
||||
# Version: 1.1.0
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.1.0 (2025-11-23): Added dry-run statistics display
|
||||
# * Added dry_run parameter to display_backup_results()
|
||||
# * Shows "Would copy", "Would skip", "Would delete" in dry-run mode
|
||||
# * Provides clear differentiation between dry-run and actual execution
|
||||
# - v1.0.0 (2025-11-18): Extracted from backup_core.py
|
||||
# * Extracted statistics display formatting
|
||||
# * Handles error/warning display
|
||||
# * Formats skipped items reports
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-23
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -35,16 +19,15 @@ Formats and displays backup operation results, statistics, errors, and warnings.
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
from aipass.prax import logger
|
||||
from aipass.backup.apps.handlers.models.backup_models import BackupResult
|
||||
|
||||
console = Console()
|
||||
# logger imported from aipass.prax
|
||||
|
||||
def _header(text):
|
||||
console.print(f"\n[bold cyan]{'='*70}[/bold cyan]")
|
||||
console.print(f"[bold cyan] {text}[/bold cyan]")
|
||||
console.print(f"[bold cyan]{'='*70}[/bold cyan]")
|
||||
|
||||
from aipass.backup.apps.handlers.models.backup_models import BackupResult
|
||||
logger.info(f"{'='*70}")
|
||||
logger.info(f" {text}")
|
||||
logger.info(f"{'='*70}")
|
||||
|
||||
# =============================================
|
||||
# REPORT FORMATTING OPERATIONS
|
||||
@@ -66,88 +49,77 @@ def display_backup_results(result: BackupResult, mode_config: dict, backup_path:
|
||||
|
||||
# Determine overall result status
|
||||
if result.critical_errors:
|
||||
status_style = "[bold red]"
|
||||
status_icon = "!"
|
||||
status_text = "FAILED"
|
||||
elif result.errors > 0:
|
||||
status_style = "[bold red]"
|
||||
status_icon = "X"
|
||||
status_text = "COMPLETED WITH ERRORS"
|
||||
elif result.warnings:
|
||||
status_style = "[bold yellow]"
|
||||
status_icon = "!"
|
||||
status_text = "COMPLETED WITH WARNINGS"
|
||||
else:
|
||||
status_style = "[bold green]"
|
||||
status_icon = "OK"
|
||||
status_text = "COMPLETED SUCCESSFULLY"
|
||||
|
||||
# Use _header for main status
|
||||
console.print()
|
||||
_header(f"{status_icon} {mode_config['name'].upper()} {status_text}")
|
||||
console.print()
|
||||
|
||||
# Statistics Section
|
||||
console.print("[bold cyan]STATISTICS:[/bold cyan]")
|
||||
console.print(f" Files checked: [dim]{result.files_checked}[/dim]")
|
||||
logger.info("STATISTICS:")
|
||||
logger.info(f" Files checked: {result.files_checked}")
|
||||
|
||||
if dry_run:
|
||||
# Dry-run mode: show "would be" language
|
||||
console.print(f" Would copy: [bold]{result.files_copied}[/bold] files")
|
||||
console.print(f" Would skip: [dim]{result.files_skipped}[/dim] files (unchanged)")
|
||||
logger.info(f" Would copy: {result.files_copied} files")
|
||||
logger.info(f" Would skip: {result.files_skipped} files (unchanged)")
|
||||
if result.files_deleted > 0:
|
||||
console.print(f" Would delete: [yellow]{result.files_deleted}[/yellow] files")
|
||||
logger.warning(f" Would delete: {result.files_deleted} files")
|
||||
else:
|
||||
# Normal mode: show actual actions
|
||||
console.print(f" Files copied: [bold]{result.files_copied}[/bold]")
|
||||
logger.info(f" Files copied: {result.files_copied}")
|
||||
if result.mode == 'versioned' and result.files_added > 0:
|
||||
console.print(f" Files added: [green]{result.files_added}[/green] (new)")
|
||||
console.print(f" Files skipped: [dim]{result.files_skipped}[/dim]")
|
||||
logger.info(f" Files added: {result.files_added} (new)")
|
||||
logger.info(f" Files skipped: {result.files_skipped}")
|
||||
if result.mode == 'versioned' and result.files_deleted == 0:
|
||||
console.print(f" Files deleted: [dim]{result.files_deleted}[/dim] [dim](cleanup disabled - keeps history)[/dim]")
|
||||
logger.info(f" Files deleted: {result.files_deleted} (cleanup disabled - keeps history)")
|
||||
else:
|
||||
console.print(f" Files deleted: [red]{result.files_deleted}[/red]")
|
||||
logger.info(f" Files deleted: {result.files_deleted}")
|
||||
|
||||
console.print(f" Errors: {status_style}{result.errors}[/]")
|
||||
console.print(f" Warnings: [yellow]{len(result.warnings)}[/yellow]")
|
||||
console.print(f" Duration: [blue]{duration.total_seconds():.2f}s[/blue]")
|
||||
console.print(f" Location: [dim]{backup_path}[/dim]")
|
||||
logger.info(f" Errors: {result.errors}")
|
||||
logger.info(f" Warnings: {len(result.warnings)}")
|
||||
logger.info(f" Duration: {duration.total_seconds():.2f}s")
|
||||
logger.info(f" Location: {backup_path}")
|
||||
|
||||
# Display detailed error information
|
||||
if result.critical_errors:
|
||||
console.print()
|
||||
console.print(f"[bold red]CRITICAL ERRORS ({len(result.critical_errors)}):[/bold red]")
|
||||
console.print("-" * 40)
|
||||
logger.error(f"CRITICAL ERRORS ({len(result.critical_errors)}):")
|
||||
logger.info("-" * 40)
|
||||
for i, error in enumerate(result.critical_errors, 1):
|
||||
console.print(f" {i}. [red]{error}[/red]")
|
||||
console.print()
|
||||
console.print("[bold]RECOVERY SUGGESTIONS:[/bold]")
|
||||
console.print(" - Check disk space and permissions")
|
||||
console.print(" - Ensure backup destination is accessible")
|
||||
console.print(" - Try running as administrator if permission issues")
|
||||
console.print(" - Check if antivirus is blocking file operations")
|
||||
logger.error(f" {i}. {error}")
|
||||
logger.info("RECOVERY SUGGESTIONS:")
|
||||
logger.info(" - Check disk space and permissions")
|
||||
logger.info(" - Ensure backup destination is accessible")
|
||||
logger.info(" - Try running as administrator if permission issues")
|
||||
logger.info(" - Check if antivirus is blocking file operations")
|
||||
|
||||
elif result.error_details:
|
||||
console.print()
|
||||
console.print(f"[bold red]ERRORS ({len(result.error_details)}):[/bold red]")
|
||||
console.print("-" * 40)
|
||||
logger.error(f"ERRORS ({len(result.error_details)}):")
|
||||
logger.info("-" * 40)
|
||||
for i, error in enumerate(result.error_details[:10], 1):
|
||||
console.print(f" {i}. [red]{error}[/red]")
|
||||
logger.error(f" {i}. {error}")
|
||||
if len(result.error_details) > 10:
|
||||
console.print(f" [dim]... and {len(result.error_details) - 10} more errors[/dim]")
|
||||
console.print()
|
||||
console.print("[bold]SUGGESTIONS:[/bold]")
|
||||
console.print(" - Some files may be in use - try closing applications")
|
||||
console.print(" - Check file permissions on failed files")
|
||||
logger.info(f" ... and {len(result.error_details) - 10} more errors")
|
||||
logger.info("SUGGESTIONS:")
|
||||
logger.info(" - Some files may be in use - try closing applications")
|
||||
logger.info(" - Check file permissions on failed files")
|
||||
|
||||
if result.warnings:
|
||||
console.print()
|
||||
console.print(f"[bold yellow]WARNINGS ({len(result.warnings)}):[/bold yellow]")
|
||||
console.print("-" * 40)
|
||||
logger.warning(f"WARNINGS ({len(result.warnings)}):")
|
||||
logger.info("-" * 40)
|
||||
for i, warning in enumerate(result.warnings[:5], 1):
|
||||
console.print(f" {i}. [yellow]{warning}[/yellow]")
|
||||
logger.warning(f" {i}. {warning}")
|
||||
if len(result.warnings) > 5:
|
||||
console.print(f" [dim]... and {len(result.warnings) - 5} more warnings[/dim]")
|
||||
logger.info(f" ... and {len(result.warnings) - 5} more warnings")
|
||||
|
||||
# Display project-specific skipped items
|
||||
tracked_items = filter_tracked_items_func(skipped_items)
|
||||
@@ -155,26 +127,24 @@ def display_backup_results(result: BackupResult, mode_config: dict, backup_path:
|
||||
total_all_skipped = len(skipped_items["directories"]) + len(skipped_items["files"])
|
||||
|
||||
if total_tracked > 0:
|
||||
console.print()
|
||||
console.print(f"[bold cyan]NOTABLE SKIPPED ITEMS ({total_tracked}):[/bold cyan]")
|
||||
console.print(f"[dim]Total ignored: {total_all_skipped}[/dim]")
|
||||
console.print("-" * 50)
|
||||
logger.info(f"NOTABLE SKIPPED ITEMS ({total_tracked}):")
|
||||
logger.info(f"Total ignored: {total_all_skipped}")
|
||||
logger.info("-" * 50)
|
||||
|
||||
if tracked_items["directories"]:
|
||||
console.print(f"[bold]Directories ({len(tracked_items['directories'])}):[/bold]")
|
||||
logger.info(f"Directories ({len(tracked_items['directories'])}):")
|
||||
for i, dir_path in enumerate(sorted(tracked_items["directories"]), 1):
|
||||
console.print(f" {i}. [dim]{dir_path}/[/dim]")
|
||||
logger.info(f" {i}. {dir_path}/")
|
||||
|
||||
if tracked_items["files"]:
|
||||
console.print(f"[bold]Files ({len(tracked_items['files'])}):[/bold]")
|
||||
logger.info(f"Files ({len(tracked_items['files'])}):")
|
||||
for i, file_path in enumerate(sorted(tracked_items["files"]), 1):
|
||||
console.print(f" {i}. [dim]{file_path}[/dim]")
|
||||
logger.info(f" {i}. {file_path}")
|
||||
else:
|
||||
console.print()
|
||||
if total_all_skipped > 0:
|
||||
console.print(f"[dim]No project-specific items skipped ({total_all_skipped} common items filtered)[/dim]")
|
||||
logger.info(f"No project-specific items skipped ({total_all_skipped} common items filtered)")
|
||||
else:
|
||||
console.print("[dim]No items were skipped.[/dim]")
|
||||
logger.info("No items were skipped.")
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: backup_timestamps.py - Tracks last-run timestamps for all backup modes
|
||||
# Date: 2026-02-22
|
||||
# =================== AIPass ====================
|
||||
# Name: backup_timestamps.py
|
||||
# Description: Tracks last-run timestamps for all backup modes
|
||||
# Version: 1.0.0
|
||||
# Category: handlers/utils
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-22): Initial version - show/update backup timestamps
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handler utility: no console output, returns data for module layer
|
||||
# - JSON file at backups/backup_timestamps.json
|
||||
# Created: 2026-02-22
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: reauth_handler.py - Google Drive Re-Authentication Handler
|
||||
# Date: 2026-02-20
|
||||
# =================== AIPass ====================
|
||||
# Name: reauth_handler.py
|
||||
# Description: Google Drive Re-Authentication Handler
|
||||
# Version: 1.0.0
|
||||
# Category: backup_system/handlers/utils
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.0.0 (2026-02-20): Extracted from reauth_drive.py module
|
||||
# * Moved reauth() implementation to handler layer
|
||||
# * Follows seed 3-layer architecture standards
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers must be independent and transportable
|
||||
# - No prax imports (handler tier)
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2026-02-20
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,31 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: system_utils.py - Platform-aware file and console utilities
|
||||
# Date: 2025-11-23
|
||||
# =================== AIPass ====================
|
||||
# Name: system_utils.py
|
||||
# Description: Platform-aware file and console utilities
|
||||
# Version: 2.1.1
|
||||
# Category: handlers
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.1.1 (2025-11-23): CRITICAL FIX - Switch to plain print() for clickability
|
||||
# * Replaced console.print() with plain print() in safe_print()
|
||||
# * Rich console truncates at 80 chars breaking long paths
|
||||
# * Plain print() preserves full paths for terminal Ctrl+click
|
||||
# - v2.1.0 (2025-11-23): Attempted fix with no_wrap parameter (insufficient)
|
||||
# * Added no_wrap=True to console.print() but Rich still truncates
|
||||
# * Identified Rich console 80-char limit as root cause
|
||||
# - v2.0.0 (2025-11-16): Extracted to seed standards
|
||||
# * Applied seed standards formatting and meta header
|
||||
# * Preserved all logic from backup_utils v1.0.0
|
||||
# - v1.1.0 (2025-11-22): Updated to seed Rich formatting standards
|
||||
# - v1.0.0 (2025-11-18): Extracted from backup_core.py
|
||||
# * Extracted system utilities
|
||||
# * Cross-platform file operations
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Handlers must be independent and transportable
|
||||
# - No cross-handler imports except within same domain
|
||||
# Created: 2025-11-23
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -48,11 +26,11 @@ Key Functions:
|
||||
import sys
|
||||
import os
|
||||
import stat
|
||||
import logging
|
||||
from aipass.prax import logger
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# logger imported from aipass.prax
|
||||
|
||||
# =============================================
|
||||
# CONSTANTS
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: backup_core.py - Main backup system orchestration module
|
||||
# Date: 2025-11-23
|
||||
# =================== AIPass ====================
|
||||
# Name: backup_core.py
|
||||
# Description: Main backup system orchestration module
|
||||
# Version: 2.1.0
|
||||
# Category: backup_system
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.1.0 (2026-03-06): Adapted for AIPass public repo
|
||||
# * Removed shebang, sys.path manipulation, prax/cli imports
|
||||
# * Uses standard logging and rich console
|
||||
# * Relative handler imports
|
||||
# * JSON_DIR uses path relative to __file__
|
||||
# - v2.0.4 (2025-11-23): CRITICAL FIX - Versioned dry-run accuracy
|
||||
# - v2.0.3 (2025-11-23): Enhanced dry-run with accurate statistics
|
||||
# - v2.0.2 (2025-11-23): CRITICAL FIX - Dry-run now shows proposed changes
|
||||
# - v2.0.1 (2025-11-23): CRITICAL BUG FIX - Dry-run statistics counter
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Orchestrate workflows, delegate to handlers
|
||||
# - Import handlers, never implement business logic
|
||||
# Created: 2025-11-23
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -48,14 +32,11 @@ Architecture Pattern:
|
||||
# Infrastructure
|
||||
import sys
|
||||
import datetime
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console = Console()
|
||||
from aipass.cli.apps.modules import console
|
||||
from aipass.prax import logger
|
||||
|
||||
|
||||
def _header(text):
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: google_drive_sync.py - Google Drive Integration for AIPass Backup System
|
||||
# Date: 2025-10-30
|
||||
# =================== AIPass ====================
|
||||
# Name: google_drive_sync.py
|
||||
# Description: Google Drive Integration for AIPass Backup System
|
||||
# Version: 2.6.0
|
||||
# Category: backup_system/modules
|
||||
#
|
||||
# CHANGELOG (Max 5 entries - remove oldest when adding new):
|
||||
# - v2.6.0 (2026-03-06): Adapted for AIPass public repo
|
||||
# * Removed shebang, sys.path manipulation, prax/cli imports
|
||||
# * Uses standard logging and rich console
|
||||
# * Relative handler imports, Google API deps wrapped in try/except
|
||||
# - v2.5.0 (2026-02-22): Rich progress bar for drive-sync uploads, per-file progress callback
|
||||
# - v2.4.0 (2026-02-21): Two-phase sync (prepare+upload), CLI visibility, dedup fix, --test mode
|
||||
# - v2.3.2 (2026-02-10): Fixed deepcopy RuntimeError in _save_data during concurrent uploads
|
||||
# - v2.2.0 (2026-02-10): Fixed SSL concurrency bug - per-thread credentials, retry with backoff, folder cache lock
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Module orchestrates, handlers implement
|
||||
# - CLI display (console.print) belongs here, not in handlers
|
||||
# - Google API calls delegated to drive_sync_client handler
|
||||
# Created: 2025-10-30
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -33,15 +18,10 @@ drive_sync_client handler for API operations.
|
||||
# =============================================
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from rich.console import Console
|
||||
from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console = Console()
|
||||
from aipass.cli.apps.modules import console
|
||||
from aipass.prax import logger
|
||||
|
||||
_BACKUP_ROOT = Path(__file__).resolve().parents[2] # src/aipass/backup/
|
||||
|
||||
@@ -103,6 +83,11 @@ except ImportError:
|
||||
get_file_tracker_stats = None # type: ignore
|
||||
_test_drive_connection = None # type: ignore
|
||||
|
||||
from aipass.backup.apps.handlers.operations.sync_test_ops import (
|
||||
create_sync_test_files,
|
||||
cleanup_sync_test_dir,
|
||||
)
|
||||
|
||||
|
||||
def _show_file_tracker_stats() -> bool:
|
||||
"""Display file tracker statistics."""
|
||||
@@ -163,34 +148,23 @@ def _test_drive_sync() -> bool:
|
||||
|
||||
def _run_sync_test() -> bool:
|
||||
"""Run a small test sync to verify Drive integration."""
|
||||
import shutil
|
||||
|
||||
console.print("[bold cyan]Drive Sync Test[/bold cyan]")
|
||||
console.print()
|
||||
|
||||
# Create test files
|
||||
test_dir = _BACKUP_ROOT / "backups" / "_sync_test"
|
||||
test_dir.mkdir(parents=True, exist_ok=True)
|
||||
# Create test files via handler
|
||||
setup = create_sync_test_files(_BACKUP_ROOT)
|
||||
if not setup["success"]:
|
||||
console.print(f"[red]Failed to create test files: {setup['error']}[/red]")
|
||||
return False
|
||||
|
||||
test_files = {
|
||||
"test_file_1.txt": "Hello from AIPass backup test",
|
||||
"test_file_2.json": '{"test": true, "timestamp": "' + datetime.now().isoformat() + '"}',
|
||||
"subdir/nested_file.txt": "Nested directory test",
|
||||
"subdir/deep/deeper_file.md": "# Deep nested test\nVerifying folder structure",
|
||||
}
|
||||
|
||||
for name, content in test_files.items():
|
||||
path = test_dir / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
|
||||
console.print(f" Created {len(test_files)} test files in {test_dir}")
|
||||
test_dir = setup["test_dir"]
|
||||
console.print(f" Created {setup['file_count']} test files in {test_dir}")
|
||||
|
||||
# Run sync
|
||||
sync = GoogleDriveSync()
|
||||
if not sync.authenticate():
|
||||
console.print("[red]Auth failed[/red]")
|
||||
shutil.rmtree(test_dir, ignore_errors=True)
|
||||
cleanup_sync_test_dir(test_dir)
|
||||
return False
|
||||
|
||||
console.print(f"\nScanning...")
|
||||
@@ -205,7 +179,7 @@ def _run_sync_test() -> bool:
|
||||
if not folder_id:
|
||||
error_msg = sync.last_error or "Unknown error"
|
||||
console.print(f"[red]FAILED: {error_msg}[/red]")
|
||||
shutil.rmtree(test_dir, ignore_errors=True)
|
||||
cleanup_sync_test_dir(test_dir)
|
||||
return False
|
||||
console.print(f" Drive folder ready: AIPass_Test")
|
||||
|
||||
@@ -225,7 +199,7 @@ def _run_sync_test() -> bool:
|
||||
|
||||
if result.get("error"):
|
||||
console.print(f"\n[red]FAILED: {result['error']}[/red]")
|
||||
shutil.rmtree(test_dir, ignore_errors=True)
|
||||
cleanup_sync_test_dir(test_dir)
|
||||
return False
|
||||
|
||||
console.print()
|
||||
@@ -243,8 +217,8 @@ def _run_sync_test() -> bool:
|
||||
else:
|
||||
console.print(f"[red]Dedup check failed: {len(files_to_upload2)} files flagged for re-upload[/red]")
|
||||
|
||||
# Cleanup local test dir
|
||||
shutil.rmtree(test_dir, ignore_errors=True)
|
||||
# Cleanup local test dir via handler
|
||||
cleanup_sync_test_dir(test_dir)
|
||||
console.print(f"\nCleaned up local test files")
|
||||
console.print(f"[dim]Test Drive folder 'AIPass_Test' left on Drive for inspection[/dim]")
|
||||
|
||||
@@ -423,6 +397,28 @@ def handle_command(args) -> bool:
|
||||
# CLI/EXECUTION
|
||||
# =============================================
|
||||
|
||||
def print_introspection():
|
||||
"""Display module introspection info."""
|
||||
console.print()
|
||||
console.print("google_drive_sync Module")
|
||||
console.print("Orchestrates Google Drive backup sync with two-phase upload pipeline")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/operations/")
|
||||
console.print(" - drive_sync_client.py (GoogleDriveSync — Drive API client and upload engine)")
|
||||
console.print(" - drive_sync_ops.py (clear_file_tracker — reset sync tracker cache)")
|
||||
console.print(" - drive_sync_ops.py (get_file_tracker_stats — tracker statistics)")
|
||||
console.print(" - drive_sync_ops.py (test_drive_connection — connectivity verification)")
|
||||
console.print(" - sync_test_ops.py (create_sync_test_files — generate test fixtures)")
|
||||
console.print(" - sync_test_ops.py (cleanup_sync_test_dir — remove test fixtures)")
|
||||
console.print(" handlers/json/")
|
||||
console.print(" - drive_sync_json.py (load_config — read module config JSON)")
|
||||
console.print(" - drive_sync_json.py (load_data — read module data JSON)")
|
||||
console.print(" handlers/utils/")
|
||||
console.print(" - backup_timestamps.py (get_timestamps, update_timestamp, format_age — backup timing)")
|
||||
console.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
@@ -550,4 +546,4 @@ EXAMPLES:
|
||||
sys.exit(0)
|
||||
else:
|
||||
console.print("Google Drive sync test failed")
|
||||
sys.exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: integrations.py - External integrations and backup protection
|
||||
# Date: 2025-11-29
|
||||
# =================== AIPass ====================
|
||||
# Name: integrations.py
|
||||
# Description: External integrations and backup protection
|
||||
# Version: 2.1.0
|
||||
# Category: backup_system
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v2.1.0 (2026-03-06): Adapted for AIPass public repo
|
||||
# * Removed shebang, sys.path manipulation, prax/cli imports
|
||||
# * Uses standard logging and rich console
|
||||
# * Relative handler imports, GoogleDriveSync via sibling module
|
||||
# - v2.0.1 (2025-11-29): UX FIX - Import error and missing help output
|
||||
# - v2.0.0 (2025-11-16): Created seed-compliant layout module
|
||||
# - v1.1.0 (2025-10-30): Original backup_integrations features
|
||||
# - v1.0.0 (2025-10-14): Initial extraction
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Follow seed 3-layer architecture
|
||||
# - Orchestrate workflows, delegate to handlers
|
||||
# - Import handlers, never implement business logic
|
||||
# Created: 2025-11-29
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -45,13 +30,10 @@ Architecture Pattern:
|
||||
import sys
|
||||
import os
|
||||
import stat
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console = Console()
|
||||
from aipass.cli.apps.modules import console
|
||||
from aipass.prax import logger
|
||||
|
||||
|
||||
def _header(text):
|
||||
@@ -278,6 +260,21 @@ def print_help():
|
||||
safe_print("="*70)
|
||||
safe_print("")
|
||||
|
||||
def print_introspection():
|
||||
"""Display module introspection info."""
|
||||
console.print()
|
||||
console.print("integrations Module")
|
||||
console.print("External integrations and backup protection (Drive sync, read-only)")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/utils/")
|
||||
console.print(" - system_utils.py (safe_print — terminal-safe output wrapper)")
|
||||
console.print(" handlers/operations/")
|
||||
console.print(" - integration_ops.py (sync_to_drive — Drive upload orchestration)")
|
||||
console.print(" - integration_ops.py (set_backup_readonly — read-only permission setter)")
|
||||
console.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""Display help when module is run directly."""
|
||||
import sys as _sys
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: reauth_drive.py - Google Drive Re-Authentication Module
|
||||
# Date: 2026-02-21
|
||||
# =================== AIPass ====================
|
||||
# Name: reauth_drive.py
|
||||
# Description: Google Drive Re-Authentication Module
|
||||
# Version: 1.2.0
|
||||
# Category: backup_system
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.2.0 (2026-03-06): Adapted for AIPass public repo
|
||||
# * Removed shebang, sys.path manipulation, prax/cli imports
|
||||
# * Uses standard logging and rich console
|
||||
# * Relative handler imports, Google API deps wrapped in try/except
|
||||
# - v1.1.0 (2026-02-21): Fixed handle_command() signature (command, args) → (args)
|
||||
# - v1.0.0 (2026-02-20): Initial version - standalone reauth utility
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Handlers implement logic, modules orchestrate
|
||||
# Created: 2026-02-21
|
||||
# Modified: 2026-03-09
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -26,13 +15,10 @@ Delegates implementation to reauth_handler.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console = Console()
|
||||
from aipass.cli.apps.modules import console
|
||||
from aipass.prax import logger
|
||||
|
||||
# Handler imports
|
||||
from aipass.backup.apps.handlers.utils.reauth_handler import reauth as _run_reauth
|
||||
@@ -139,6 +125,18 @@ def _execute_reauth() -> bool:
|
||||
return success
|
||||
|
||||
|
||||
def print_introspection():
|
||||
"""Display module introspection info."""
|
||||
console.print()
|
||||
console.print("reauth_drive Module")
|
||||
console.print("Google Drive re-authentication via console OAuth flow")
|
||||
console.print()
|
||||
console.print("Connected Handlers:")
|
||||
console.print(" handlers/utils/")
|
||||
console.print(" - reauth_handler.py (reauth — OAuth credential refresh and re-auth)")
|
||||
console.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] in ['--help', '-h', 'help']:
|
||||
print_help()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# CLI
|
||||
|
||||
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.
|
||||
**Purpose:** 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.
|
||||
**Module:** `aipass.cli`
|
||||
**Last Updated:** 2026-03-08
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -48,3 +50,29 @@ cli/
|
||||
|
||||
- `apps/modules/` — Public API. Import from here.
|
||||
- `apps/handlers/` — Internal implementation. Don't import directly.
|
||||
|
||||
## Commands / Usage
|
||||
|
||||
```bash
|
||||
drone @cli --help # Show services and Rich formatting showcase
|
||||
drone @cli --version # Show version
|
||||
drone @cli help # Same as --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Depends On
|
||||
- `aipass.prax` — Logging via `system_logger`
|
||||
- `rich` — Rich library for terminal formatting (Table, Panel, Columns, Text)
|
||||
- Python stdlib (`sys`, `importlib`, `pathlib`)
|
||||
|
||||
### Provides To
|
||||
- All modules — display formatting (headers, success/error/warning, section breaks)
|
||||
- All modules — operation templates (`operation_start`, `operation_complete`)
|
||||
- All modules — Rich console access
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2026-03-08*
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: cli.py - CLI Branch Entry Point [SHOWROOM]
|
||||
# Date: 2025-11-12
|
||||
# Version: 0.2.0
|
||||
# Category: cli
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v0.2.0 (2025-11-15): Added print_introspection() - the CLI showroom
|
||||
# - v0.1.0 (2025-11-12): Initial structure - service provider for display/output
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - SHOWROOM - demonstrates CLI capabilities and architecture
|
||||
# =================== AIPass ====================
|
||||
# Name: cli.py
|
||||
# Description: Entry point CLI for drone @cli — display service and Rich formatting
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-03-08
|
||||
# Modified: 2026-03-08
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: json_handler.py - JSON Auto-Creating Handler
|
||||
# Date: 2025-11-21
|
||||
# =================== AIPass ====================
|
||||
# Name: json_handler.py
|
||||
# Description: JSON auto-creating handler — manages CLI JSON files with templates and rotation
|
||||
# Version: 1.1.0
|
||||
# Category: cli/handlers/json
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v1.1.0 (2025-11-21): Refactored to comply with error handling
|
||||
# - v1.0.0 (2025-11-13): Initial JSON auto-creation system
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - Pure functions with proper error raising
|
||||
# - No Prax imports (handler tier 3)
|
||||
# Created: 2025-11-13
|
||||
# Modified: 2025-11-21
|
||||
# =============================================
|
||||
|
||||
"""JSON Auto-Creating Handler - manages CLI JSON files with templates and auto-rotation."""
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: display.py - CLI Display Module
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: display.py
|
||||
# Description: CLI Display Module — public API for Rich terminal output formatting
|
||||
# Version: 0.4.0
|
||||
# Category: cli/modules
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v0.4.0 (2025-11-15): Replaced argparse help with Rich formatted help (SEED pattern)
|
||||
# - v0.3.0 (2025-11-15): Restructured to follow SEED module pattern
|
||||
# - v0.2.0 (2025-11-12): Implemented Rich library formatting
|
||||
# - v0.1.0 (2025-11-12): Public API for display functions
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - PUBLIC API - thin wrapper over handler implementation
|
||||
# - Follows SEED module pattern (introspection/help/command handling)
|
||||
# Created: 2025-11-12
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -40,6 +29,9 @@ from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.columns import Columns
|
||||
|
||||
# NOTE: Cannot import prax here — circular import (prax depends on cli)
|
||||
# from aipass.prax import logger
|
||||
|
||||
# Initialize Rich console (lowercase follows service instance pattern)
|
||||
CONSOLE = Console() # Internal constant
|
||||
console = CONSOLE # Primary export (lowercase service instance pattern)
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: templates.py - CLI Templates Module
|
||||
# Date: 2025-11-15
|
||||
# =================== AIPass ====================
|
||||
# Name: templates.py
|
||||
# Description: CLI Templates Module — reusable operation output patterns
|
||||
# Version: 0.3.0
|
||||
# Category: cli/modules
|
||||
#
|
||||
# CHANGELOG (Max 5 entries):
|
||||
# - v0.3.0 (2025-11-15): Restructured to follow SEED module pattern
|
||||
# - v0.2.0 (2025-11-12): Implemented Rich library formatting
|
||||
# - v0.1.0 (2025-11-12): Public API for output templates
|
||||
#
|
||||
# CODE STANDARDS:
|
||||
# - PUBLIC API - reusable output patterns
|
||||
# - Follows SEED module pattern (introspection/help/command handling)
|
||||
# Created: 2025-11-12
|
||||
# Modified: 2025-11-15
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
@@ -32,6 +22,8 @@ from typing import Dict, Any, Optional, List
|
||||
|
||||
# Import console from CLI display module (using our own service!)
|
||||
from aipass.cli.apps.modules.display import console as CONSOLE
|
||||
# NOTE: Cannot import prax here — circular import (prax depends on cli)
|
||||
# from aipass.prax import logger
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user