flycal-cli 0.7.7 → 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 -181
- 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_cli → flycal}/calendar_service.rb +48 -4
- data/lib/{flycal_cli/cli.rb → flycal/cli/app.rb} +253 -109
- 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_cli → flycal}/description_query.rb +3 -3
- 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_cli → flycal}/mock/calendar_service.rb +1 -1
- data/lib/{flycal_cli → flycal}/mock/config.rb +14 -14
- data/lib/{flycal_cli → flycal}/mock/event_generator.rb +2 -2
- data/lib/flycal/mock.rb +6 -0
- data/lib/{flycal_cli → flycal}/pipeline/aggregator.rb +21 -24
- data/lib/{flycal_cli → flycal}/pipeline/json_renderer.rb +16 -13
- data/lib/{flycal_cli → flycal}/pipeline/params.rb +1 -1
- data/lib/{flycal_cli → flycal}/pipeline/renderer.rb +4 -4
- data/lib/{flycal_cli → flycal}/pipeline/retriever.rb +1 -1
- data/lib/{flycal_cli → flycal}/pipeline/search_pipeline.rb +1 -1
- data/lib/{flycal_cli → flycal}/pipeline/text_renderer.rb +9 -8
- 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 -17
- data/mcp/tools.json +21 -8
- metadata +36 -25
- data/lib/flycal_cli/mock.rb +0 -10
- data/lib/flycal_cli/pipeline.rb +0 -14
- data/lib/flycal_cli/slot_formatter.rb +0 -68
- data/lib/flycal_cli/version.rb +0 -5
|
@@ -4,7 +4,7 @@ require "active_support/core_ext/date"
|
|
|
4
4
|
require "active_support/core_ext/integer"
|
|
5
5
|
require "active_support/core_ext/time"
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module Flycal
|
|
8
8
|
module Pipeline
|
|
9
9
|
# Second pipeline layer: group events and compute aggregate metrics.
|
|
10
10
|
#
|
|
@@ -14,10 +14,10 @@ module FlycalCli
|
|
|
14
14
|
# month — timeframe > 30 days
|
|
15
15
|
#
|
|
16
16
|
# Override with params[:group_by_option] / --groupBy:
|
|
17
|
-
# day | week | month |
|
|
17
|
+
# day | week | month | <any other string> (split on | for string grouping)
|
|
18
18
|
class Aggregator
|
|
19
19
|
HOURS_PER_WORKING_DAY = 8
|
|
20
|
-
|
|
20
|
+
TIME_GROUP_BY = %w[day week month].freeze
|
|
21
21
|
|
|
22
22
|
def call(params)
|
|
23
23
|
time_min = params[:time_min]
|
|
@@ -49,7 +49,7 @@ module FlycalCli
|
|
|
49
49
|
case group_by
|
|
50
50
|
when "week" then weekly_groups(events, ranges, time_min, time_max)
|
|
51
51
|
when "month" then monthly_groups(events, ranges, time_min, time_max)
|
|
52
|
-
when "
|
|
52
|
+
when "string" then string_groups(events, params[:group_by_option])
|
|
53
53
|
else daily_groups(events, ranges, time_min, time_max)
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -57,21 +57,18 @@ module FlycalCli
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def self.resolve_group_by(timeframe_days, explicit = nil)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
raw = explicit.to_s.strip
|
|
61
|
+
if raw.empty?
|
|
62
|
+
if timeframe_days > 30
|
|
63
|
+
"month"
|
|
64
|
+
elsif timeframe_days > 7
|
|
65
|
+
"week"
|
|
66
|
+
else
|
|
67
|
+
"day"
|
|
65
68
|
end
|
|
66
|
-
return key
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
if timeframe_days > 30
|
|
70
|
-
"month"
|
|
71
|
-
elsif timeframe_days > 7
|
|
72
|
-
"week"
|
|
73
69
|
else
|
|
74
|
-
|
|
70
|
+
key = raw.downcase
|
|
71
|
+
TIME_GROUP_BY.include?(key) ? key : "string"
|
|
75
72
|
end
|
|
76
73
|
end
|
|
77
74
|
|
|
@@ -163,11 +160,11 @@ module FlycalCli
|
|
|
163
160
|
groups
|
|
164
161
|
end
|
|
165
162
|
|
|
166
|
-
def
|
|
167
|
-
patterns = DescriptionQuery.patterns(
|
|
163
|
+
def string_groups(events, group_by_string)
|
|
164
|
+
patterns = DescriptionQuery.patterns(group_by_string)
|
|
168
165
|
if patterns.empty?
|
|
169
|
-
raise
|
|
170
|
-
"groupBy
|
|
166
|
+
raise Flycal::Error,
|
|
167
|
+
"groupBy string requires at least one term (e.g. --groupBy \"rui|solver\")."
|
|
171
168
|
end
|
|
172
169
|
|
|
173
170
|
patterns.each_with_index.map do |pattern, idx|
|
|
@@ -183,13 +180,13 @@ module FlycalCli
|
|
|
183
180
|
period_label_end: nil,
|
|
184
181
|
total_minutes: minutes,
|
|
185
182
|
events: matched,
|
|
186
|
-
|
|
183
|
+
string: pattern
|
|
187
184
|
)
|
|
188
185
|
end
|
|
189
186
|
end
|
|
190
187
|
|
|
191
188
|
def build_group(index:, key:, start_at:, end_at:, period_label_end:, total_minutes:, events:,
|
|
192
|
-
month_name: nil,
|
|
189
|
+
month_name: nil, string: nil)
|
|
193
190
|
{
|
|
194
191
|
index: index,
|
|
195
192
|
key: key,
|
|
@@ -197,7 +194,7 @@ module FlycalCli
|
|
|
197
194
|
end_at: end_at,
|
|
198
195
|
period_label_end: period_label_end,
|
|
199
196
|
month_name: month_name,
|
|
200
|
-
|
|
197
|
+
string: string,
|
|
201
198
|
event_count: events.size,
|
|
202
199
|
total_minutes: total_minutes,
|
|
203
200
|
hours: (total_minutes / 60.0).round(1),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module Flycal
|
|
6
6
|
module Pipeline
|
|
7
7
|
# Pretty-printed JSON output for search (API / tooling friendly).
|
|
8
8
|
class JsonRenderer < Renderer
|
|
@@ -10,6 +10,7 @@ module FlycalCli
|
|
|
10
10
|
payload = {
|
|
11
11
|
"params" => build_params(params),
|
|
12
12
|
"info" => build_info(params),
|
|
13
|
+
"text" => lines_for_output(TextRenderer.new.render(params)),
|
|
13
14
|
"items" => Array(params[:events]).map { |ev| serialize_event(ev) },
|
|
14
15
|
"groups" => build_groups(params)
|
|
15
16
|
}
|
|
@@ -19,6 +20,14 @@ module FlycalCli
|
|
|
19
20
|
|
|
20
21
|
private
|
|
21
22
|
|
|
23
|
+
def strip_ansi(text)
|
|
24
|
+
text.to_s.gsub(/\e\[[0-9;]*m/, "")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def lines_for_output(text)
|
|
28
|
+
strip_ansi(text).split("\n")
|
|
29
|
+
end
|
|
30
|
+
|
|
22
31
|
def build_params(params)
|
|
23
32
|
out = {
|
|
24
33
|
"command" => params[:command] || "search",
|
|
@@ -57,6 +66,7 @@ module FlycalCli
|
|
|
57
66
|
"total_working_days" => totals[:working_days].to_f
|
|
58
67
|
}
|
|
59
68
|
info["mock_seed"] = params[:mock_seed] if params[:use_mock] && !params[:mock_seed].nil?
|
|
69
|
+
info["cache"] = params[:cache_status] if params[:cache_status]
|
|
60
70
|
info
|
|
61
71
|
end
|
|
62
72
|
|
|
@@ -72,11 +82,11 @@ module FlycalCli
|
|
|
72
82
|
"events_found" => group[:event_count] || Array(group[:events]).size,
|
|
73
83
|
"items" => Array(group[:events]).map { |ev| serialize_event(ev) }
|
|
74
84
|
}.tap do |g|
|
|
75
|
-
if type == "
|
|
76
|
-
g["
|
|
85
|
+
if type == "string"
|
|
86
|
+
g["string"] = group[:string] || group[:key]
|
|
77
87
|
else
|
|
78
|
-
g["from"] =
|
|
79
|
-
g["to"] =
|
|
88
|
+
g["from"] = format_time(group[:start_at])
|
|
89
|
+
g["to"] = format_time(group[:end_at])
|
|
80
90
|
end
|
|
81
91
|
g["month_name"] = group[:month_name] if group[:month_name]
|
|
82
92
|
end
|
|
@@ -118,6 +128,7 @@ module FlycalCli
|
|
|
118
128
|
end
|
|
119
129
|
end
|
|
120
130
|
|
|
131
|
+
# Google Calendar-style ISO-8601 datetime, e.g. 2026-07-29T14:41:00+00:00
|
|
121
132
|
def format_time(value)
|
|
122
133
|
return nil if value.nil?
|
|
123
134
|
return value.iso8601 if value.respond_to?(:iso8601)
|
|
@@ -125,14 +136,6 @@ module FlycalCli
|
|
|
125
136
|
value.to_s
|
|
126
137
|
end
|
|
127
138
|
|
|
128
|
-
# Compact local datetime used on group boundaries: YYYYMMDDTHHMMSS
|
|
129
|
-
def format_group_boundary(value)
|
|
130
|
-
return nil if value.nil?
|
|
131
|
-
|
|
132
|
-
t = value.respond_to?(:to_time) ? value.to_time : value
|
|
133
|
-
t.strftime("%Y%m%dT%H%M%S")
|
|
134
|
-
end
|
|
135
|
-
|
|
136
139
|
def blank_to_nil(value)
|
|
137
140
|
str = value.to_s
|
|
138
141
|
str.empty? ? nil : str
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Flycal
|
|
4
4
|
module Pipeline
|
|
5
5
|
# Third pipeline layer: render aggregated results to a chosen format.
|
|
6
6
|
class Renderer
|
|
7
7
|
FORMATS = {
|
|
8
|
-
"text" => "
|
|
9
|
-
"json" => "
|
|
8
|
+
"text" => "Flycal::Pipeline::TextRenderer",
|
|
9
|
+
"json" => "Flycal::Pipeline::JsonRenderer"
|
|
10
10
|
}.freeze
|
|
11
11
|
|
|
12
12
|
def self.for(format)
|
|
13
13
|
key = format.to_s.strip.downcase
|
|
14
14
|
key = "text" if key.empty?
|
|
15
15
|
class_name = FORMATS[key]
|
|
16
|
-
raise
|
|
16
|
+
raise Flycal::Error, "Unsupported format #{format.inspect}. Available: #{FORMATS.keys.join(", ")}" unless class_name
|
|
17
17
|
|
|
18
18
|
Object.const_get(class_name).new
|
|
19
19
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Flycal
|
|
4
4
|
module Pipeline
|
|
5
5
|
# Shell text output — preserves the historical flycal search formatting.
|
|
6
6
|
class TextRenderer < Renderer
|
|
@@ -40,6 +40,7 @@ module FlycalCli
|
|
|
40
40
|
lines << "From: #{from_str} | To: #{to_str}"
|
|
41
41
|
lines << "Events found: #{totals[:event_count]}"
|
|
42
42
|
lines << "Total time occupied: #{format_duration(totals[:total_minutes].to_f)}"
|
|
43
|
+
lines << "Cache: #{params[:cache_status]}" if params[:cache_status]
|
|
43
44
|
lines << "Mock seed: #{params[:mock_seed]}" if params[:use_mock] && !params[:mock_seed].nil?
|
|
44
45
|
|
|
45
46
|
case params[:group_by]
|
|
@@ -52,21 +53,21 @@ module FlycalCli
|
|
|
52
53
|
lines << "By month:"
|
|
53
54
|
lines.concat(month_group_lines(params[:groups]))
|
|
54
55
|
when "day"
|
|
55
|
-
if
|
|
56
|
+
if explicit_time_group_by?(params, "day")
|
|
56
57
|
lines << ""
|
|
57
58
|
lines << "By day:"
|
|
58
59
|
lines.concat(day_group_lines(params[:groups]))
|
|
59
60
|
end
|
|
60
|
-
when "
|
|
61
|
+
when "string"
|
|
61
62
|
lines << ""
|
|
62
|
-
lines << "By
|
|
63
|
-
lines.concat(
|
|
63
|
+
lines << "By string:"
|
|
64
|
+
lines.concat(string_group_lines(params[:groups]))
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
lines
|
|
67
68
|
end
|
|
68
69
|
|
|
69
|
-
def
|
|
70
|
+
def explicit_time_group_by?(params, value)
|
|
70
71
|
params[:group_by_option].to_s.strip.downcase == value
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -93,9 +94,9 @@ module FlycalCli
|
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
|
|
96
|
-
def
|
|
97
|
+
def string_group_lines(groups)
|
|
97
98
|
Array(groups).map do |g|
|
|
98
|
-
label = g[:
|
|
99
|
+
label = g[:string] || g[:key]
|
|
99
100
|
" #{bold(g[:index])}. #{label}: #{g[:event_count]} events, #{format_hours_and_days(g[:hours], g[:working_days])}"
|
|
100
101
|
end
|
|
101
102
|
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,20 +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/description_query"
|
|
11
|
-
require "flycal_cli/calendar_service"
|
|
12
|
-
require "flycal_cli/slot_finder"
|
|
13
|
-
require "flycal_cli/slot_formatter"
|
|
14
|
-
require "flycal_cli/mock"
|
|
15
|
-
require "flycal_cli/pipeline"
|
|
16
|
-
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)
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
@@ -461,7 +461,7 @@
|
|
|
461
461
|
{
|
|
462
462
|
"name": "flycal_slots",
|
|
463
463
|
"title": "Find free time slots",
|
|
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.
|
|
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.",
|
|
465
465
|
"interactive": false,
|
|
466
466
|
"requires_auth": true,
|
|
467
467
|
"command": {
|
|
@@ -473,14 +473,16 @@
|
|
|
473
473
|
"in": ["--in", "{value}"],
|
|
474
474
|
"template": ["--template", "{value}"],
|
|
475
475
|
"calendar": ["--calendar", "{value}"],
|
|
476
|
-
"locale": ["--locale", "{value}"]
|
|
476
|
+
"locale": ["--locale", "{value}"],
|
|
477
|
+
"format": ["--format", "{value}"]
|
|
477
478
|
},
|
|
478
479
|
"examples": [
|
|
479
480
|
["flycal", "slots"],
|
|
480
481
|
["flycal", "slots", "--in", "5 days"],
|
|
481
482
|
["flycal", "slots", "--from", "monday", "--in", "5 days", "--locale", "it"],
|
|
482
483
|
["flycal", "slots", "--in", "12 days", "--template", "dinner"],
|
|
483
|
-
["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"]
|
|
484
486
|
]
|
|
485
487
|
},
|
|
486
488
|
"parameters": {
|
|
@@ -532,15 +534,21 @@
|
|
|
532
534
|
"enum": ["en", "it"],
|
|
533
535
|
"required": false,
|
|
534
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)."
|
|
535
545
|
}
|
|
536
546
|
},
|
|
537
547
|
"output": {
|
|
538
|
-
"format": "text/plain",
|
|
548
|
+
"format": "text/plain or application/json",
|
|
539
549
|
"includes": [
|
|
540
|
-
"count + from/to + duration + template",
|
|
541
|
-
"
|
|
542
|
-
"link: Google Calendar day URL at --from",
|
|
543
|
-
"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"
|
|
544
552
|
],
|
|
545
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"
|
|
546
554
|
},
|
|
@@ -573,6 +581,11 @@
|
|
|
573
581
|
"type": "string",
|
|
574
582
|
"enum": ["en", "it"],
|
|
575
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."
|
|
576
589
|
}
|
|
577
590
|
}
|
|
578
591
|
}
|