Class: Pikuri::Thunderbird::CalendarSearch

Inherits:
Pikuri::Tool
  • Object
show all
Defined in:
lib/pikuri/thunderbird/calendar_search.rb

Overview

The thunderbird_calendar_search tool — search local + cached Thunderbird calendars by title/description/location. Inbound-only; its read half is thunderbird_calendar_read.

Recall-first (words OR-matched, ranked by matched-word count) on purpose, mirroring MailSearch: tighten it to AND and the model — primed by the forgiving mail search — reads the sparser hits as "no such event".

Empty-corpus guidance (Q6)

Thunderbird keeps a CalDAV calendar's events in memory only unless "Offline Support" is enabled, so a calendar can look full in the UI yet have nothing on disk. When no events are cached, this tool returns a self-fixing observation telling the user the one-click fix rather than a bare "no results" — and the same fact rides the description, so the LLM can advise proactively before any empty search.

Sharing: P_shared_locked — no state of its own, and the Calendar backend it queries locks; see that class's == Sharing.

Constant Summary collapse

DEFAULT_LIMIT =
15
MAX_LIMIT =
50
OFFLINE_HINT =

Returns the enable-Offline-Support guidance, reused by the description and the empty-corpus observation.

Returns:

  • (String)

    the enable-Offline-Support guidance, reused by the description and the empty-corpus observation.

'No calendar events are cached to disk. If Thunderbird shows events but ' \
'search finds none, enable Offline Support on each calendar ' \
'(right-click the calendar → Properties → Offline Support) so its events ' \
'are stored locally and become searchable.'
DESCRIPTION =

Returns opencode-shape description.

Returns:

  • (String)

    opencode-shape description.

<<~DESC
  Search the user's local and cached Thunderbird calendars by title, description or location.

  Usage:
  - Free-text query and/or a date range — both optional; supply at least one.
  - To list everything on a day or in a span (e.g. "tomorrow's meetings"), pass a broad or empty query with after/before; a single day is after=before=that date.
  - Each query word is matched independently (across title, description, and location) and events matching more of the words rank first — so extra words broaden the search and surface partial matches, they don't require every word. Matching ignores accents and case. To narrow, add a date range rather than more words.
  - Optionally restrict to recurring events (a repeating series) or single one-off events with the recurrence filter; omit it to get both.
  - Reads Thunderbird's on-disk calendar store — it never connects to any calendar server.
  - Only events cached to disk are visible. A CalDAV calendar keeps events in memory unless its "Offline Support" is enabled (right-click the calendar → Properties → Offline Support); if a search comes back empty but the calendar looks full, that setting is why.
  - Read an event in full with thunderbird_calendar_read.
DESC

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(backend:) ⇒ CalendarSearch

Parameters:

  • backend (Calendar)

    the calendar backend.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/pikuri/thunderbird/calendar_search.rb', line 52

def initialize(backend:)
  @backend = backend
  super(
    name: 'thunderbird_calendar_search',
    description: DESCRIPTION,
    parameters: Parameters.build { |p|
      p.optional_string :query, 'Words to find in event title/description/location, e.g. "standup". Omit to list everything in the date range.'
      p.optional_string :after, 'Only events on/after this date, e.g. "2026-01-01".'
      p.optional_string :before, 'Only events on/before this date, e.g. "2026-12-31".'
      p.optional_enum :recurrence, 'Restrict by repetition: "recurring" for repeating series only, "single" for one-off events only. Omit for both.', values: %w[recurring single]
      p.optional_integer :limit, "Max events (default #{DEFAULT_LIMIT}, max #{MAX_LIMIT}), e.g. 15."
    },
    execute: lambda { |query: nil, after: nil, before: nil, recurrence: nil, limit: DEFAULT_LIMIT|
      CalendarSearch.run_search(backend: @backend, query:, after:, before:, recurrence:, limit:)
    },
    trifecta_legs: Pikuri::Thunderbird::INBOUND_LEGS
  )
end

Class Method Details

.run_search(backend:, query:, after:, before:, recurrence: nil, limit:) ⇒ String

Parameters:

  • recurrence (String, nil) (defaults to: nil)

    "recurring", "single", or nil for both (mapped to the backend's tri-state recurring: flag).

Returns:

  • (String)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/pikuri/thunderbird/calendar_search.rb', line 74

def self.run_search(backend:, query:, after:, before:, recurrence: nil, limit:)
  # Refuse a match-everything call: with no query and no date bound this
  # would dump every cached event. "What's on my calendar" should name a
  # span — steer the model to add after/before (e.g. this week). A lone
  # recurrence filter is still too broad to stand in for a span.
  if [query, after, before].all? { |v| v.to_s.strip.empty? }
    return 'Error: give a query and/or a date range (after/before) to search for'
  end

  limit = limit.to_i.clamp(1, MAX_LIMIT)
  recurring = { 'recurring' => true, 'single' => false }[recurrence]
  # Fetch one past the cap so a full page is distinguishable from an
  # exhausted calendar (see {MailSearch.run_search}); limit+1 back means
  # more exist and {header} says so, rather than let the model read a
  # full page as the whole calendar.
  hits = backend.search(query:, limit: limit + 1, recurring:,
                        after: DateHelpers.parse_after(after), before: DateHelpers.parse_before(before))
  return offline_or_no_match(backend, query) if hits.empty?

  has_more = hits.size > limit
  hits = hits.first(limit)
  [header(shown: hits.size, has_more:), *hits.map { |h| render(h) }].join("\n\n")
rescue ArgumentError => e
  "Error: bad date filter (#{e.message})."
end