flycal-cli 0.5.0 → 0.6.1
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 +42 -10
- data/config/defaults.yml +6 -1
- data/lib/flycal_cli/cli.rb +81 -48
- data/lib/flycal_cli/config.rb +5 -0
- data/lib/flycal_cli/date_time_parser.rb +129 -0
- data/lib/flycal_cli/duration_parser.rb +15 -6
- data/lib/flycal_cli/locale.rb +6 -0
- data/lib/flycal_cli/slot_finder.rb +25 -10
- data/lib/flycal_cli/slot_formatter.rb +25 -0
- data/lib/flycal_cli/version.rb +1 -1
- data/lib/flycal_cli.rb +1 -0
- data/locales/en.json +3 -1
- data/locales/it.json +3 -1
- data/mcp/README.md +29 -0
- data/mcp/tools.json +318 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7333fcbb8ba227c7694b96cba4b35893e324c13e38bdc6d996e69f54cc3d64a
|
|
4
|
+
data.tar.gz: aab159e8f8e68368db1e45d1a799dd93386f79a0f2f9c29bb92aba776bcefa21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2036031048ebcb74e1954ff1d574051945407bdef922f161f4b1ad7e8be7cd1f3dfdec6dea2efe671502a1310ba834cee44426d837a3b7dfee4fb76fbd504117
|
|
7
|
+
data.tar.gz: 40646b4ce7bfaa3256a35c435cdf6dcb3913aaf2a3811370219e32c510fc88d7469d652b573973016d96b9e1d5f49cca8da159bfad7d787742dc8c0bba9239d0
|
data/README.md
CHANGED
|
@@ -128,8 +128,8 @@ flycal search -i 2months -d placeholder
|
|
|
128
128
|
|
|
129
129
|
**Options:**
|
|
130
130
|
|
|
131
|
-
- `--from` / `-f` — Start date/time. Default: midnight of current day.
|
|
132
|
-
- `--to` / `-t` — End date/time. Default: 23:59 of the 30th day from today.
|
|
131
|
+
- `--from` / `-f` — Start date/time. Default: midnight of current day. Formats: `YYYY-MM-DD`, locale forms (`DD-MM-YYYY` for `it`, `MM-DD-YYYY` for `en`, `/` or `-`), or with time `YYYY-MM-DDTHH:MM`
|
|
132
|
+
- `--to` / `-t` — End date/time. Default: 23:59 of the 30th day from today. Same formats as `--from`
|
|
133
133
|
- `--in` / `-i` — Duration from `--from`, overrides `--to`. Format: `30days`, `48hours`, `2months`, `1year` (no space). With space use quotes: `--in "30 days"`
|
|
134
134
|
- `--calendar` / `-c` — Calendar name or ID. Default: calendar set via `flycal config`
|
|
135
135
|
- `--description` / `-d` — Filter events by text. Matches events where the string appears in title or description (case-insensitive, contains)
|
|
@@ -153,28 +153,56 @@ For time frames longer than 7 days, a weekly breakdown is added (week number, st
|
|
|
153
153
|
|
|
154
154
|
### slots
|
|
155
155
|
|
|
156
|
-
Find free time slots in your calendar
|
|
156
|
+
Find free time slots in your calendar. Output is a simple list for copy/paste into email or other tools.
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
|
-
flycal slots
|
|
160
|
-
flycal slots --
|
|
161
|
-
flycal slots --in "
|
|
159
|
+
flycal slots
|
|
160
|
+
flycal slots --duration 1h
|
|
161
|
+
flycal slots --from 2026-08-01 --in "2 weeks" --duration 1h
|
|
162
|
+
flycal slots --from 01/08/2026 --in 3days --duration 30min
|
|
163
|
+
flycal slots --in 1week -c Work
|
|
162
164
|
```
|
|
163
165
|
|
|
164
166
|
**Options:**
|
|
165
167
|
|
|
166
|
-
- `--duration` —
|
|
167
|
-
- `--
|
|
168
|
+
- `--duration` — Slot length. Default: `slots.defaults.default_duration` in config (`45min`). Examples: `1h`, `30 minutes`
|
|
169
|
+
- `--from` / `-f` — Start date/time. Default: `slots.defaults.from` in config (`now`). If a date without time is today, starts from the current time.
|
|
170
|
+
- `--in` / `-i` — Search window from `--from`. Default: `1 week`.
|
|
168
171
|
- `--calendar` / `-c` — Calendar name or ID used as fallback when `exclude_calendars` is not configured
|
|
169
172
|
|
|
173
|
+
**Output:**
|
|
174
|
+
|
|
175
|
+
Always starts with a header and clickable calendar links (OSC 8), then the availability list:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
found 3 slots
|
|
179
|
+
from wed 29 July 2026 to wed 5 August 2026
|
|
180
|
+
with duration 45min
|
|
181
|
+
for calendars
|
|
182
|
+
Incode - Antonio
|
|
183
|
+
Personal
|
|
184
|
+
https://calendar.google.com/calendar/r/day/2026/7/29
|
|
185
|
+
|
|
186
|
+
friday 15/7
|
|
187
|
+
10-12
|
|
188
|
+
13-15.30
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If `pbcopy` is available (macOS), the slot list without the header is also copied to the clipboard.
|
|
192
|
+
|
|
170
193
|
Slot search windows are configured in `~/.flycal/config.yml`:
|
|
171
194
|
|
|
172
195
|
```yaml
|
|
173
196
|
slots:
|
|
197
|
+
defaults:
|
|
198
|
+
from: now
|
|
199
|
+
default_duration: 45min
|
|
200
|
+
free_before: 0m
|
|
201
|
+
free_after: 15m
|
|
174
202
|
exclude_calendars:
|
|
175
203
|
- user@example.com
|
|
176
204
|
- user@gmail.com
|
|
177
|
-
|
|
205
|
+
hours:
|
|
178
206
|
- 9:30-13:00
|
|
179
207
|
- 14:00-18:30
|
|
180
208
|
weekdays_only: true
|
|
@@ -183,8 +211,12 @@ locale: en
|
|
|
183
211
|
|
|
184
212
|
Missing keys are filled from `config/defaults.yml` in the gem and saved to your `config.yml` on first read.
|
|
185
213
|
|
|
214
|
+
- `defaults.from` — default `--from` for slots (`now` or a date string)
|
|
215
|
+
- `defaults.default_duration` — default slot length when `--duration` is omitted
|
|
216
|
+
- `free_before` — buffer before each slot; busy intervals are extended backward so no event may fall in this window before a slot
|
|
217
|
+
- `free_after` — buffer after each slot; gaps must fit `duration + free_after`, but output shows slots of `duration` only
|
|
186
218
|
- `exclude_calendars` lists calendars whose events block free slots; if empty or not set, events from `calendar_default` are used
|
|
187
|
-
- `
|
|
219
|
+
- `hours` accepts one or more ranges (`H-H`, `HH:MM-HH:MM`, mixed)
|
|
188
220
|
- `weekdays_only: true` limits slots to Monday-Friday
|
|
189
221
|
- `weekdays_only: false` includes weekends
|
|
190
222
|
- `locale` supports `en` and `it` (default is `en`)
|
data/config/defaults.yml
CHANGED
data/lib/flycal_cli/cli.rb
CHANGED
|
@@ -137,12 +137,12 @@ module FlycalCli
|
|
|
137
137
|
exit 1
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
-
time_min = options[:from].to_s.empty? ? Date.today.to_time :
|
|
140
|
+
time_min = options[:from].to_s.empty? ? Date.today.to_time : DateTimeParser.parse(options[:from])
|
|
141
141
|
begin
|
|
142
142
|
time_max = if options[:in].to_s.empty?
|
|
143
|
-
options[:to].to_s.empty? ? (Date.today + 30).to_time + 86400 - 1 :
|
|
143
|
+
options[:to].to_s.empty? ? (Date.today + 30).to_time + 86400 - 1 : DateTimeParser.parse(options[:to], end_of_day: true)
|
|
144
144
|
else
|
|
145
|
-
|
|
145
|
+
DurationParser.add_to_time(options[:in], time_min)
|
|
146
146
|
end
|
|
147
147
|
rescue FlycalCli::Error => e
|
|
148
148
|
puts "Error: #{e.message}"
|
|
@@ -178,14 +178,22 @@ module FlycalCli
|
|
|
178
178
|
long_desc <<-LONGDESC
|
|
179
179
|
List free time slots long enough for a given duration.
|
|
180
180
|
|
|
181
|
+
Defaults:
|
|
182
|
+
--from from config (default: now)
|
|
183
|
+
--in 1 week
|
|
184
|
+
--duration from config (default: 45min)
|
|
185
|
+
|
|
181
186
|
Examples:
|
|
182
|
-
flycal slots
|
|
183
|
-
flycal slots --
|
|
187
|
+
flycal slots
|
|
188
|
+
flycal slots --duration 1h
|
|
189
|
+
flycal slots --from 2026-08-01 --in "2 weeks" --duration 1h
|
|
184
190
|
LONGDESC
|
|
185
|
-
option :duration, type: :string,
|
|
186
|
-
desc: "
|
|
187
|
-
option :in, type: :string, aliases: "-i",
|
|
188
|
-
desc: "Search window from
|
|
191
|
+
option :duration, type: :string,
|
|
192
|
+
desc: "Slot length (default from config: slots.defaults.default_duration)"
|
|
193
|
+
option :in, type: :string, aliases: "-i", default: "1 week",
|
|
194
|
+
desc: "Search window from --from (default: 1 week)"
|
|
195
|
+
option :from, type: :string, aliases: "-f",
|
|
196
|
+
desc: "Start date/time (default from config: slots.defaults.from)"
|
|
189
197
|
option :calendar, type: :string, aliases: "-c", desc: "Calendar name or ID"
|
|
190
198
|
def slots
|
|
191
199
|
apply_locale_override
|
|
@@ -194,12 +202,26 @@ module FlycalCli
|
|
|
194
202
|
exit 1
|
|
195
203
|
end
|
|
196
204
|
|
|
205
|
+
slot_cfg = Config.slots_config
|
|
206
|
+
defaults = slot_cfg.fetch("defaults", {})
|
|
207
|
+
in_value = options[:in].to_s.strip
|
|
208
|
+
in_value = "1 week" if in_value.empty?
|
|
209
|
+
|
|
210
|
+
duration_value = options[:duration].to_s.strip
|
|
211
|
+
duration_value = defaults["default_duration"].to_s.strip if duration_value.empty?
|
|
212
|
+
duration_value = "45min" if duration_value.empty?
|
|
213
|
+
|
|
214
|
+
from_value = options[:from].to_s.strip
|
|
215
|
+
from_value = defaults["from"].to_s.strip if from_value.empty?
|
|
216
|
+
from_value = "now" if from_value.empty?
|
|
217
|
+
|
|
197
218
|
begin
|
|
198
|
-
slot_duration = DurationParser.to_seconds(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
slot_duration = DurationParser.to_seconds(duration_value)
|
|
220
|
+
free_before = DurationParser.to_seconds(slot_cfg["free_before"] || "0m")
|
|
221
|
+
free_after = DurationParser.to_seconds(slot_cfg["free_after"] || "0m")
|
|
222
|
+
time_min = parse_slots_from(from_value)
|
|
223
|
+
time_max = DurationParser.add_to_time(in_value, time_min)
|
|
224
|
+
workhours = parse_workhours(slot_cfg["hours"])
|
|
203
225
|
weekdays_only = !!slot_cfg["weekdays_only"]
|
|
204
226
|
rescue FlycalCli::Error => e
|
|
205
227
|
puts "Error: #{e.message}"
|
|
@@ -220,10 +242,18 @@ module FlycalCli
|
|
|
220
242
|
exit 1
|
|
221
243
|
end
|
|
222
244
|
|
|
245
|
+
calendars = service.list_calendars
|
|
246
|
+
calendar_meta = exclude_calendar_ids.filter_map do |id|
|
|
247
|
+
cal = calendars.find { |c| c.id == id }
|
|
248
|
+
name = cal&.summary || id
|
|
249
|
+
{ id: id, name: name }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
fetch_from = time_min - free_before
|
|
223
253
|
events = exclude_calendar_ids.flat_map do |calendar_id|
|
|
224
254
|
service.list_events(
|
|
225
255
|
calendar_id,
|
|
226
|
-
time_min:
|
|
256
|
+
time_min: fetch_from,
|
|
227
257
|
time_max: time_max
|
|
228
258
|
)
|
|
229
259
|
rescue Google::Apis::Errors::Error => e
|
|
@@ -237,11 +267,28 @@ module FlycalCli
|
|
|
237
267
|
time_max: time_max,
|
|
238
268
|
slot_duration_seconds: slot_duration,
|
|
239
269
|
workhours: workhours,
|
|
240
|
-
weekdays_only: weekdays_only
|
|
270
|
+
weekdays_only: weekdays_only,
|
|
271
|
+
free_before_seconds: free_before,
|
|
272
|
+
free_after_seconds: free_after
|
|
241
273
|
)
|
|
242
274
|
|
|
243
|
-
|
|
244
|
-
|
|
275
|
+
slots_by_day = finder.slots_by_day
|
|
276
|
+
slot_count = slots_by_day.values.sum(&:size)
|
|
277
|
+
puts SlotFormatter.format_header(
|
|
278
|
+
from: time_min,
|
|
279
|
+
to: time_max,
|
|
280
|
+
duration: duration_value,
|
|
281
|
+
calendars: calendar_meta,
|
|
282
|
+
count: slot_count
|
|
283
|
+
)
|
|
284
|
+
puts ""
|
|
285
|
+
output = SlotFormatter.format_output(slots_by_day)
|
|
286
|
+
if output.empty?
|
|
287
|
+
puts Locale.t("slots.no_available")
|
|
288
|
+
else
|
|
289
|
+
puts output
|
|
290
|
+
copy_slots_to_clipboard(output)
|
|
291
|
+
end
|
|
245
292
|
end
|
|
246
293
|
|
|
247
294
|
desc "update", "Update flycal-cli to the latest gem version"
|
|
@@ -419,38 +466,24 @@ module FlycalCli
|
|
|
419
466
|
end
|
|
420
467
|
end
|
|
421
468
|
|
|
422
|
-
def
|
|
423
|
-
return
|
|
469
|
+
def parse_slots_from(value)
|
|
470
|
+
return Time.now if value.to_s.strip.downcase == "now"
|
|
424
471
|
|
|
425
|
-
|
|
426
|
-
Time.parse(str)
|
|
427
|
-
else
|
|
428
|
-
d = Date.parse(str)
|
|
429
|
-
t = d.to_time
|
|
430
|
-
end_of_day ? t + 86400 - 1 : t # 23:59:59 for --to when date-only
|
|
431
|
-
end
|
|
472
|
+
DateTimeParser.parse(value)
|
|
432
473
|
end
|
|
433
474
|
|
|
434
|
-
def
|
|
435
|
-
|
|
436
|
-
m = str.to_s.strip.match(/\A(\d+)\s*(day|days|d|hour|hours|h|month|months|m|year|years|y)\z/i)
|
|
437
|
-
raise FlycalCli::Error, "Invalid --in format. Use: 30days, 48hours, 2months, 1year (no space, or quote: --in \"30 days\")" unless m
|
|
475
|
+
def copy_slots_to_clipboard(text)
|
|
476
|
+
return unless pbcopy_available?
|
|
438
477
|
|
|
439
|
-
|
|
440
|
-
|
|
478
|
+
IO.popen("pbcopy", "w") { |io| io.write(text) }
|
|
479
|
+
puts ""
|
|
480
|
+
puts Locale.t("slots.copied_to_clipboard")
|
|
481
|
+
rescue StandardError
|
|
482
|
+
nil
|
|
483
|
+
end
|
|
441
484
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
from_time + n * 3600
|
|
445
|
-
when "day", "days", "d"
|
|
446
|
-
from_time + n * 86400
|
|
447
|
-
when "month", "months", "m"
|
|
448
|
-
(from_time.to_date >> n).to_time + (from_time.hour * 3600 + from_time.min * 60 + from_time.sec)
|
|
449
|
-
when "year", "years", "y"
|
|
450
|
-
(from_time.to_date >> (n * 12)).to_time + (from_time.hour * 3600 + from_time.min * 60 + from_time.sec)
|
|
451
|
-
else
|
|
452
|
-
raise FlycalCli::Error, "Invalid unit: #{unit}. Use: days, hours, months, years"
|
|
453
|
-
end
|
|
485
|
+
def pbcopy_available?
|
|
486
|
+
system("which", "pbcopy", out: File::NULL, err: File::NULL)
|
|
454
487
|
end
|
|
455
488
|
|
|
456
489
|
def parse_hour_minute(value)
|
|
@@ -469,18 +502,18 @@ module FlycalCli
|
|
|
469
502
|
def parse_workhours(values)
|
|
470
503
|
ranges = Array(values).map do |item|
|
|
471
504
|
start_str, end_str = item.to_s.strip.split("-", 2)
|
|
472
|
-
raise FlycalCli::Error, "Invalid
|
|
505
|
+
raise FlycalCli::Error, "Invalid hours item #{item.inspect}. Use format like '9-13' or '14:30-18:00'." if start_str.nil? || end_str.nil?
|
|
473
506
|
|
|
474
507
|
sh, sm = parse_hour_minute_flexible(start_str)
|
|
475
508
|
eh, em = parse_hour_minute_flexible(end_str)
|
|
476
509
|
start_minutes = (sh * 60) + sm
|
|
477
510
|
end_minutes = (eh * 60) + em
|
|
478
|
-
raise FlycalCli::Error, "Invalid
|
|
511
|
+
raise FlycalCli::Error, "Invalid hours range #{item.inspect}: end must be after start." if end_minutes <= start_minutes
|
|
479
512
|
|
|
480
513
|
[sh, sm, eh, em]
|
|
481
514
|
end
|
|
482
515
|
|
|
483
|
-
raise FlycalCli::Error, "slots.
|
|
516
|
+
raise FlycalCli::Error, "slots.hours cannot be empty in config.yml." if ranges.empty?
|
|
484
517
|
|
|
485
518
|
ranges.sort_by { |sh, sm, _eh, _em| (sh * 60) + sm }
|
|
486
519
|
end
|
data/lib/flycal_cli/config.rb
CHANGED
|
@@ -93,6 +93,11 @@ module FlycalCli
|
|
|
93
93
|
changed = true
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
if normalized.key?("workhours") && !normalized.key?("hours")
|
|
97
|
+
normalized["hours"] = normalized.delete("workhours")
|
|
98
|
+
changed = true
|
|
99
|
+
end
|
|
100
|
+
|
|
96
101
|
return [data, false] unless changed
|
|
97
102
|
|
|
98
103
|
data = data.dup
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require "active_support/core_ext/time"
|
|
6
|
+
require "active_support/core_ext/date"
|
|
7
|
+
|
|
8
|
+
module FlycalCli
|
|
9
|
+
# Parses CLI date/time arguments in a locale-aware way.
|
|
10
|
+
#
|
|
11
|
+
# Always accepts ISO-like forms:
|
|
12
|
+
# YYYY-MM-DD, YYYY/MM/DD
|
|
13
|
+
# YYYY-MM-DDTHH:MM, YYYY-MM-DD HH:MM(:SS)
|
|
14
|
+
#
|
|
15
|
+
# With locale +it+: DD-MM-YYYY / DD/MM/YYYY (+ optional time)
|
|
16
|
+
# With locale +en+ (default): MM-DD-YYYY / MM/DD/YYYY (+ optional time)
|
|
17
|
+
class DateTimeParser
|
|
18
|
+
TIME_SUFFIX = '(?:\s+|T)(\d{1,2}):(\d{2})(?::(\d{2}))?'.freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def parse(str, end_of_day: false, locale: nil)
|
|
22
|
+
value = str.to_s.strip
|
|
23
|
+
raise FlycalCli::Error, invalid_message(str) if value.empty?
|
|
24
|
+
|
|
25
|
+
loc = (locale || Locale.current_locale).to_s
|
|
26
|
+
formats = formats_for(loc)
|
|
27
|
+
|
|
28
|
+
formats.each do |pattern, order|
|
|
29
|
+
match = value.match(pattern)
|
|
30
|
+
next unless match
|
|
31
|
+
|
|
32
|
+
year, month, day, hour, min, sec = extract_parts(match, order)
|
|
33
|
+
return build_time(year, month, day, hour, min, sec, end_of_day: end_of_day)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Last resort: ISO8601 / Time.parse for full timestamps
|
|
37
|
+
begin
|
|
38
|
+
return Time.iso8601(value)
|
|
39
|
+
rescue ArgumentError
|
|
40
|
+
# fall through
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
parsed = Time.parse(value)
|
|
45
|
+
return end_of_day && !time_component?(value) ? end_of_day_for(parsed.to_date) : parsed
|
|
46
|
+
rescue ArgumentError
|
|
47
|
+
raise FlycalCli::Error, invalid_message(str)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_or_default(str, default: Time.now, end_of_day: false, locale: nil)
|
|
52
|
+
return default if str.nil? || str.to_s.strip.empty?
|
|
53
|
+
|
|
54
|
+
parse(str, end_of_day: end_of_day, locale: locale)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def formats_for(locale)
|
|
60
|
+
iso = [
|
|
61
|
+
[/\A(\d{4})[-\/](\d{1,2})[-\/](\d{1,2})#{TIME_SUFFIX}\z/i, %i[year month day hour min sec]],
|
|
62
|
+
[/\A(\d{4})[-\/](\d{1,2})[-\/](\d{1,2})\z/, %i[year month day]]
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
local =
|
|
66
|
+
if locale == "it"
|
|
67
|
+
[
|
|
68
|
+
[/\A(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})#{TIME_SUFFIX}\z/i, %i[day month year hour min sec]],
|
|
69
|
+
[/\A(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})\z/, %i[day month year]]
|
|
70
|
+
]
|
|
71
|
+
else
|
|
72
|
+
[
|
|
73
|
+
[/\A(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})#{TIME_SUFFIX}\z/i, %i[month day year hour min sec]],
|
|
74
|
+
[/\A(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})\z/, %i[month day year]]
|
|
75
|
+
]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
iso + local
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_parts(match, order)
|
|
82
|
+
parts = {}
|
|
83
|
+
order.each_with_index do |key, idx|
|
|
84
|
+
parts[key] = match[idx + 1]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
[
|
|
88
|
+
parts[:year].to_i,
|
|
89
|
+
parts[:month].to_i,
|
|
90
|
+
parts[:day].to_i,
|
|
91
|
+
parts[:hour]&.to_i,
|
|
92
|
+
parts[:min]&.to_i,
|
|
93
|
+
parts[:sec]&.to_i
|
|
94
|
+
]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_time(year, month, day, hour, min, sec, end_of_day:)
|
|
98
|
+
date = Date.new(year, month, day)
|
|
99
|
+
|
|
100
|
+
if hour.nil?
|
|
101
|
+
return end_of_day_for(date) if end_of_day
|
|
102
|
+
return [date.to_time, Time.now].max if date == Date.today
|
|
103
|
+
|
|
104
|
+
return date.to_time
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
Time.local(year, month, day, hour, min || 0, sec || 0)
|
|
108
|
+
rescue ArgumentError
|
|
109
|
+
raise FlycalCli::Error, "Invalid date: #{year}-#{month}-#{day}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def end_of_day_for(date)
|
|
113
|
+
Time.local(date.year, date.month, date.day, 23, 59, 59)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def time_component?(value)
|
|
117
|
+
value.match?(/T|\d{1,2}:\d{2}/)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def invalid_message(str)
|
|
121
|
+
if Locale.current_locale.to_s == "it"
|
|
122
|
+
"Data non valida: #{str.inspect}. Usa YYYY-MM-DD, DD-MM-YYYY (anche con /) o con orario."
|
|
123
|
+
else
|
|
124
|
+
"Invalid date: #{str.inspect}. Use YYYY-MM-DD, MM-DD-YYYY (also with /), or with time."
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -10,7 +10,9 @@ module FlycalCli
|
|
|
10
10
|
"m" => :minutes, "min" => :minutes, "mins" => :minutes, "minute" => :minutes, "minutes" => :minutes,
|
|
11
11
|
"h" => :hours, "hr" => :hours, "hour" => :hours, "hours" => :hours,
|
|
12
12
|
"d" => :days, "day" => :days, "days" => :days,
|
|
13
|
-
"w" => :weeks, "week" => :weeks, "weeks" => :weeks
|
|
13
|
+
"w" => :weeks, "week" => :weeks, "weeks" => :weeks,
|
|
14
|
+
"month" => :months, "months" => :months,
|
|
15
|
+
"y" => :years, "year" => :years, "years" => :years
|
|
14
16
|
}.freeze
|
|
15
17
|
|
|
16
18
|
class << self
|
|
@@ -21,11 +23,18 @@ module FlycalCli
|
|
|
21
23
|
raise FlycalCli::Error, invalid_message(str) unless match
|
|
22
24
|
|
|
23
25
|
value = match[1].to_f
|
|
24
|
-
|
|
26
|
+
unit_key = match[2]
|
|
27
|
+
unit = UNITS[unit_key]
|
|
25
28
|
raise FlycalCli::Error, invalid_message(str) unless unit
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
# Ambiguous bare "m": treat as minutes (not months)
|
|
30
|
+
unit = :minutes if unit_key == "m"
|
|
31
|
+
|
|
32
|
+
# months/years are Integer-only ActiveSupport extensions
|
|
33
|
+
if %i[months years].include?(unit)
|
|
34
|
+
value.to_i.public_send(unit)
|
|
35
|
+
else
|
|
36
|
+
value.public_send(unit)
|
|
37
|
+
end
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
def to_seconds(str)
|
|
@@ -39,7 +48,7 @@ module FlycalCli
|
|
|
39
48
|
private
|
|
40
49
|
|
|
41
50
|
def invalid_message(str)
|
|
42
|
-
"Invalid duration: #{str.inspect}. Examples: 1h, 1
|
|
51
|
+
"Invalid duration: #{str.inspect}. Examples: 1h, 30 minutes, 3 days, 1 week, 2 months, 1 year"
|
|
43
52
|
end
|
|
44
53
|
end
|
|
45
54
|
end
|
data/lib/flycal_cli/locale.rb
CHANGED
|
@@ -31,6 +31,12 @@ module FlycalCli
|
|
|
31
31
|
months[idx] || date_or_time.strftime("%B")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def format_long_date(date_or_time)
|
|
35
|
+
t = date_or_time.respond_to?(:to_time) ? date_or_time.to_time : date_or_time
|
|
36
|
+
d = t.to_date
|
|
37
|
+
"#{day_abbr(d).downcase} #{d.day} #{month_name(d)} #{d.year}"
|
|
38
|
+
end
|
|
39
|
+
|
|
34
40
|
def current_locale
|
|
35
41
|
Thread.current[:flycal_locale_override] || Config.locale
|
|
36
42
|
rescue StandardError
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module FlycalCli
|
|
4
4
|
class SlotFinder
|
|
5
5
|
DEFAULT_WORKHOURS = [[9, 0, 18, 0]].freeze
|
|
6
|
+
STEP_SECONDS = 900
|
|
6
7
|
|
|
7
8
|
def initialize(
|
|
8
9
|
events:,
|
|
@@ -10,7 +11,9 @@ module FlycalCli
|
|
|
10
11
|
time_max:,
|
|
11
12
|
slot_duration_seconds:,
|
|
12
13
|
workhours: DEFAULT_WORKHOURS,
|
|
13
|
-
weekdays_only: true
|
|
14
|
+
weekdays_only: true,
|
|
15
|
+
free_before_seconds: 0,
|
|
16
|
+
free_after_seconds: 0
|
|
14
17
|
)
|
|
15
18
|
@events = events
|
|
16
19
|
@time_min = time_min
|
|
@@ -18,6 +21,8 @@ module FlycalCli
|
|
|
18
21
|
@slot_duration_seconds = slot_duration_seconds
|
|
19
22
|
@workhours = workhours
|
|
20
23
|
@weekdays_only = weekdays_only
|
|
24
|
+
@free_before_seconds = free_before_seconds
|
|
25
|
+
@free_after_seconds = free_after_seconds
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def slots_by_day
|
|
@@ -30,7 +35,9 @@ module FlycalCli
|
|
|
30
35
|
|
|
31
36
|
busy = busy_intervals_for(date, day_start, day_end)
|
|
32
37
|
gaps = free_gaps(day_start, day_end, busy)
|
|
33
|
-
|
|
38
|
+
gaps.each do |gap_start, gap_end|
|
|
39
|
+
slots.concat(slots_in_gap(gap_start, gap_end))
|
|
40
|
+
end
|
|
34
41
|
end
|
|
35
42
|
result[date] = slots unless slots.empty?
|
|
36
43
|
end
|
|
@@ -74,7 +81,14 @@ module FlycalCli
|
|
|
74
81
|
intervals = @events.filter_map do |event|
|
|
75
82
|
interval_for_event(event, date, day_start, day_end)
|
|
76
83
|
end
|
|
77
|
-
merge_intervals(intervals)
|
|
84
|
+
merge_intervals(intervals.map { |start_at, end_at| pad_busy_interval(start_at, end_at) })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def pad_busy_interval(start_at, end_at)
|
|
88
|
+
[
|
|
89
|
+
start_at - @free_before_seconds,
|
|
90
|
+
end_at + @free_after_seconds
|
|
91
|
+
]
|
|
78
92
|
end
|
|
79
93
|
|
|
80
94
|
def interval_for_event(event, date, day_start, day_end)
|
|
@@ -127,26 +141,27 @@ module FlycalCli
|
|
|
127
141
|
gaps
|
|
128
142
|
end
|
|
129
143
|
|
|
130
|
-
def
|
|
144
|
+
def slots_in_gap(start_at, end_at)
|
|
131
145
|
rounded_start = round_up_15(start_at)
|
|
132
146
|
rounded_end = round_down_15(end_at)
|
|
133
|
-
return
|
|
134
|
-
return
|
|
147
|
+
return [] if rounded_start >= rounded_end
|
|
148
|
+
return [] if (rounded_end - rounded_start) < @slot_duration_seconds
|
|
135
149
|
|
|
136
|
-
|
|
150
|
+
# One continuous free range (not sliding windows of fixed duration)
|
|
151
|
+
[[rounded_start, rounded_end]]
|
|
137
152
|
end
|
|
138
153
|
|
|
139
154
|
def round_up_15(time)
|
|
140
155
|
sec = time.to_i
|
|
141
|
-
remainder = sec %
|
|
156
|
+
remainder = sec % STEP_SECONDS
|
|
142
157
|
return time if remainder.zero?
|
|
143
158
|
|
|
144
|
-
Time.at(sec + (
|
|
159
|
+
Time.at(sec + (STEP_SECONDS - remainder))
|
|
145
160
|
end
|
|
146
161
|
|
|
147
162
|
def round_down_15(time)
|
|
148
163
|
sec = time.to_i
|
|
149
|
-
Time.at(sec - (sec %
|
|
164
|
+
Time.at(sec - (sec % STEP_SECONDS))
|
|
150
165
|
end
|
|
151
166
|
|
|
152
167
|
def to_time(value)
|
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
module FlycalCli
|
|
4
4
|
class SlotFormatter
|
|
5
5
|
class << self
|
|
6
|
+
def format_header(from:, to:, duration:, calendars:, count:)
|
|
7
|
+
lines = []
|
|
8
|
+
lines << Locale.t(
|
|
9
|
+
"slots.header",
|
|
10
|
+
count: count,
|
|
11
|
+
from: underline(Locale.format_long_date(from)),
|
|
12
|
+
to: underline(Locale.format_long_date(to)),
|
|
13
|
+
duration: underline(duration)
|
|
14
|
+
)
|
|
15
|
+
calendars.each do |cal|
|
|
16
|
+
lines << cal[:name].to_s
|
|
17
|
+
end
|
|
18
|
+
lines << google_calendar_day_url(from)
|
|
19
|
+
lines.join("\n")
|
|
20
|
+
end
|
|
21
|
+
|
|
6
22
|
def format_output(slots_by_day)
|
|
7
23
|
lines = []
|
|
8
24
|
|
|
@@ -19,6 +35,15 @@ module FlycalCli
|
|
|
19
35
|
|
|
20
36
|
private
|
|
21
37
|
|
|
38
|
+
def underline(str)
|
|
39
|
+
"\e[4m#{str}\e[0m"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def google_calendar_day_url(from)
|
|
43
|
+
t = from.respond_to?(:to_time) ? from.to_time : from
|
|
44
|
+
"https://calendar.google.com/calendar/r/day/#{t.year}/#{t.month}/#{t.day}"
|
|
45
|
+
end
|
|
46
|
+
|
|
22
47
|
def day_header(date)
|
|
23
48
|
"#{Locale.day_name(date)} #{date.day}/#{date.month}"
|
|
24
49
|
end
|
data/lib/flycal_cli/version.rb
CHANGED
data/lib/flycal_cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "flycal_cli/config"
|
|
|
5
5
|
require "flycal_cli/locale"
|
|
6
6
|
require "flycal_cli/auth"
|
|
7
7
|
require "flycal_cli/duration_parser"
|
|
8
|
+
require "flycal_cli/date_time_parser"
|
|
8
9
|
require "flycal_cli/calendar_service"
|
|
9
10
|
require "flycal_cli/slot_finder"
|
|
10
11
|
require "flycal_cli/slot_formatter"
|
data/locales/en.json
CHANGED
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"slots": {
|
|
32
32
|
"no_available": "No available slots found.",
|
|
33
|
-
"no_calendar": "No calendar found. Run 'flycal config' to set a default."
|
|
33
|
+
"no_calendar": "No calendar found. Run 'flycal config' to set a default.",
|
|
34
|
+
"header": "found %{count} slots\nfrom %{from} to %{to}\nwith duration %{duration}\nfor calendars",
|
|
35
|
+
"copied_to_clipboard": "✓ Slot list copied to clipboard (pbcopy)"
|
|
34
36
|
},
|
|
35
37
|
"errors": {
|
|
36
38
|
"not_connected": "You are not connected. Run 'flycal login' first.",
|
data/locales/it.json
CHANGED
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"slots": {
|
|
32
32
|
"no_available": "Nessuno slot disponibile.",
|
|
33
|
-
"no_calendar": "Nessun calendario trovato. Esegui 'flycal config' per impostare il predefinito."
|
|
33
|
+
"no_calendar": "Nessun calendario trovato. Esegui 'flycal config' per impostare il predefinito.",
|
|
34
|
+
"header": "trovati %{count} slot\nda %{from} a %{to}\ncon durata %{duration}\nper i calendari",
|
|
35
|
+
"copied_to_clipboard": "✓ Lista slot copiata negli appunti (pbcopy)"
|
|
34
36
|
},
|
|
35
37
|
"errors": {
|
|
36
38
|
"not_connected": "Non sei connesso. Esegui prima 'flycal login'.",
|
data/mcp/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# flycal MCP catalog
|
|
2
|
+
|
|
3
|
+
This directory contains a machine-readable description of **flycal-cli** commands for MCP (Model Context Protocol) servers.
|
|
4
|
+
|
|
5
|
+
## File
|
|
6
|
+
|
|
7
|
+
- [`tools.json`](./tools.json) — tools catalog with MCP-style `inputSchema`, CLI argv templates, auth/interactivity flags, and config keys.
|
|
8
|
+
|
|
9
|
+
## How an MCP server can use it
|
|
10
|
+
|
|
11
|
+
1. Load `tools.json` at startup.
|
|
12
|
+
2. Register each entry in `tools` with your MCP SDK (`server.tool(...)`), using `name`, `description`, and `inputSchema`.
|
|
13
|
+
3. On tool call, build the argv:
|
|
14
|
+
- static tools → `command.argv`
|
|
15
|
+
- parameterized tools → start from `command.argv_template`, then append pairs from `command.option_map` for each provided argument
|
|
16
|
+
- if `locale` is set → append `--locale <value>`
|
|
17
|
+
4. Run `flycal` as a subprocess and return stdout as text content.
|
|
18
|
+
5. Skip or gate tools with `"interactive": true` (`flycal_login`, `flycal_config`) unless a TTY is available.
|
|
19
|
+
|
|
20
|
+
## Suggested tools to expose
|
|
21
|
+
|
|
22
|
+
| MCP tool | CLI | Notes |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `flycal_search` | `flycal search ...` | Primary read tool |
|
|
25
|
+
| `flycal_slots` | `flycal slots --in ... --duration ...` | Free slots |
|
|
26
|
+
| `flycal_calendars` | `flycal calendars` | List name + id |
|
|
27
|
+
| `flycal_version` | `flycal version` | Health / version check |
|
|
28
|
+
|
|
29
|
+
Interactive / side-effect tools (`login`, `logout`, `config`, `update`) are documented in the catalog but are usually better left to the user terminal.
|
data/mcp/tools.json
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"name": "flycal-cli",
|
|
4
|
+
"title": "flycal CLI MCP tools catalog",
|
|
5
|
+
"description": "Machine-readable catalog of flycal-cli commands for MCP servers. Each tool maps to a CLI invocation via the `command` template. Non-interactive tools are safe to run from an MCP host; interactive tools require a TTY and should be surfaced as guidance only or refused unless the client can attach a terminal.",
|
|
6
|
+
"version": "0.5.0",
|
|
7
|
+
"binary": "flycal",
|
|
8
|
+
"homepage": "https://github.com/magnum/flycal-cli",
|
|
9
|
+
"locale": {
|
|
10
|
+
"global_option": "--locale",
|
|
11
|
+
"supported": ["en", "it"],
|
|
12
|
+
"default": "en",
|
|
13
|
+
"config_key": "locale"
|
|
14
|
+
},
|
|
15
|
+
"config": {
|
|
16
|
+
"path": "~/.flycal/config.yml",
|
|
17
|
+
"credentials_path": "~/.flycal/credentials.json",
|
|
18
|
+
"keys": {
|
|
19
|
+
"calendar_default": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Default Google Calendar ID used by search and as slots fallback"
|
|
22
|
+
},
|
|
23
|
+
"locale": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": ["en", "it"],
|
|
26
|
+
"description": "Default CLI locale"
|
|
27
|
+
},
|
|
28
|
+
"slots.hours": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": { "type": "string" },
|
|
31
|
+
"description": "Slot search hour ranges, e.g. [\"9:30-13:00\", \"14:00-18:30\"]"
|
|
32
|
+
},
|
|
33
|
+
"slots.defaults.from": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Default --from for slots (e.g. now or a date string)"
|
|
36
|
+
},
|
|
37
|
+
"slots.defaults.default_duration": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Default slot duration when --duration is omitted (e.g. 45min)"
|
|
40
|
+
},
|
|
41
|
+
"slots.free_before": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Buffer before each slot; events block this window before slot start"
|
|
44
|
+
},
|
|
45
|
+
"slots.free_after": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Buffer after each slot; gaps must fit duration + free_after"
|
|
48
|
+
},
|
|
49
|
+
"slots.weekdays_only": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"description": "When true, slots are searched Mon–Fri only"
|
|
52
|
+
},
|
|
53
|
+
"slots.exclude_calendars": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": { "type": "string" },
|
|
56
|
+
"description": "Calendar IDs whose events block free slots. If empty, calendar_default is used."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"notes": [
|
|
61
|
+
"flycal is read-only against Google Calendar (no create/update/delete of events).",
|
|
62
|
+
"Auth uses OAuth desktop flow; tokens live under ~/.flycal/.",
|
|
63
|
+
"Prefer quoting duration values that contain spaces (e.g. --in \"3 days\").",
|
|
64
|
+
"Duration formats without spaces also work (e.g. 30days, 48hours, 1h, 30min)."
|
|
65
|
+
],
|
|
66
|
+
"tools": [
|
|
67
|
+
{
|
|
68
|
+
"name": "flycal_version",
|
|
69
|
+
"title": "Show flycal version",
|
|
70
|
+
"description": "Print the installed flycal-cli version.",
|
|
71
|
+
"interactive": false,
|
|
72
|
+
"requires_auth": false,
|
|
73
|
+
"command": {
|
|
74
|
+
"argv": ["flycal", "version"],
|
|
75
|
+
"alternatives": [["flycal", "--version"], ["flycal", "-v"]]
|
|
76
|
+
},
|
|
77
|
+
"inputSchema": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"additionalProperties": false,
|
|
80
|
+
"properties": {
|
|
81
|
+
"locale": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"enum": ["en", "it"],
|
|
84
|
+
"description": "Optional locale override for this invocation"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "flycal_update",
|
|
91
|
+
"title": "Update flycal gem",
|
|
92
|
+
"description": "Run gem update flycal-cli to install the latest published version.",
|
|
93
|
+
"interactive": false,
|
|
94
|
+
"requires_auth": false,
|
|
95
|
+
"side_effects": ["updates_local_gem"],
|
|
96
|
+
"command": {
|
|
97
|
+
"argv": ["flycal", "update"]
|
|
98
|
+
},
|
|
99
|
+
"inputSchema": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"additionalProperties": false,
|
|
102
|
+
"properties": {
|
|
103
|
+
"locale": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"enum": ["en", "it"]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "flycal_login",
|
|
112
|
+
"title": "Connect Google account",
|
|
113
|
+
"description": "Start OAuth browser login for Google Calendar. Requires ~/.flycal/credentials.json and a browser. Interactive.",
|
|
114
|
+
"interactive": true,
|
|
115
|
+
"requires_auth": false,
|
|
116
|
+
"side_effects": ["writes_tokens"],
|
|
117
|
+
"command": {
|
|
118
|
+
"argv": ["flycal", "login"]
|
|
119
|
+
},
|
|
120
|
+
"inputSchema": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"additionalProperties": false,
|
|
123
|
+
"properties": {
|
|
124
|
+
"locale": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"enum": ["en", "it"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "flycal_logout",
|
|
133
|
+
"title": "Disconnect Google account",
|
|
134
|
+
"description": "Remove stored Google OAuth tokens and disconnect the account.",
|
|
135
|
+
"interactive": false,
|
|
136
|
+
"requires_auth": false,
|
|
137
|
+
"side_effects": ["deletes_tokens"],
|
|
138
|
+
"command": {
|
|
139
|
+
"argv": ["flycal", "logout"]
|
|
140
|
+
},
|
|
141
|
+
"inputSchema": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"additionalProperties": false,
|
|
144
|
+
"properties": {
|
|
145
|
+
"locale": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"enum": ["en", "it"]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "flycal_calendars",
|
|
154
|
+
"title": "List calendars",
|
|
155
|
+
"description": "List available Google calendars as lines of \"name id\". Does not change configuration.",
|
|
156
|
+
"interactive": false,
|
|
157
|
+
"requires_auth": true,
|
|
158
|
+
"command": {
|
|
159
|
+
"argv": ["flycal", "calendars"]
|
|
160
|
+
},
|
|
161
|
+
"output": {
|
|
162
|
+
"format": "text/lines",
|
|
163
|
+
"example": "Work molinari@incode.it\nPersonal user@gmail.com"
|
|
164
|
+
},
|
|
165
|
+
"inputSchema": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"additionalProperties": false,
|
|
168
|
+
"properties": {
|
|
169
|
+
"locale": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"enum": ["en", "it"]
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "flycal_config",
|
|
178
|
+
"title": "Configure flycal (interactive)",
|
|
179
|
+
"description": "Interactive configuration menu. Options: calendar_default (single select), exclude_calendars (multi-select, min 1), edit config (opens ~/.flycal/config.yml in $EDITOR or vi). Ctrl+C prints \"config cancelled...\". Prefer editing config.yml or using dedicated non-interactive flows from MCP hosts.",
|
|
180
|
+
"interactive": true,
|
|
181
|
+
"requires_auth": true,
|
|
182
|
+
"side_effects": ["writes_config"],
|
|
183
|
+
"command": {
|
|
184
|
+
"argv": ["flycal", "config"]
|
|
185
|
+
},
|
|
186
|
+
"menu_options": [
|
|
187
|
+
"calendar_default",
|
|
188
|
+
"exclude_calendars",
|
|
189
|
+
"edit config"
|
|
190
|
+
],
|
|
191
|
+
"inputSchema": {
|
|
192
|
+
"type": "object",
|
|
193
|
+
"additionalProperties": false,
|
|
194
|
+
"properties": {
|
|
195
|
+
"locale": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"enum": ["en", "it"]
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "flycal_search",
|
|
204
|
+
"title": "Search calendar events",
|
|
205
|
+
"description": "Search Google Calendar events. Prints one line per event (Calendar | Start | End | Title) plus a summary with total occupied time in hours/minutes and 8-hour working days. For ranges >7 days adds a weekly breakdown; for >30 days a monthly breakdown. Contains-filter on title/description when description is set. Read-only.",
|
|
206
|
+
"interactive": false,
|
|
207
|
+
"requires_auth": true,
|
|
208
|
+
"command": {
|
|
209
|
+
"argv_template": ["flycal", "search"],
|
|
210
|
+
"option_map": {
|
|
211
|
+
"calendar": ["--calendar", "{value}"],
|
|
212
|
+
"from": ["--from", "{value}"],
|
|
213
|
+
"to": ["--to", "{value}"],
|
|
214
|
+
"in": ["--in", "{value}"],
|
|
215
|
+
"description": ["--description", "{value}"],
|
|
216
|
+
"locale": ["--locale", "{value}"]
|
|
217
|
+
},
|
|
218
|
+
"examples": [
|
|
219
|
+
["flycal", "search"],
|
|
220
|
+
["flycal", "search", "--in", "30days", "--description", "placeholder"],
|
|
221
|
+
["flycal", "search", "-f", "2025-03-01", "-t", "2025-03-31", "-c", "Work"],
|
|
222
|
+
["flycal", "search", "--in", "2months", "-d", "placeholder", "--locale", "it"]
|
|
223
|
+
]
|
|
224
|
+
},
|
|
225
|
+
"inputSchema": {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"additionalProperties": false,
|
|
228
|
+
"properties": {
|
|
229
|
+
"calendar": {
|
|
230
|
+
"type": "string",
|
|
231
|
+
"description": "Calendar name or ID. Default: calendar_default from config."
|
|
232
|
+
},
|
|
233
|
+
"from": {
|
|
234
|
+
"type": "string",
|
|
235
|
+
"description": "Start datetime. Default: today midnight. Formats: YYYY-MM-DD or YYYY-MM-DDTHH:MM"
|
|
236
|
+
},
|
|
237
|
+
"to": {
|
|
238
|
+
"type": "string",
|
|
239
|
+
"description": "End datetime. Default: 23:59 of day 30 from today. Ignored when `in` is set."
|
|
240
|
+
},
|
|
241
|
+
"in": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"description": "Duration from `from` (or now/from default), overrides `to`. Prefer no spaces or quote values: 30days, 48hours, 2months, 1year, or \"30 days\"."
|
|
244
|
+
},
|
|
245
|
+
"description": {
|
|
246
|
+
"type": "string",
|
|
247
|
+
"description": "Case-insensitive contains filter on event summary or description."
|
|
248
|
+
},
|
|
249
|
+
"locale": {
|
|
250
|
+
"type": "string",
|
|
251
|
+
"enum": ["en", "it"]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"name": "flycal_slots",
|
|
258
|
+
"title": "Find free time slots",
|
|
259
|
+
"description": "Find free slots within configured hours, subtracting busy events from slots.exclude_calendars (or calendar_default if that list is empty). Starts from --from (default: config slots.defaults.from or now) for a window of --in (default: 1 week). Duration defaults to slots.defaults.default_duration (45min). Respects free_before and free_after buffers from config. Output begins with a multi-line header, calendar names, a Google Calendar day link, then continuous free ranges (each at least as long as duration). Read-only.",
|
|
260
|
+
"interactive": false,
|
|
261
|
+
"requires_auth": true,
|
|
262
|
+
"command": {
|
|
263
|
+
"argv_template": ["flycal", "slots"],
|
|
264
|
+
"required_options": [],
|
|
265
|
+
"option_map": {
|
|
266
|
+
"duration": ["--duration", "{value}"],
|
|
267
|
+
"from": ["--from", "{value}"],
|
|
268
|
+
"in": ["--in", "{value}"],
|
|
269
|
+
"calendar": ["--calendar", "{value}"],
|
|
270
|
+
"locale": ["--locale", "{value}"]
|
|
271
|
+
},
|
|
272
|
+
"examples": [
|
|
273
|
+
["flycal", "slots"],
|
|
274
|
+
["flycal", "slots", "--duration", "1h"],
|
|
275
|
+
["flycal", "slots", "--from", "2026-08-01", "--in", "2 weeks", "--duration", "1h"]
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
"output": {
|
|
279
|
+
"format": "text/plain",
|
|
280
|
+
"example": "found 3 slots\nfrom wed 29 July 2026 to wed 5 August 2026\nwith duration 45min\nfor calendars\nWork\nhttps://calendar.google.com/calendar/r/day/2026/7/29\n\nfriday 15/7\n10-12\n13-15.30"
|
|
281
|
+
},
|
|
282
|
+
"inputSchema": {
|
|
283
|
+
"type": "object",
|
|
284
|
+
"additionalProperties": false,
|
|
285
|
+
"required": [],
|
|
286
|
+
"properties": {
|
|
287
|
+
"duration": {
|
|
288
|
+
"type": "string",
|
|
289
|
+
"description": "Slot length. Default from config slots.defaults.default_duration (45min)."
|
|
290
|
+
},
|
|
291
|
+
"from": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"description": "Start date/time. Default: now. YYYY-MM-DD always; with locale it also DD-MM-YYYY; with locale en also MM-DD-YYYY. Separators - or /. Optional time."
|
|
294
|
+
},
|
|
295
|
+
"in": {
|
|
296
|
+
"type": "string",
|
|
297
|
+
"description": "Search window from --from. Default: 1 week. Examples: 3 days, 1 week, 48 hours (quote if spaces)."
|
|
298
|
+
},
|
|
299
|
+
"calendar": {
|
|
300
|
+
"type": "string",
|
|
301
|
+
"description": "Fallback calendar name/ID when exclude_calendars is not configured."
|
|
302
|
+
},
|
|
303
|
+
"locale": {
|
|
304
|
+
"type": "string",
|
|
305
|
+
"enum": ["en", "it"]
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
],
|
|
311
|
+
"mcp_implementation_hints": {
|
|
312
|
+
"execution": "Spawn the flycal binary with argv from command.argv or build argv from option_map. Capture stdout/stderr and exit code. Return stdout as MCP text content.",
|
|
313
|
+
"interactive_tools": "Do not auto-run tools with interactive=true unless the host provides a TTY. Instead tell the user to run the command in a terminal.",
|
|
314
|
+
"auth_errors": "If stdout/stderr indicates the user is not connected, suggest flycal login.",
|
|
315
|
+
"locale_arg": "If input.locale is set, append --locale <value> to argv for any tool.",
|
|
316
|
+
"tool_filter": "Expose at least flycal_search, flycal_slots, flycal_calendars, and flycal_version for agent use."
|
|
317
|
+
}
|
|
318
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flycal-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- flycal-cli
|
|
@@ -139,6 +139,7 @@ files:
|
|
|
139
139
|
- lib/flycal_cli/calendar_service.rb
|
|
140
140
|
- lib/flycal_cli/cli.rb
|
|
141
141
|
- lib/flycal_cli/config.rb
|
|
142
|
+
- lib/flycal_cli/date_time_parser.rb
|
|
142
143
|
- lib/flycal_cli/duration_parser.rb
|
|
143
144
|
- lib/flycal_cli/locale.rb
|
|
144
145
|
- lib/flycal_cli/slot_finder.rb
|
|
@@ -146,6 +147,8 @@ files:
|
|
|
146
147
|
- lib/flycal_cli/version.rb
|
|
147
148
|
- locales/en.json
|
|
148
149
|
- locales/it.json
|
|
150
|
+
- mcp/README.md
|
|
151
|
+
- mcp/tools.json
|
|
149
152
|
homepage: https://github.com/magnum/flycal-cli
|
|
150
153
|
licenses:
|
|
151
154
|
- MIT
|