feat(heartbeat): save emails to KB + fix memory symlink access

- heartbeat saves unread whitelisted emails via email_process --save --json
- fix: add --add-dir so Claude CLI subprocess can access memory/ symlink
- email_check/process: use BODY.PEEK[] to avoid marking emails as read
- email_process: simplify credential loading via credential_store only
- config: heartbeat interval 30→120min, quiet hours end 08→07

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 12:10:53 +00:00
parent 08c330a371
commit 19e253ec43
5 changed files with 72 additions and 61 deletions

View File

@@ -144,22 +144,43 @@ def _is_quiet_hour(hour: int, quiet_hours: tuple[int, int]) -> bool:
def _check_email(state: dict) -> str | None:
"""Check for new emails via tools/email_check.py. Parses JSON output."""
script = TOOLS_DIR / "email_check.py"
if not script.exists():
"""Save unread whitelisted emails as notes via tools/email_process.py.
Uses --save --json to process and mark as read only after saving.
Falls back to email_check.py for reporting if no emails to save.
"""
process_script = TOOLS_DIR / "email_process.py"
check_script = TOOLS_DIR / "email_check.py"
# First: save unread emails as notes
if process_script.exists():
try:
result = subprocess.run(
["python3", str(process_script), "--save", "--json"],
capture_output=True, text=True, timeout=60,
cwd=str(PROJECT_ROOT)
)
if result.returncode == 0 and result.stdout.strip():
saved = json.loads(result.stdout.strip())
ok_saves = [r for r in saved if r.get("ok")]
if ok_saves:
subjects = [r.get("subject", "?") for r in ok_saves[:5]]
return f"Email: {len(ok_saves)} salvate ({', '.join(subjects)})"
except Exception as e:
log.warning("Email process failed: %s", e)
# Fallback: just report unread count (without marking as read)
if not check_script.exists():
return None
try:
result = subprocess.run(
["python3", str(script)],
["python3", str(check_script)],
capture_output=True, text=True, timeout=30,
cwd=str(PROJECT_ROOT)
)
if result.returncode != 0:
return None
output = result.stdout.strip()
if not output:
return None
data = json.loads(output)
data = json.loads(result.stdout.strip())
if not data.get("ok"):
return None
count = data.get("unread_count", 0)
@@ -167,14 +188,7 @@ def _check_email(state: dict) -> str | None:
return None
emails = data.get("emails", [])
subjects = [e.get("subject", "?") for e in emails[:5]]
subject_list = ", ".join(subjects)
return f"Email: {count} necitite ({subject_list})"
except json.JSONDecodeError:
# Fallback: treat as plain text
output = result.stdout.strip()
if output and output != "0":
return f"Email: {output}"
return None
return f"Email: {count} necitite ({', '.join(subjects)})"
except Exception as e:
log.warning("Email check failed: %s", e)
return None
@@ -421,10 +435,13 @@ def _run_claude_extra(hb_config: dict, python_results: list[str],
)
prompt = "\n\n".join(context_parts)
# Resolve symlink for memory/ so Claude CLI can access it from outside project root
memory_real = (PROJECT_ROOT / "memory").resolve()
cmd = [
CLAUDE_BIN, "-p", prompt,
"--model", model,
"--output-format", "json",
"--add-dir", str(memory_real),
]
try: