flycal-cli 0.7.4 → 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -174
- data/bin/flycal +2 -2
- data/lib/flycal/cache_annotator.rb +83 -0
- data/lib/flycal/cache_key.rb +82 -0
- data/lib/flycal/calendar_query.rb +60 -0
- data/lib/flycal/calendar_service.rb +115 -0
- data/lib/{flycal_cli/cli.rb → flycal/cli/app.rb} +320 -250
- data/lib/{flycal_cli → flycal/cli}/auth.rb +5 -3
- data/lib/{flycal_cli → flycal/cli}/clipboard.rb +3 -1
- data/lib/{flycal_cli → flycal/cli}/config.rb +4 -2
- data/lib/flycal/cli/file_cache.rb +44 -0
- data/lib/flycal/cli/locale.rb +58 -0
- data/lib/flycal/cli.rb +14 -0
- data/lib/flycal/client.rb +186 -0
- data/lib/flycal/credentials.rb +34 -0
- data/lib/{flycal_cli → flycal}/date_time_parser.rb +4 -4
- data/lib/flycal/description_query.rb +32 -0
- data/lib/{flycal_cli → flycal}/duration_parser.rb +3 -3
- data/lib/flycal/error.rb +5 -0
- data/lib/flycal/event_mapper.rb +58 -0
- data/lib/{flycal_cli → flycal}/locale.rb +35 -3
- data/lib/flycal/mock/calendar_service.rb +43 -0
- data/lib/flycal/mock/config.rb +171 -0
- data/lib/flycal/mock/event_generator.rb +71 -0
- data/lib/flycal/mock.rb +6 -0
- data/lib/flycal/pipeline/aggregator.rb +226 -0
- data/lib/flycal/pipeline/json_renderer.rb +145 -0
- data/lib/flycal/pipeline/params.rb +46 -0
- data/lib/flycal/pipeline/renderer.rb +27 -0
- data/lib/flycal/pipeline/retriever.rb +72 -0
- data/lib/flycal/pipeline/search_pipeline.rb +18 -0
- data/lib/flycal/pipeline/text_renderer.rb +134 -0
- data/lib/flycal/pipeline.rb +6 -0
- data/lib/{flycal_cli → flycal}/slot_finder.rb +1 -1
- data/lib/flycal/slot_formatter.rb +195 -0
- data/lib/flycal/version.rb +11 -0
- data/lib/flycal.rb +12 -0
- data/lib/flycal_cli.rb +37 -14
- data/mcp/tools.json +36 -9
- data/mockTemplates/mock1.json +11 -0
- data/mocks/mock1.json +11 -0
- metadata +38 -12
- data/lib/flycal_cli/calendar_service.rb +0 -74
- data/lib/flycal_cli/slot_formatter.rb +0 -68
- data/lib/flycal_cli/version.rb +0 -5
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flycal
|
|
4
|
+
module Pipeline
|
|
5
|
+
# First pipeline layer: fetch calendar events for the requested timeframe.
|
|
6
|
+
class Retriever
|
|
7
|
+
def initialize(calendar_service)
|
|
8
|
+
@service = calendar_service
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Expects params: :calendar_ids, :time_min, :time_max, optional :description
|
|
12
|
+
# Writes: :events (normalized), :calendar_names
|
|
13
|
+
def call(params)
|
|
14
|
+
calendar_ids = Array(params[:calendar_ids])
|
|
15
|
+
time_min = params[:time_min]
|
|
16
|
+
time_max = params[:time_max]
|
|
17
|
+
query = params[:description]
|
|
18
|
+
|
|
19
|
+
calendar_list = @service.list_calendars
|
|
20
|
+
calendar_names = calendar_list.to_h { |c| [c.id, c.summary || c.id] }
|
|
21
|
+
params[:calendar_names] = calendar_names
|
|
22
|
+
|
|
23
|
+
raw = @service.list_all_events(
|
|
24
|
+
calendar_ids,
|
|
25
|
+
time_min: time_min,
|
|
26
|
+
time_max: time_max,
|
|
27
|
+
query: query
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
params[:events] = raw.map { |item| normalize_event(item, calendar_names) }
|
|
31
|
+
params
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def normalize_event(item, calendar_names)
|
|
37
|
+
event = item[:event]
|
|
38
|
+
cal_id = item[:calendar_id]
|
|
39
|
+
start_at = to_time(event.start&.date_time || event.start&.date)
|
|
40
|
+
end_at = to_time(event.end&.date_time || event.end&.date)
|
|
41
|
+
minutes =
|
|
42
|
+
if start_at && end_at && end_at > start_at
|
|
43
|
+
(end_at - start_at) / 60.0
|
|
44
|
+
else
|
|
45
|
+
0.0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
calendar_id: cal_id,
|
|
50
|
+
calendar_name: calendar_names[cal_id] || cal_id,
|
|
51
|
+
summary: event.summary,
|
|
52
|
+
description: event.description,
|
|
53
|
+
start_at: start_at,
|
|
54
|
+
end_at: end_at,
|
|
55
|
+
duration_minutes: minutes,
|
|
56
|
+
all_day: !event.start&.date.nil? && event.start&.date_time.nil?,
|
|
57
|
+
raw: event
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_time(value)
|
|
62
|
+
return nil if value.nil?
|
|
63
|
+
return value if value.is_a?(Time)
|
|
64
|
+
return value.to_time if value.respond_to?(:to_time) && !value.is_a?(String)
|
|
65
|
+
|
|
66
|
+
Time.parse(value.to_s)
|
|
67
|
+
rescue ArgumentError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flycal
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Orchestrates Retriever → Aggregator → Renderer for search.
|
|
6
|
+
class SearchPipeline
|
|
7
|
+
def initialize(calendar_service)
|
|
8
|
+
@service = calendar_service
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(params)
|
|
12
|
+
Retriever.new(@service).call(params)
|
|
13
|
+
Aggregator.new.call(params)
|
|
14
|
+
Renderer.for(params[:format]).render(params)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flycal
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Shell text output — preserves the historical flycal search formatting.
|
|
6
|
+
class TextRenderer < Renderer
|
|
7
|
+
HOURS_PER_WORKING_DAY = Aggregator::HOURS_PER_WORKING_DAY
|
|
8
|
+
|
|
9
|
+
def render(params)
|
|
10
|
+
lines = []
|
|
11
|
+
lines.concat(event_lines(params))
|
|
12
|
+
lines << ""
|
|
13
|
+
lines.concat(summary_lines(params))
|
|
14
|
+
lines.join("\n") + "\n"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def event_lines(params)
|
|
20
|
+
Array(params[:events]).map do |ev|
|
|
21
|
+
start_str = format_datetime(ev[:start_at])
|
|
22
|
+
end_str = format_datetime(ev[:end_at])
|
|
23
|
+
desc = ev[:summary] || "(no title)"
|
|
24
|
+
if ev[:summary].nil? && ev[:description]
|
|
25
|
+
desc = ev[:description].to_s.slice(0, 80)
|
|
26
|
+
end
|
|
27
|
+
"#{ev[:calendar_name]} | #{start_str} | #{end_str} | #{desc}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def summary_lines(params)
|
|
32
|
+
totals = params[:totals] || {}
|
|
33
|
+
time_min = params[:time_min]
|
|
34
|
+
time_max = params[:time_max]
|
|
35
|
+
from_str = time_min.strftime("%a %Y-%m-%d %H:%M")
|
|
36
|
+
to_str = time_max.strftime("%a %Y-%m-%d %H:%M")
|
|
37
|
+
|
|
38
|
+
lines = []
|
|
39
|
+
lines << "---"
|
|
40
|
+
lines << "From: #{from_str} | To: #{to_str}"
|
|
41
|
+
lines << "Events found: #{totals[:event_count]}"
|
|
42
|
+
lines << "Total time occupied: #{format_duration(totals[:total_minutes].to_f)}"
|
|
43
|
+
lines << "Cache: #{params[:cache_status]}" if params[:cache_status]
|
|
44
|
+
lines << "Mock seed: #{params[:mock_seed]}" if params[:use_mock] && !params[:mock_seed].nil?
|
|
45
|
+
|
|
46
|
+
case params[:group_by]
|
|
47
|
+
when "week"
|
|
48
|
+
lines << ""
|
|
49
|
+
lines << "By week:"
|
|
50
|
+
lines.concat(week_group_lines(params[:groups]))
|
|
51
|
+
when "month"
|
|
52
|
+
lines << ""
|
|
53
|
+
lines << "By month:"
|
|
54
|
+
lines.concat(month_group_lines(params[:groups]))
|
|
55
|
+
when "day"
|
|
56
|
+
if explicit_time_group_by?(params, "day")
|
|
57
|
+
lines << ""
|
|
58
|
+
lines << "By day:"
|
|
59
|
+
lines.concat(day_group_lines(params[:groups]))
|
|
60
|
+
end
|
|
61
|
+
when "string"
|
|
62
|
+
lines << ""
|
|
63
|
+
lines << "By string:"
|
|
64
|
+
lines.concat(string_group_lines(params[:groups]))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
lines
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def explicit_time_group_by?(params, value)
|
|
71
|
+
params[:group_by_option].to_s.strip.downcase == value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def day_group_lines(groups)
|
|
75
|
+
Array(groups).map do |g|
|
|
76
|
+
day_str = format_date_with_day(g[:start_at])
|
|
77
|
+
" #{bold(g[:index])}. #{day_str}: #{format_hours_and_days(g[:hours], g[:working_days])}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def week_group_lines(groups)
|
|
82
|
+
Array(groups).map do |g|
|
|
83
|
+
start_str = format_date_with_day(g[:start_at])
|
|
84
|
+
end_str = format_date_with_day(g[:period_label_end])
|
|
85
|
+
" #{bold(g[:index])}. #{start_str} - #{end_str}: #{format_hours_and_days(g[:hours], g[:working_days])}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def month_group_lines(groups)
|
|
90
|
+
Array(groups).map do |g|
|
|
91
|
+
start_str = format_date_with_day(g[:start_at])
|
|
92
|
+
end_str = format_date_with_day(g[:period_label_end])
|
|
93
|
+
" #{g[:index]}. #{g[:month_name]} (#{start_str} - #{end_str}): #{format_hours_and_days(g[:hours], g[:working_days])}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def string_group_lines(groups)
|
|
98
|
+
Array(groups).map do |g|
|
|
99
|
+
label = g[:string] || g[:key]
|
|
100
|
+
" #{bold(g[:index])}. #{label}: #{g[:event_count]} events, #{format_hours_and_days(g[:hours], g[:working_days])}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def format_datetime(dt)
|
|
105
|
+
return "-" if dt.nil?
|
|
106
|
+
return dt if dt.is_a?(String)
|
|
107
|
+
|
|
108
|
+
"#{Locale.day_abbr(dt)} #{dt.strftime("%Y-%m-%d %H:%M")}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def format_date_with_day(dt)
|
|
112
|
+
return "-" if dt.nil?
|
|
113
|
+
|
|
114
|
+
t = dt.respond_to?(:to_time) ? dt.to_time : dt
|
|
115
|
+
"#{Locale.day_abbr(t)} #{t.strftime("%Y-%m-%d")}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def format_duration(total_minutes)
|
|
119
|
+
hours = (total_minutes / 60).floor
|
|
120
|
+
mins = (total_minutes % 60).round
|
|
121
|
+
working_days = (total_minutes / 60.0 / HOURS_PER_WORKING_DAY).round(1)
|
|
122
|
+
"#{bold(hours)}h #{bold(mins)}min (#{bold(working_days)} working days)"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def format_hours_and_days(hours, working_days)
|
|
126
|
+
"#{bold(hours)}h (#{bold(working_days)} working days)"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def bold(str)
|
|
130
|
+
"\e[1m#{str}\e[0m"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flycal
|
|
6
|
+
class SlotFormatter
|
|
7
|
+
class << self
|
|
8
|
+
def format_header(from:, to:, duration:, calendars:, count:, template: nil)
|
|
9
|
+
lines = []
|
|
10
|
+
lines << Locale.t(
|
|
11
|
+
"slots.header",
|
|
12
|
+
count: underline(count),
|
|
13
|
+
from: underline(Locale.format_long_date(from)),
|
|
14
|
+
to: underline(Locale.format_long_date(to)),
|
|
15
|
+
duration: underline(duration),
|
|
16
|
+
template: underline(template.to_s)
|
|
17
|
+
)
|
|
18
|
+
calendars.each do |cal|
|
|
19
|
+
lines << "- #{cal[:name]}"
|
|
20
|
+
end
|
|
21
|
+
lines << "link: #{google_calendar_day_url(from)}"
|
|
22
|
+
lines.join("\n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def format_output(slots_by_day)
|
|
26
|
+
lines = []
|
|
27
|
+
|
|
28
|
+
slots_by_day.sort_by(&:first).each do |date, slots|
|
|
29
|
+
lines << "" unless lines.empty?
|
|
30
|
+
lines << underline(day_header(date))
|
|
31
|
+
slots.each do |start_at, end_at|
|
|
32
|
+
lines << slot_range(start_at, end_at)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
lines.join("\n")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Full shell text for slots (header + body). Used by CLI and JSON `text` key.
|
|
40
|
+
def format_text(
|
|
41
|
+
slots_by_day:,
|
|
42
|
+
time_min:,
|
|
43
|
+
time_max:,
|
|
44
|
+
duration:,
|
|
45
|
+
calendars:,
|
|
46
|
+
template: nil,
|
|
47
|
+
empty_message: nil
|
|
48
|
+
)
|
|
49
|
+
count = slots_by_day.values.sum(&:size)
|
|
50
|
+
header = format_header(
|
|
51
|
+
from: time_min,
|
|
52
|
+
to: time_max,
|
|
53
|
+
duration: duration,
|
|
54
|
+
calendars: calendars,
|
|
55
|
+
count: count,
|
|
56
|
+
template: template
|
|
57
|
+
)
|
|
58
|
+
body = format_output(slots_by_day)
|
|
59
|
+
body = empty_message.to_s if body.empty? && empty_message
|
|
60
|
+
|
|
61
|
+
parts = [ header, "" ]
|
|
62
|
+
parts << body unless body.to_s.empty?
|
|
63
|
+
"#{parts.join("\n").rstrip}\n"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Pretty JSON for --format json (EmCP / API friendly).
|
|
67
|
+
def format_json(**kwargs)
|
|
68
|
+
JSON.pretty_generate(payload_hash(**kwargs)) + "\n"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Structured hash used by CLI JSON output and HTTP API.
|
|
72
|
+
def payload_hash(
|
|
73
|
+
slots_by_day:,
|
|
74
|
+
time_min:,
|
|
75
|
+
time_max:,
|
|
76
|
+
duration:,
|
|
77
|
+
template:,
|
|
78
|
+
calendars:,
|
|
79
|
+
locale: nil,
|
|
80
|
+
calendar_option: nil,
|
|
81
|
+
from_option: nil,
|
|
82
|
+
in_option: nil,
|
|
83
|
+
free_before: nil,
|
|
84
|
+
free_after: nil,
|
|
85
|
+
empty_message: nil
|
|
86
|
+
)
|
|
87
|
+
items = []
|
|
88
|
+
groups = slots_by_day.sort_by(&:first).map do |date, slots|
|
|
89
|
+
slot_items = slots.map { |start_at, end_at| serialize_slot(start_at, end_at, date) }
|
|
90
|
+
items.concat(slot_items)
|
|
91
|
+
{
|
|
92
|
+
"type" => "day",
|
|
93
|
+
"key" => date.iso8601,
|
|
94
|
+
"date" => date.iso8601,
|
|
95
|
+
"from" => format_iso(slots.map(&:first).min),
|
|
96
|
+
"to" => format_iso(slots.map(&:last).max),
|
|
97
|
+
"slots_found" => slot_items.size,
|
|
98
|
+
"items" => slot_items
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
text = format_text(
|
|
103
|
+
slots_by_day: slots_by_day,
|
|
104
|
+
time_min: time_min,
|
|
105
|
+
time_max: time_max,
|
|
106
|
+
duration: duration,
|
|
107
|
+
calendars: calendars,
|
|
108
|
+
template: template,
|
|
109
|
+
empty_message: empty_message
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
"params" => {
|
|
114
|
+
"command" => "slots",
|
|
115
|
+
"from" => format_iso(time_min),
|
|
116
|
+
"to" => format_iso(time_max),
|
|
117
|
+
"duration" => duration,
|
|
118
|
+
"template" => template,
|
|
119
|
+
"calendar" => blank_to_nil(calendar_option),
|
|
120
|
+
"calendar_ids" => Array(calendars).map { |c| c[:id] },
|
|
121
|
+
"format" => "json",
|
|
122
|
+
"locale" => locale || Locale.current_locale,
|
|
123
|
+
"from_option" => blank_to_nil(from_option),
|
|
124
|
+
"in_option" => blank_to_nil(in_option),
|
|
125
|
+
"free_before" => free_before,
|
|
126
|
+
"free_after" => free_after
|
|
127
|
+
}.compact,
|
|
128
|
+
"info" => {
|
|
129
|
+
"slots_found" => items.size,
|
|
130
|
+
"from" => format_iso(time_min),
|
|
131
|
+
"to" => format_iso(time_max),
|
|
132
|
+
"duration" => duration,
|
|
133
|
+
"template" => template,
|
|
134
|
+
"calendars" => Array(calendars).map { |c| { "id" => c[:id], "name" => c[:name] } },
|
|
135
|
+
"link" => google_calendar_day_url(time_min)
|
|
136
|
+
},
|
|
137
|
+
"text" => strip_ansi(text).split("\n"),
|
|
138
|
+
"items" => items,
|
|
139
|
+
"groups" => groups
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def strip_ansi(text)
|
|
144
|
+
text.to_s.gsub(/\e\[[0-9;]*m/, "")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def serialize_slot(start_at, end_at, date)
|
|
150
|
+
{
|
|
151
|
+
"date" => date.iso8601,
|
|
152
|
+
"start" => { "dateTime" => format_iso(start_at) },
|
|
153
|
+
"end" => { "dateTime" => format_iso(end_at) },
|
|
154
|
+
"duration_seconds" => (end_at - start_at).to_i
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def format_iso(value)
|
|
159
|
+
return nil if value.nil?
|
|
160
|
+
return value.iso8601 if value.respond_to?(:iso8601)
|
|
161
|
+
|
|
162
|
+
value.to_s
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def blank_to_nil(value)
|
|
166
|
+
str = value.to_s
|
|
167
|
+
str.empty? ? nil : str
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def underline(str)
|
|
171
|
+
"\e[4m#{str}\e[0m"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def google_calendar_day_url(from)
|
|
175
|
+
t = from.respond_to?(:to_time) ? from.to_time : from
|
|
176
|
+
"https://calendar.google.com/calendar/r/day/#{t.year}/#{t.month}/#{t.day}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def day_header(date)
|
|
180
|
+
"#{Locale.day_name(date)} #{date.day}/#{date.month}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def slot_range(start_at, end_at)
|
|
184
|
+
"#{format_time(start_at)} - #{format_time(end_at)}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def format_time(time)
|
|
188
|
+
return time.strftime("%-H") if time.min.zero?
|
|
189
|
+
|
|
190
|
+
minutes = sprintf("%02d", time.min)
|
|
191
|
+
"#{time.hour}.#{minutes}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flycal
|
|
4
|
+
# Kept for Zeitwerk (`lib/flycal/version.rb` → Flycal::Version).
|
|
5
|
+
# Canonical version string lives on Flycal::VERSION (see lib/flycal.rb).
|
|
6
|
+
module Version
|
|
7
|
+
def self.to_s
|
|
8
|
+
Flycal::VERSION
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/flycal.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Flycal core namespace.
|
|
4
|
+
# Under Rails, Zeitwerk autoloads lib/flycal/*.
|
|
5
|
+
# The CLI gem entrypoint (flycal_cli/lib/flycal_cli.rb) eager-requires what it needs.
|
|
6
|
+
module Flycal
|
|
7
|
+
VERSION = "1.0"
|
|
8
|
+
|
|
9
|
+
def self.connect(credentials:)
|
|
10
|
+
Client.new(credentials: credentials)
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/flycal_cli.rb
CHANGED
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
require "flycal_cli/date_time_parser"
|
|
9
|
-
require "flycal_cli/clipboard"
|
|
10
|
-
require "flycal_cli/calendar_service"
|
|
11
|
-
require "flycal_cli/slot_finder"
|
|
12
|
-
require "flycal_cli/slot_formatter"
|
|
13
|
-
require "flycal_cli/cli"
|
|
3
|
+
# Gem entrypoint for the flycal-cli package.
|
|
4
|
+
core_lib = File.expand_path("../../lib", __dir__)
|
|
5
|
+
$LOAD_PATH.unshift(core_lib) unless $LOAD_PATH.include?(core_lib)
|
|
6
|
+
cli_lib = File.expand_path(".", __dir__)
|
|
7
|
+
$LOAD_PATH.unshift(cli_lib) unless $LOAD_PATH.include?(cli_lib)
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
# Eager-load Flycal core (outside Rails/Zeitwerk).
|
|
10
|
+
require "flycal/version"
|
|
11
|
+
require "flycal/error"
|
|
12
|
+
require "flycal/locale"
|
|
13
|
+
require "flycal/credentials"
|
|
14
|
+
require "flycal/event_mapper"
|
|
15
|
+
require "flycal/cache_key"
|
|
16
|
+
require "flycal/cache_annotator"
|
|
17
|
+
require "flycal/client"
|
|
18
|
+
require "flycal/duration_parser"
|
|
19
|
+
require "flycal/date_time_parser"
|
|
20
|
+
require "flycal/description_query"
|
|
21
|
+
require "flycal/calendar_query"
|
|
22
|
+
require "flycal/calendar_service"
|
|
23
|
+
require "flycal/slot_finder"
|
|
24
|
+
require "flycal/slot_formatter"
|
|
25
|
+
require "flycal/mock"
|
|
26
|
+
require "flycal/mock/config"
|
|
27
|
+
require "flycal/mock/event_generator"
|
|
28
|
+
require "flycal/mock/calendar_service"
|
|
29
|
+
require "flycal/pipeline"
|
|
30
|
+
require "flycal/pipeline/params"
|
|
31
|
+
require "flycal/pipeline/retriever"
|
|
32
|
+
require "flycal/pipeline/aggregator"
|
|
33
|
+
require "flycal/pipeline/renderer"
|
|
34
|
+
require "flycal/pipeline/text_renderer"
|
|
35
|
+
require "flycal/pipeline/json_renderer"
|
|
36
|
+
require "flycal/pipeline/search_pipeline"
|
|
37
|
+
|
|
38
|
+
require "flycal/cli"
|
|
39
|
+
|
|
40
|
+
FlycalCli = Flycal::Cli unless defined?(FlycalCli)
|
data/mcp/tools.json
CHANGED
|
@@ -358,7 +358,8 @@
|
|
|
358
358
|
"to": ["--to", "{value}"],
|
|
359
359
|
"in": ["--in", "{value}"],
|
|
360
360
|
"description": ["--description", "{value}"],
|
|
361
|
-
"locale": ["--locale", "{value}"]
|
|
361
|
+
"locale": ["--locale", "{value}"],
|
|
362
|
+
"format": ["--format", "{value}"]
|
|
362
363
|
},
|
|
363
364
|
"examples": [
|
|
364
365
|
["flycal", "search"],
|
|
@@ -410,6 +411,14 @@
|
|
|
410
411
|
"enum": ["en", "it"],
|
|
411
412
|
"required": false,
|
|
412
413
|
"description": "Optional locale override."
|
|
414
|
+
},
|
|
415
|
+
"format": {
|
|
416
|
+
"cli": ["--format"],
|
|
417
|
+
"type": "string",
|
|
418
|
+
"enum": ["text", "json"],
|
|
419
|
+
"required": false,
|
|
420
|
+
"default": "text",
|
|
421
|
+
"description": "Output format: text (shell) or json (pretty-printed)."
|
|
413
422
|
}
|
|
414
423
|
},
|
|
415
424
|
"inputSchema": {
|
|
@@ -440,6 +449,11 @@
|
|
|
440
449
|
"type": "string",
|
|
441
450
|
"enum": ["en", "it"],
|
|
442
451
|
"description": "--locale. Optional locale override (en, it)."
|
|
452
|
+
},
|
|
453
|
+
"format": {
|
|
454
|
+
"type": "string",
|
|
455
|
+
"enum": ["text", "json"],
|
|
456
|
+
"description": "--format. Output format (default: text). json returns params/info/items/groups."
|
|
443
457
|
}
|
|
444
458
|
}
|
|
445
459
|
}
|
|
@@ -447,7 +461,7 @@
|
|
|
447
461
|
{
|
|
448
462
|
"name": "flycal_slots",
|
|
449
463
|
"title": "Find free time slots",
|
|
450
|
-
"description": "Find continuous free ranges using a slots.templates schedule (days + hours). Default template = first key (usually work). Busy events come from slots.exclude_calendars (or calendar_default if empty). Respects free_before/free_after.
|
|
464
|
+
"description": "Find continuous free ranges using a slots.templates schedule (days + hours). Default template = first key (usually work). Busy events come from slots.exclude_calendars (or calendar_default if empty). Respects free_before/free_after. Output: text (header + availability, clipboard) or JSON via --format json.",
|
|
451
465
|
"interactive": false,
|
|
452
466
|
"requires_auth": true,
|
|
453
467
|
"command": {
|
|
@@ -459,14 +473,16 @@
|
|
|
459
473
|
"in": ["--in", "{value}"],
|
|
460
474
|
"template": ["--template", "{value}"],
|
|
461
475
|
"calendar": ["--calendar", "{value}"],
|
|
462
|
-
"locale": ["--locale", "{value}"]
|
|
476
|
+
"locale": ["--locale", "{value}"],
|
|
477
|
+
"format": ["--format", "{value}"]
|
|
463
478
|
},
|
|
464
479
|
"examples": [
|
|
465
480
|
["flycal", "slots"],
|
|
466
481
|
["flycal", "slots", "--in", "5 days"],
|
|
467
482
|
["flycal", "slots", "--from", "monday", "--in", "5 days", "--locale", "it"],
|
|
468
483
|
["flycal", "slots", "--in", "12 days", "--template", "dinner"],
|
|
469
|
-
["flycal", "slots", "--from", "next monday", "--duration", "1h", "--template", "work"]
|
|
484
|
+
["flycal", "slots", "--from", "next monday", "--duration", "1h", "--template", "work"],
|
|
485
|
+
["flycal", "slots", "--format", "json", "--in", "5 days"]
|
|
470
486
|
]
|
|
471
487
|
},
|
|
472
488
|
"parameters": {
|
|
@@ -518,15 +534,21 @@
|
|
|
518
534
|
"enum": ["en", "it"],
|
|
519
535
|
"required": false,
|
|
520
536
|
"description": "Optional locale override."
|
|
537
|
+
},
|
|
538
|
+
"format": {
|
|
539
|
+
"cli": ["--format"],
|
|
540
|
+
"type": "string",
|
|
541
|
+
"enum": ["text", "json"],
|
|
542
|
+
"required": false,
|
|
543
|
+
"default": "text",
|
|
544
|
+
"description": "Output format: text (shell + clipboard) or json (params/info/items/groups)."
|
|
521
545
|
}
|
|
522
546
|
},
|
|
523
547
|
"output": {
|
|
524
|
-
"format": "text/plain",
|
|
548
|
+
"format": "text/plain or application/json",
|
|
525
549
|
"includes": [
|
|
526
|
-
"count + from/to + duration + template",
|
|
527
|
-
"
|
|
528
|
-
"link: Google Calendar day URL at --from",
|
|
529
|
-
"availability ranges grouped by day (copied to clipboard without header)"
|
|
550
|
+
"text: count + from/to + duration + template, calendars, link, ranges by day (clipboard)",
|
|
551
|
+
"json: params, info (slots_found, calendars, link), flat items, groups by day"
|
|
530
552
|
],
|
|
531
553
|
"example": "found 6 slots\nfrom sat 1 August 2026 to sat 8 August 2026\nwith duration 45min, template dinner\nconsidering calendars\n- Work\n- Personal\nlink: https://calendar.google.com/calendar/r/day/2026/8/1\n\nsaturday 1/8\n19 - 23"
|
|
532
554
|
},
|
|
@@ -559,6 +581,11 @@
|
|
|
559
581
|
"type": "string",
|
|
560
582
|
"enum": ["en", "it"],
|
|
561
583
|
"description": "--locale. Optional locale override (en, it)."
|
|
584
|
+
},
|
|
585
|
+
"format": {
|
|
586
|
+
"type": "string",
|
|
587
|
+
"enum": ["text", "json"],
|
|
588
|
+
"description": "--format. text (default) or json."
|
|
562
589
|
}
|
|
563
590
|
}
|
|
564
591
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mockCalendar": "mock1",
|
|
3
|
+
"mockEventDescriptionPatterns": "work,personal,event",
|
|
4
|
+
"mockEventCount": 100,
|
|
5
|
+
"mockEventFrom": "2026-01-01",
|
|
6
|
+
"mockEventTo": "2026-01-31",
|
|
7
|
+
"mockEventDurationMin": "4h",
|
|
8
|
+
"mockEventDurationMax": "4h",
|
|
9
|
+
"mockEventHoursFrom": "09:00",
|
|
10
|
+
"mockEventHoursTo": "17:00"
|
|
11
|
+
}
|
data/mocks/mock1.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mockCalendar": "mock1",
|
|
3
|
+
"mockEventDescriptionPatterns": "work,personal,event",
|
|
4
|
+
"mockEventCount": 100,
|
|
5
|
+
"mockEventFrom": "2026-01-01",
|
|
6
|
+
"mockEventTo": "2026-01-31",
|
|
7
|
+
"mockEventDurationMin": "4h",
|
|
8
|
+
"mockEventDurationMax": "4h",
|
|
9
|
+
"mockEventHoursFrom": "09:00",
|
|
10
|
+
"mockEventHoursTo": "17:00"
|
|
11
|
+
}
|