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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -174
  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/calendar_service.rb +115 -0
  8. data/lib/{flycal_cli/cli.rb → flycal/cli/app.rb} +320 -250
  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/description_query.rb +32 -0
  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/mock/calendar_service.rb +43 -0
  24. data/lib/flycal/mock/config.rb +171 -0
  25. data/lib/flycal/mock/event_generator.rb +71 -0
  26. data/lib/flycal/mock.rb +6 -0
  27. data/lib/flycal/pipeline/aggregator.rb +226 -0
  28. data/lib/flycal/pipeline/json_renderer.rb +145 -0
  29. data/lib/flycal/pipeline/params.rb +46 -0
  30. data/lib/flycal/pipeline/renderer.rb +27 -0
  31. data/lib/flycal/pipeline/retriever.rb +72 -0
  32. data/lib/flycal/pipeline/search_pipeline.rb +18 -0
  33. data/lib/flycal/pipeline/text_renderer.rb +134 -0
  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 -14
  40. data/mcp/tools.json +36 -9
  41. data/mockTemplates/mock1.json +11 -0
  42. data/mocks/mock1.json +11 -0
  43. metadata +38 -12
  44. data/lib/flycal_cli/calendar_service.rb +0 -74
  45. data/lib/flycal_cli/slot_formatter.rb +0 -68
  46. 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,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,17 +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/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
- module FlycalCli
16
- class Error < StandardError; end
17
- 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
@@ -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. 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.",
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
- "bullet list of considered calendars",
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
+ }