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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -181
  3. data/bin/flycal +2 -2
  4. data/lib/flycal/cache_annotator.rb +83 -0
  5. data/lib/flycal/cache_key.rb +82 -0
  6. data/lib/flycal/calendar_query.rb +60 -0
  7. data/lib/{flycal_cli → flycal}/calendar_service.rb +48 -4
  8. data/lib/{flycal_cli/cli.rb → flycal/cli/app.rb} +253 -109
  9. data/lib/{flycal_cli → flycal/cli}/auth.rb +5 -3
  10. data/lib/{flycal_cli → flycal/cli}/clipboard.rb +3 -1
  11. data/lib/{flycal_cli → flycal/cli}/config.rb +4 -2
  12. data/lib/flycal/cli/file_cache.rb +44 -0
  13. data/lib/flycal/cli/locale.rb +58 -0
  14. data/lib/flycal/cli.rb +14 -0
  15. data/lib/flycal/client.rb +186 -0
  16. data/lib/flycal/credentials.rb +34 -0
  17. data/lib/{flycal_cli → flycal}/date_time_parser.rb +4 -4
  18. data/lib/{flycal_cli → flycal}/description_query.rb +3 -3
  19. data/lib/{flycal_cli → flycal}/duration_parser.rb +3 -3
  20. data/lib/flycal/error.rb +5 -0
  21. data/lib/flycal/event_mapper.rb +58 -0
  22. data/lib/{flycal_cli → flycal}/locale.rb +35 -3
  23. data/lib/{flycal_cli → flycal}/mock/calendar_service.rb +1 -1
  24. data/lib/{flycal_cli → flycal}/mock/config.rb +14 -14
  25. data/lib/{flycal_cli → flycal}/mock/event_generator.rb +2 -2
  26. data/lib/flycal/mock.rb +6 -0
  27. data/lib/{flycal_cli → flycal}/pipeline/aggregator.rb +21 -24
  28. data/lib/{flycal_cli → flycal}/pipeline/json_renderer.rb +16 -13
  29. data/lib/{flycal_cli → flycal}/pipeline/params.rb +1 -1
  30. data/lib/{flycal_cli → flycal}/pipeline/renderer.rb +4 -4
  31. data/lib/{flycal_cli → flycal}/pipeline/retriever.rb +1 -1
  32. data/lib/{flycal_cli → flycal}/pipeline/search_pipeline.rb +1 -1
  33. data/lib/{flycal_cli → flycal}/pipeline/text_renderer.rb +9 -8
  34. data/lib/flycal/pipeline.rb +6 -0
  35. data/lib/{flycal_cli → flycal}/slot_finder.rb +1 -1
  36. data/lib/flycal/slot_formatter.rb +195 -0
  37. data/lib/flycal/version.rb +11 -0
  38. data/lib/flycal.rb +12 -0
  39. data/lib/flycal_cli.rb +37 -17
  40. data/mcp/tools.json +21 -8
  41. metadata +36 -25
  42. data/lib/flycal_cli/mock.rb +0 -10
  43. data/lib/flycal_cli/pipeline.rb +0 -14
  44. data/lib/flycal_cli/slot_formatter.rb +0 -68
  45. 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 FlycalCli
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 | description
17
+ # day | week | month | <any other string> (split on | for string grouping)
18
18
  class Aggregator
19
19
  HOURS_PER_WORKING_DAY = 8
20
- ALLOWED_GROUP_BY = %w[day week month description].freeze
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 "description" then description_groups(events, params[:description])
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
- key = explicit.to_s.strip.downcase
61
- unless key.empty?
62
- unless ALLOWED_GROUP_BY.include?(key)
63
- raise FlycalCli::Error,
64
- "Unsupported groupBy #{explicit.inspect}. Available: #{ALLOWED_GROUP_BY.join(", ")}"
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
- "day"
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 description_groups(events, description_query)
167
- patterns = DescriptionQuery.patterns(description_query)
163
+ def string_groups(events, group_by_string)
164
+ patterns = DescriptionQuery.patterns(group_by_string)
168
165
  if patterns.empty?
169
- raise FlycalCli::Error,
170
- "groupBy description requires --description with at least one term (e.g. rui|solver)."
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
- description: pattern
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, description: 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
- description: description,
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 FlycalCli
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 == "description"
76
- g["description"] = group[:description] || group[:key]
85
+ if type == "string"
86
+ g["string"] = group[:string] || group[:key]
77
87
  else
78
- g["from"] = format_group_boundary(group[:start_at])
79
- g["to"] = format_group_boundary(group[:end_at])
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,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module FlycalCli
3
+ module Flycal
4
4
  module Pipeline
5
5
  # Mutable bag of CLI + derived parameters passed through the pipeline.
6
6
  class Params
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module FlycalCli
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" => "FlycalCli::Pipeline::TextRenderer",
9
- "json" => "FlycalCli::Pipeline::JsonRenderer"
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 FlycalCli::Error, "Unsupported format #{format.inspect}. Available: #{FORMATS.keys.join(", ")}" unless class_name
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 FlycalCli
3
+ module Flycal
4
4
  module Pipeline
5
5
  # First pipeline layer: fetch calendar events for the requested timeframe.
6
6
  class Retriever
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module FlycalCli
3
+ module Flycal
4
4
  module Pipeline
5
5
  # Orchestrates Retriever → Aggregator → Renderer for search.
6
6
  class SearchPipeline
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module FlycalCli
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 explicit_group_by?(params, "day")
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 "description"
61
+ when "string"
61
62
  lines << ""
62
- lines << "By description:"
63
- lines.concat(description_group_lines(params[:groups]))
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 explicit_group_by?(params, value)
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 description_group_lines(groups)
97
+ def string_group_lines(groups)
97
98
  Array(groups).map do |g|
98
- label = g[:description] || g[:key]
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flycal
4
+ module Pipeline
5
+ end
6
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module FlycalCli
3
+ module Flycal
4
4
  class SlotFinder
5
5
  DEFAULT_HOURS = [[9, 0, 18, 0]].freeze
6
6
  DEFAULT_DAYS = [1, 2, 3, 4, 5].freeze
@@ -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
- require "flycal_cli/version"
4
- require "flycal_cli/config"
5
- require "flycal_cli/locale"
6
- require "flycal_cli/auth"
7
- require "flycal_cli/duration_parser"
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
- module FlycalCli
19
- class Error < StandardError; end
20
- end
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. Prints header + availability; copies availability list to clipboard when a clipboard tool exists.",
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
- "bullet list of considered calendars",
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
  }