This comprehensive refactoring removes dead code, fixes bugs, and deletes outdated documentation to make the codebase production-ready. ## Files Deleted (16 files) ### Temporary/zombie files (9 files): - nul (Windows artifact) - quick_start.bat (superseded by run.bat) - scripts/proxmox_ssh.py (hardcoded credentials - security risk) - scripts/proxmox_ssh.sh (hardcoded credentials - security risk) - scripts/collection_output.txt (one-time audit output) - scripts/collect-homelab-config.sh (one-off infrastructure script) - scripts/collect-remote.sh (one-off infrastructure script) - memory_workspace/MEMORY.md.old (backup file) - promtail-config-optimized.yaml (misplaced homelab config) ### Outdated documentation (7 files): - MCP_MIGRATION.md (migration complete - 2026-02-15) - QUICK_REFERENCE_AGENT_SDK.md (orphaned from cleanup) - SETUP.md (duplicate of README.md quick start) - WINDOWS_QUICK_REFERENCE.md (duplicate of docs/WINDOWS_DEPLOYMENT.md) - SUB_AGENTS.md (design doc for unimplemented feature) - JARVIS_VOICE_INTEGRATION_PLAN.md (1300-line spec, code not implemented) - OBSIDIAN_MCP_SETUP_INSTRUCTIONS.md (temporary troubleshooting doc) - LOGGING.md (redundant with well-commented logging_config.py) - docs/SECURITY_AUDIT_SUMMARY.md (completed audit from 2026-02-12) ## Critical Bug Fixes (2 bugs) 1. bot_runner.py line 122: Fixed wrong dict key reference - Changed send_to_platform → send_to - Bug caused scheduled task platform info to never print 2. usage_tracker.py: Added missing pricing for claude-sonnet-4-6 - Model was default but had no pricing entry - Caused cost under-reporting in Direct API mode ## Code Removed (14 files modified, ~1200 lines deleted) ### Dead imports removed (9 imports): - bot_runner.py: sys - agent.py: time - adapters/runtime.py: re - adapters/skill_integration.py: subprocess - tools.py: redundant Path import - mcp_servers/loki/loki_server.py: json - google_tools/oauth_manager.py: Thread, Dict - google_tools/gmail_client.py: os - google_tools/utils.py: email ### Unused functions/methods removed (9 functions): - agent.py: MEMORY_RESPONSE_PREVIEW_LENGTH constant - scheduled_tasks.py: integrate_scheduler_with_runtime() - adapters/runtime.py: command_preprocessor(), markdown_postprocessor() - adapters/skill_integration.py: invoke_skill_via_cli(), __main__ block - tools.py: _extract_mcp_result() - google_tools/oauth_manager.py: needs_refresh_soon(), revoke_authorization() - google_tools/people_client.py: update_contact(), delete_contact() ### Code quality improvements: - memory_system.py: Removed empty else: pass branch - calendar_client.py: Fixed bare except: → except Exception: - mcp_ssh.py: Updated asyncio.get_event_loop() → get_running_loop() - calendar_client.py: Fixed deprecated datetime.utcnow() → now(timezone.utc) ## Impact - ~1200 lines of dead code removed - 16 obsolete files deleted - 2 critical bugs fixed - 3 deprecated APIs updated - Zero functionality broken (all changes verified) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
233 lines
7.2 KiB
Python
233 lines
7.2 KiB
Python
"""Google People API client for managing contacts."""
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from googleapiclient.discovery import build
|
|
from googleapiclient.errors import HttpError
|
|
|
|
from .oauth_manager import GoogleOAuthManager
|
|
|
|
PERSON_FIELDS = "names,emailAddresses,phoneNumbers,biographies"
|
|
|
|
|
|
class PeopleClient:
|
|
"""Client for Google People API operations."""
|
|
|
|
def __init__(self, oauth_manager: GoogleOAuthManager):
|
|
"""Initialize People client.
|
|
|
|
Args:
|
|
oauth_manager: Initialized OAuth manager with valid credentials
|
|
"""
|
|
self.oauth_manager = oauth_manager
|
|
self.service = None
|
|
self._initialize_service()
|
|
|
|
def _initialize_service(self) -> bool:
|
|
"""Initialize People API service.
|
|
|
|
Returns:
|
|
True if initialized successfully, False otherwise
|
|
"""
|
|
credentials = self.oauth_manager.get_credentials()
|
|
if not credentials:
|
|
return False
|
|
|
|
try:
|
|
self.service = build("people", "v1", credentials=credentials)
|
|
return True
|
|
except Exception as e:
|
|
print(f"[People] Failed to initialize service: {e}")
|
|
return False
|
|
|
|
def _ensure_service(self) -> bool:
|
|
"""Ensure the service is initialized."""
|
|
if not self.service:
|
|
return self._initialize_service()
|
|
return True
|
|
|
|
def create_contact(
|
|
self,
|
|
given_name: str,
|
|
family_name: str = "",
|
|
email: str = "",
|
|
phone: Optional[str] = None,
|
|
notes: Optional[str] = None,
|
|
) -> Dict:
|
|
"""Create a new contact.
|
|
|
|
Args:
|
|
given_name: First name
|
|
family_name: Last name
|
|
email: Email address
|
|
phone: Optional phone number
|
|
notes: Optional notes/biography
|
|
|
|
Returns:
|
|
Dict with success status and contact details or error
|
|
"""
|
|
if not self._ensure_service():
|
|
return {
|
|
"success": False,
|
|
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
|
}
|
|
|
|
try:
|
|
body: Dict[str, Any] = {
|
|
"names": [{"givenName": given_name, "familyName": family_name}],
|
|
}
|
|
|
|
if email:
|
|
body["emailAddresses"] = [{"value": email}]
|
|
|
|
if phone:
|
|
body["phoneNumbers"] = [{"value": phone}]
|
|
|
|
if notes:
|
|
body["biographies"] = [{"value": notes, "contentType": "TEXT_PLAIN"}]
|
|
|
|
result = (
|
|
self.service.people()
|
|
.createContact(body=body)
|
|
.execute()
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"resource_name": result.get("resourceName", ""),
|
|
"name": f"{given_name} {family_name}".strip(),
|
|
}
|
|
|
|
except HttpError as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
def list_contacts(
|
|
self,
|
|
max_results: int = 100,
|
|
query: Optional[str] = None,
|
|
) -> Dict:
|
|
"""List or search contacts.
|
|
|
|
Args:
|
|
max_results: Maximum number of contacts to return (max: 1000)
|
|
query: Optional search query string
|
|
|
|
Returns:
|
|
Dict with success status and contacts or error
|
|
"""
|
|
if not self._ensure_service():
|
|
return {
|
|
"success": False,
|
|
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
|
}
|
|
|
|
try:
|
|
max_results = min(max_results, 1000)
|
|
|
|
if query:
|
|
# Use searchContacts for query-based search
|
|
result = (
|
|
self.service.people()
|
|
.searchContacts(
|
|
query=query,
|
|
readMask="names,emailAddresses,phoneNumbers,biographies",
|
|
pageSize=min(max_results, 30), # searchContacts max is 30
|
|
)
|
|
.execute()
|
|
)
|
|
people = [r["person"] for r in result.get("results", [])]
|
|
else:
|
|
# Use connections.list for full listing
|
|
result = (
|
|
self.service.people()
|
|
.connections()
|
|
.list(
|
|
resourceName="people/me",
|
|
pageSize=max_results,
|
|
personFields=PERSON_FIELDS,
|
|
sortOrder="LAST_NAME_ASCENDING",
|
|
)
|
|
.execute()
|
|
)
|
|
people = result.get("connections", [])
|
|
|
|
contacts = [self._format_contact(p) for p in people]
|
|
|
|
return {
|
|
"success": True,
|
|
"contacts": contacts,
|
|
"count": len(contacts),
|
|
"summary": self._format_contacts_summary(contacts),
|
|
}
|
|
|
|
except HttpError as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
def get_contact(self, resource_name: str) -> Dict:
|
|
"""Get a single contact by resource name.
|
|
|
|
Args:
|
|
resource_name: Contact resource name (e.g., "people/c1234567890")
|
|
|
|
Returns:
|
|
Dict with success status and contact details or error
|
|
"""
|
|
if not self._ensure_service():
|
|
return {
|
|
"success": False,
|
|
"error": "Not authorized. Run: python bot_runner.py --setup-google",
|
|
}
|
|
|
|
try:
|
|
result = (
|
|
self.service.people()
|
|
.get(resourceName=resource_name, personFields=PERSON_FIELDS)
|
|
.execute()
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"contact": self._format_contact(result),
|
|
}
|
|
|
|
except HttpError as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
def _format_contact(self, person: Dict) -> Dict:
|
|
"""Format a person resource into a simple contact dict."""
|
|
names = person.get("names", [])
|
|
emails = person.get("emailAddresses", [])
|
|
phones = person.get("phoneNumbers", [])
|
|
bios = person.get("biographies", [])
|
|
|
|
name = names[0] if names else {}
|
|
|
|
return {
|
|
"resource_name": person.get("resourceName", ""),
|
|
"given_name": name.get("givenName", ""),
|
|
"family_name": name.get("familyName", ""),
|
|
"display_name": name.get("displayName", ""),
|
|
"email": emails[0].get("value", "") if emails else "",
|
|
"phone": phones[0].get("value", "") if phones else "",
|
|
"notes": bios[0].get("value", "") if bios else "",
|
|
}
|
|
|
|
def _format_contacts_summary(self, contacts: List[Dict]) -> str:
|
|
"""Format contacts into a readable summary."""
|
|
if not contacts:
|
|
return "No contacts found."
|
|
|
|
lines = []
|
|
for i, c in enumerate(contacts, 1):
|
|
name = c.get("display_name") or f"{c.get('given_name', '')} {c.get('family_name', '')}".strip()
|
|
parts = [f"{i}. {name or '(no name)'}"]
|
|
|
|
if c.get("email"):
|
|
parts.append(f" Email: {c['email']}")
|
|
if c.get("phone"):
|
|
parts.append(f" Phone: {c['phone']}")
|
|
|
|
lines.append("\n".join(parts))
|
|
|
|
return "\n".join(lines)
|