Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions notes_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,42 @@ async def _run_notes_script(script: str, timeout: float = 10) -> str:


async def get_recent_notes(count: int = 10) -> list[dict]:
"""Get most recent notes (title + creation date)."""
"""Get most recent notes (title + creation date).

Individual notes are read defensively, because an unguarded property
access aborts the entire script and one unreadable note would otherwise
cost every note.

"folder" is best-effort and is currently empty in practice: on recent
macOS, `name of container of note` fails with -1728 for every note (the
container resolves, but as a bare `item` whose name is not exposed). Real
folder names are only reachable by walking `every note of folder`, which
means scanning the whole library on every call — not worth it while no
caller reads the field. Guarding the lookup rather than dropping it keeps
this self-healing if Apple restores the property.
"""
# Bound the scan so a run of unreadable notes can't walk a whole library.
scan_limit = count + 50
script = f'''
tell application "Notes"
set output to ""
set allNotes to every note
set limit to count of allNotes
if limit > {count} then set limit to {count}
repeat with i from 1 to limit
set n to item i of allNotes
set nName to name of n
set nDate to creation date of n as string
set nFolder to name of container of n
set output to output & nName & "|||" & nDate & "|||" & nFolder & linefeed
set scanCount to count of allNotes
if scanCount > {scan_limit} then set scanCount to {scan_limit}
set collected to 0
repeat with i from 1 to scanCount
if collected >= {count} then exit repeat
try
set n to item i of allNotes
set nName to name of n
set nDate to creation date of n as string
set nFolder to ""
try
set nFolder to name of container of n
end try
set output to output & nName & "|||" & nDate & "|||" & nFolder & linefeed
set collected to collected + 1
end try
end repeat
return output
end tell
Expand Down
43 changes: 34 additions & 9 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@
from actions import execute_action, monitor_build, open_terminal, open_browser, open_claude_in_project, _generate_project_name, prompt_existing_terminal, applescript_escape
from work_mode import WorkSession, is_casual_question
from screen import get_active_windows, take_screenshot, describe_screen, format_windows_for_context
from calendar_access import get_todays_events, get_upcoming_events, get_next_event, format_events_for_context, format_schedule_summary, refresh_cache as refresh_calendar_cache
from calendar_access import get_todays_events, get_upcoming_events, get_next_event, get_calendar_names, format_events_for_context, format_schedule_summary, refresh_cache as refresh_calendar_cache
from mail_access import get_unread_count, get_unread_messages, get_recent_messages, search_mail, read_message, format_unread_summary, format_messages_for_context, format_messages_for_voice
from memory import (
remember, recall, get_open_tasks, create_task, complete_task, search_tasks,
create_note, search_notes, get_tasks_for_date, build_memory_context,
format_tasks_for_voice, extract_memories, get_important_memories,
)
from notes_access import get_recent_notes, read_note, search_notes_apple, create_apple_note
from notes_access import get_recent_notes, get_note_folders, read_note, search_notes_apple, create_apple_note
from dispatch_registry import DispatchRegistry
from planner import TaskPlanner, detect_planning_mode, BYPASS_PHRASES

Expand All @@ -67,6 +67,9 @@
USER_NAME = os.getenv("USER_NAME", "sir")
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
_SKIP_PERMISSIONS = os.getenv("JARVIS_SKIP_PERMISSIONS", "true").lower() not in ("0", "false", "no")
# Per-integration ceiling for the /api/settings/status probes, so the settings
# panel stays responsive when Calendar/Mail/Notes are slow or unauthorized.
_SETTINGS_PROBE_TIMEOUT = 8.0

DESKTOP_PATH = Path.home() / "Desktop"

Expand Down Expand Up @@ -2530,18 +2533,40 @@ async def api_test_fish(body: KeyTest):
except Exception as e:
return {"valid": False, "error": str(e)[:200]}

async def _probe_integration(coro, ok=lambda _: True, timeout: float = _SETTINGS_PROBE_TIMEOUT) -> bool:
"""Return whether a macOS integration answered within the time budget.

Individual osascript calls are already bounded, but the totals are not, and
before the user grants TCC permission they block on a system dialog. The
settings panel only needs a reachable/unreachable flag, so cap the wait and
report a slow integration the same as a broken one.
"""
try:
return bool(ok(await asyncio.wait_for(coro, timeout=timeout)))
except Exception:
return False


@app.get("/api/settings/status")
async def api_settings_status():
import shutil as _shutil
_, env_dict = _read_env()
claude_installed = _shutil.which("claude") is not None
calendar_ok = mail_ok = notes_ok = False
try: await get_todays_events(); calendar_ok = True
except Exception: pass
try: await get_unread_count(); mail_ok = True
except Exception: pass
try: await get_recent_notes(count=1); notes_ok = True
except Exception: pass
# Probed concurrently — they touch three separate apps and don't depend on
# each other, so the endpoint costs one budget rather than three.
#
# Calendar and Notes are probed by listing containers, and their empty
# result is read as a failure. Both modules answer an unreachable app with
# the same empty value they use for "nothing found", so a probe that only
# checked for a raised exception would report an app as healthy while it
# was in fact failing. Listing also avoids the expensive paths: fetching
# today's events fans a cold cache out over every calendar in batches of
# two, which alone outruns the budget on a well-populated account.
calendar_ok, mail_ok, notes_ok = await asyncio.gather(
_probe_integration(get_calendar_names(), ok=bool),
_probe_integration(get_unread_count()),
_probe_integration(get_note_folders(), ok=bool),
)
memory_count = task_count = 0
try: memory_count = len(get_important_memories(limit=9999))
except Exception: pass
Expand Down