godspeed app sync

This commit is contained in:
Tim Bendt
2025-08-20 08:30:54 -04:00
parent ca6e4cdf5d
commit c46d53b261
17 changed files with 3170 additions and 45 deletions

View File

@@ -1,8 +1,11 @@
import click
import asyncio
import os
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn
import json
import time
from datetime import datetime, timedelta
from pathlib import Path
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn
from src.utils.mail_utils.helpers import ensure_directory_exists
from src.utils.calendar_utils import save_events_to_vdir, save_events_to_file
@@ -21,6 +24,180 @@ from src.services.microsoft_graph.mail import (
process_outbox_async,
)
from src.services.microsoft_graph.auth import get_access_token
from src.services.godspeed.client import GodspeedClient
from src.services.godspeed.sync import GodspeedSync
# Timing state management
def get_sync_state_file():
"""Get the path to the sync state file."""
return os.path.expanduser("~/.local/share/gtd-terminal-tools/sync_state.json")
def load_sync_state():
"""Load the sync state from file."""
state_file = get_sync_state_file()
if os.path.exists(state_file):
try:
with open(state_file, "r") as f:
return json.load(f)
except Exception:
pass
return {
"last_godspeed_sync": 0,
"last_sweep_date": None,
"sweep_completed_today": False,
}
def save_sync_state(state):
"""Save the sync state to file."""
state_file = get_sync_state_file()
os.makedirs(os.path.dirname(state_file), exist_ok=True)
with open(state_file, "w") as f:
json.dump(state, f, indent=2)
def should_run_godspeed_sync():
"""Check if Godspeed sync should run (every 15 minutes)."""
state = load_sync_state()
current_time = time.time()
last_sync = state.get("last_godspeed_sync", 0)
return current_time - last_sync >= 900 # 15 minutes in seconds
def should_run_sweep():
"""Check if sweep should run (once after 6pm each day)."""
state = load_sync_state()
current_time = datetime.now()
# Check if it's after 6 PM
if current_time.hour < 18:
return False
# Check if we've already swept today
today_str = current_time.strftime("%Y-%m-%d")
last_sweep_date = state.get("last_sweep_date")
return last_sweep_date != today_str
def get_godspeed_sync_directory():
"""Get Godspeed sync directory from environment or default."""
sync_dir = os.getenv("GODSPEED_SYNC_DIR")
if sync_dir:
return Path(sync_dir)
# Default to ~/Documents/Godspeed or ~/.local/share/gtd-terminal-tools/godspeed
home = Path.home()
# Try Documents first
docs_dir = home / "Documents" / "Godspeed"
if docs_dir.parent.exists():
return docs_dir
# Fall back to data directory
data_dir = home / ".local" / "share" / "gtd-terminal-tools" / "godspeed"
return data_dir
def get_godspeed_credentials():
"""Get Godspeed credentials from environment."""
email = os.getenv("GODSPEED_EMAIL")
password = os.getenv("GODSPEED_PASSWORD")
token = os.getenv("GODSPEED_TOKEN")
return email, password, token
async def run_godspeed_sync(progress=None):
"""Run Godspeed bidirectional sync."""
try:
email, password, token = get_godspeed_credentials()
if not (token or (email and password)):
if progress:
progress.console.print(
"[yellow]⚠️ Skipping Godspeed sync: No credentials configured[/yellow]"
)
return False
sync_dir = get_godspeed_sync_directory()
if progress:
progress.console.print(
f"[cyan]🔄 Running Godspeed sync to {sync_dir}...[/cyan]"
)
client = GodspeedClient(email=email, password=password, token=token)
sync_engine = GodspeedSync(client, sync_dir)
sync_engine.sync_bidirectional()
# Update sync state
state = load_sync_state()
state["last_godspeed_sync"] = time.time()
save_sync_state(state)
if progress:
progress.console.print("[green]✅ Godspeed sync completed[/green]")
return True
except Exception as e:
if progress:
progress.console.print(f"[red]❌ Godspeed sync failed: {e}[/red]")
return False
async def run_task_sweep(progress=None):
"""Run task sweep from notes directory to Godspeed inbox."""
try:
from src.cli.godspeed import TaskSweeper
notes_dir_env = os.getenv("NOTES_DIR")
if not notes_dir_env:
if progress:
progress.console.print(
"[yellow]⚠️ Skipping task sweep: $NOTES_DIR not configured[/yellow]"
)
return False
notes_dir = Path(notes_dir_env)
if not notes_dir.exists():
if progress:
progress.console.print(
f"[yellow]⚠️ Skipping task sweep: Notes directory does not exist: {notes_dir}[/yellow]"
)
return False
godspeed_dir = get_godspeed_sync_directory()
if progress:
progress.console.print(
f"[cyan]🧹 Running task sweep from {notes_dir} to {godspeed_dir}...[/cyan]"
)
sweeper = TaskSweeper(notes_dir, godspeed_dir, dry_run=False)
result = sweeper.sweep_tasks()
# Update sweep state
state = load_sync_state()
state["last_sweep_date"] = datetime.now().strftime("%Y-%m-%d")
save_sync_state(state)
if result["swept_tasks"] > 0:
if progress:
progress.console.print(
f"[green]✅ Task sweep completed: {result['swept_tasks']} tasks swept[/green]"
)
else:
if progress:
progress.console.print(
"[green]✅ Task sweep completed: No tasks to sweep[/green]"
)
return True
except Exception as e:
if progress:
progress.console.print(f"[red]❌ Task sweep failed: {e}[/red]")
return False
# Function to create Maildir structure
@@ -362,6 +539,36 @@ async def _sync_outlook_data(
notify_new_emails(new_message_count, org)
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
# Stage 3: Run Godspeed operations based on timing
progress.console.print(
"\n[bold cyan]Step 3: Running Godspeed operations...[/bold cyan]"
)
# Check if Godspeed sync should run (every 15 minutes)
if should_run_godspeed_sync():
await run_godspeed_sync(progress)
else:
progress.console.print("[dim]⏭️ Skipping Godspeed sync (not due yet)[/dim]")
# Check if task sweep should run (once after 6pm daily)
if should_run_sweep():
await run_task_sweep(progress)
else:
current_hour = datetime.now().hour
if current_hour < 18:
progress.console.print(
"[dim]⏭️ Skipping task sweep (before 6 PM)[/dim]"
)
else:
progress.console.print(
"[dim]⏭️ Skipping task sweep (already completed today)[/dim]"
)
progress.console.print(
"[bold green]Step 3: Godspeed operations completed.[/bold green]"
)
click.echo("Sync complete.")
@@ -656,59 +863,43 @@ async def daemon_mode(
pending_email_count = len(pending_emails)
outbox_changes = pending_email_count > 0
# Check Godspeed operations
godspeed_sync_due = should_run_godspeed_sync()
sweep_due = should_run_sweep()
# Determine what changed and show appropriate status
if mail_changes and calendar_changes and outbox_changes:
console.print(
create_status_display(
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Calendar: {calendar_change_desc} | Outbox: {pending_email_count} pending. Starting sync...",
"yellow",
changes_detected = (
mail_changes
or calendar_changes
or outbox_changes
or godspeed_sync_due
or sweep_due
)
if changes_detected:
change_parts = []
if mail_changes:
change_parts.append(
f"Mail: Remote {remote_message_count}, Local {local_message_count}"
)
)
elif mail_changes and calendar_changes:
if calendar_changes:
change_parts.append(f"Calendar: {calendar_change_desc}")
if outbox_changes:
change_parts.append(f"Outbox: {pending_email_count} pending")
if godspeed_sync_due:
change_parts.append("Godspeed sync due")
if sweep_due:
change_parts.append("Task sweep due")
console.print(
create_status_display(
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Calendar: {calendar_change_desc}. Starting sync...",
"yellow",
)
)
elif mail_changes and outbox_changes:
console.print(
create_status_display(
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Outbox: {pending_email_count} pending. Starting sync...",
"yellow",
)
)
elif calendar_changes and outbox_changes:
console.print(
create_status_display(
f"Changes detected! Calendar: {calendar_change_desc} | Outbox: {pending_email_count} pending. Starting sync...",
"yellow",
)
)
elif mail_changes:
console.print(
create_status_display(
f"New messages detected! Remote: {remote_message_count}, Local: {local_message_count}. Starting sync...",
"yellow",
)
)
elif calendar_changes:
console.print(
create_status_display(
f"Calendar changes detected! {calendar_change_desc}. Starting sync...",
"yellow",
)
)
elif outbox_changes:
console.print(
create_status_display(
f"Outbound emails detected! {pending_email_count} emails pending. Starting sync...",
f"Changes detected! {' | '.join(change_parts)}. Starting sync...",
"yellow",
)
)
# Sync if any changes detected
if mail_changes or calendar_changes or outbox_changes:
if changes_detected:
await _sync_outlook_data(
dry_run,
vdir,
@@ -732,6 +923,23 @@ async def daemon_mode(
status_parts.append(f"Outbox: {pending_email_count} pending")
# Add Godspeed status
state = load_sync_state()
last_godspeed = state.get("last_godspeed_sync", 0)
minutes_since_godspeed = int((time.time() - last_godspeed) / 60)
status_parts.append(f"Godspeed: {minutes_since_godspeed}m ago")
last_sweep = state.get("last_sweep_date")
if last_sweep == datetime.now().strftime("%Y-%m-%d"):
status_parts.append("Sweep: done today")
else:
current_hour = datetime.now().hour
if current_hour >= 18:
status_parts.append("Sweep: due")
else:
hours_until_sweep = 18 - current_hour
status_parts.append(f"Sweep: in {hours_until_sweep}h")
console.print(
create_status_display(
f"No changes detected ({', '.join(status_parts)})",