mcpeasy 0.1.0 → 0.2.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/.claudeignore +0 -3
- data/.mcp.json +10 -1
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +19 -5
- data/README.md +19 -3
- data/lib/mcpeasy/cli.rb +33 -10
- data/lib/mcpeasy/config.rb +22 -1
- data/lib/mcpeasy/setup.rb +1 -0
- data/lib/mcpeasy/version.rb +1 -1
- data/lib/utilities/gcal/README.md +11 -3
- data/lib/utilities/gcal/cli.rb +110 -108
- data/lib/utilities/gcal/mcp.rb +463 -308
- data/lib/utilities/gcal/service.rb +312 -0
- data/lib/utilities/gdrive/README.md +3 -3
- data/lib/utilities/gdrive/cli.rb +98 -96
- data/lib/utilities/gdrive/mcp.rb +290 -288
- data/lib/utilities/gdrive/service.rb +293 -0
- data/lib/utilities/gmeet/cli.rb +131 -129
- data/lib/utilities/gmeet/mcp.rb +374 -372
- data/lib/utilities/gmeet/service.rb +409 -0
- data/lib/utilities/notion/README.md +287 -0
- data/lib/utilities/notion/cli.rb +245 -0
- data/lib/utilities/notion/mcp.rb +607 -0
- data/lib/utilities/notion/service.rb +327 -0
- data/lib/utilities/slack/README.md +3 -3
- data/lib/utilities/slack/cli.rb +69 -54
- data/lib/utilities/slack/mcp.rb +277 -226
- data/lib/utilities/slack/service.rb +134 -0
- metadata +11 -8
- data/env.template +0 -11
- data/lib/utilities/gcal/gcal_tool.rb +0 -308
- data/lib/utilities/gdrive/gdrive_tool.rb +0 -291
- data/lib/utilities/gmeet/gmeet_tool.rb +0 -407
- data/lib/utilities/slack/slack_tool.rb +0 -119
- data/logs/.keep +0 -0
@@ -0,0 +1,293 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "google/apis/drive_v3"
|
5
|
+
require "googleauth"
|
6
|
+
require "signet/oauth_2/client"
|
7
|
+
require "fileutils"
|
8
|
+
require "json"
|
9
|
+
require "time"
|
10
|
+
require_relative "../_google/auth_server"
|
11
|
+
require_relative "../../mcpeasy/config"
|
12
|
+
|
13
|
+
module Gdrive
|
14
|
+
class Service
|
15
|
+
SCOPES = [
|
16
|
+
"https://www.googleapis.com/auth/calendar.readonly",
|
17
|
+
"https://www.googleapis.com/auth/drive.readonly"
|
18
|
+
]
|
19
|
+
SCOPE = SCOPES.join(" ")
|
20
|
+
|
21
|
+
# MIME type mappings for Google Workspace documents
|
22
|
+
EXPORT_FORMATS = {
|
23
|
+
"application/vnd.google-apps.document" => {
|
24
|
+
format: "text/markdown",
|
25
|
+
extension: ".md"
|
26
|
+
},
|
27
|
+
"application/vnd.google-apps.spreadsheet" => {
|
28
|
+
format: "text/csv",
|
29
|
+
extension: ".csv"
|
30
|
+
},
|
31
|
+
"application/vnd.google-apps.presentation" => {
|
32
|
+
format: "text/plain",
|
33
|
+
extension: ".txt"
|
34
|
+
},
|
35
|
+
"application/vnd.google-apps.drawing" => {
|
36
|
+
format: "image/png",
|
37
|
+
extension: ".png"
|
38
|
+
}
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
def initialize(skip_auth: false)
|
42
|
+
ensure_env!
|
43
|
+
@service = Google::Apis::DriveV3::DriveService.new
|
44
|
+
@service.authorization = authorize unless skip_auth
|
45
|
+
end
|
46
|
+
|
47
|
+
def search_files(query, max_results: 10)
|
48
|
+
results = @service.list_files(
|
49
|
+
q: "fullText contains '#{query.gsub("'", "\\'")}' and trashed=false",
|
50
|
+
page_size: max_results,
|
51
|
+
fields: "files(id,name,mimeType,size,modifiedTime,webViewLink)"
|
52
|
+
)
|
53
|
+
|
54
|
+
files = results.files.map do |file|
|
55
|
+
{
|
56
|
+
id: file.id,
|
57
|
+
name: file.name,
|
58
|
+
mime_type: file.mime_type,
|
59
|
+
size: file.size&.to_i,
|
60
|
+
modified_time: file.modified_time,
|
61
|
+
web_view_link: file.web_view_link
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
{files: files, count: files.length}
|
66
|
+
rescue Google::Apis::Error => e
|
67
|
+
raise "Google Drive API Error: #{e.message}"
|
68
|
+
rescue => e
|
69
|
+
log_error("search_files", e)
|
70
|
+
raise e
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_file_content(file_id)
|
74
|
+
# First get file metadata
|
75
|
+
file = @service.get_file(file_id, fields: "id,name,mimeType,size")
|
76
|
+
|
77
|
+
content = if EXPORT_FORMATS.key?(file.mime_type)
|
78
|
+
# Export Google Workspace document
|
79
|
+
export_format = EXPORT_FORMATS[file.mime_type][:format]
|
80
|
+
@service.export_file(file_id, export_format)
|
81
|
+
else
|
82
|
+
# Download regular file
|
83
|
+
@service.get_file(file_id, download_dest: StringIO.new)
|
84
|
+
end
|
85
|
+
|
86
|
+
{
|
87
|
+
id: file.id,
|
88
|
+
name: file.name,
|
89
|
+
mime_type: file.mime_type,
|
90
|
+
size: file.size&.to_i,
|
91
|
+
content: content.is_a?(StringIO) ? content.string : content
|
92
|
+
}
|
93
|
+
rescue Google::Apis::Error => e
|
94
|
+
raise "Google Drive API Error: #{e.message}"
|
95
|
+
rescue => e
|
96
|
+
log_error("get_file_content", e)
|
97
|
+
raise e
|
98
|
+
end
|
99
|
+
|
100
|
+
def list_files(max_results: 20)
|
101
|
+
results = @service.list_files(
|
102
|
+
q: "trashed=false",
|
103
|
+
page_size: max_results,
|
104
|
+
order_by: "modifiedTime desc",
|
105
|
+
fields: "files(id,name,mimeType,size,modifiedTime,webViewLink)"
|
106
|
+
)
|
107
|
+
|
108
|
+
files = results.files.map do |file|
|
109
|
+
{
|
110
|
+
id: file.id,
|
111
|
+
name: file.name,
|
112
|
+
mime_type: file.mime_type,
|
113
|
+
size: file.size&.to_i,
|
114
|
+
modified_time: file.modified_time,
|
115
|
+
web_view_link: file.web_view_link
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
{files: files, count: files.length}
|
120
|
+
rescue Google::Apis::Error => e
|
121
|
+
raise "Google Drive API Error: #{e.message}"
|
122
|
+
rescue => e
|
123
|
+
log_error("list_files", e)
|
124
|
+
raise e
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_connection
|
128
|
+
about = @service.get_about(fields: "user,storageQuota")
|
129
|
+
{
|
130
|
+
ok: true,
|
131
|
+
user: about.user.display_name,
|
132
|
+
email: about.user.email_address,
|
133
|
+
storage_used: about.storage_quota&.usage,
|
134
|
+
storage_limit: about.storage_quota&.limit
|
135
|
+
}
|
136
|
+
rescue Google::Apis::Error => e
|
137
|
+
raise "Google Drive API Error: #{e.message}"
|
138
|
+
rescue => e
|
139
|
+
log_error("test_connection", e)
|
140
|
+
raise e
|
141
|
+
end
|
142
|
+
|
143
|
+
def authenticate
|
144
|
+
perform_auth_flow
|
145
|
+
{success: true}
|
146
|
+
rescue => e
|
147
|
+
{success: false, error: e.message}
|
148
|
+
end
|
149
|
+
|
150
|
+
def perform_auth_flow
|
151
|
+
client_id = Mcpeasy::Config.google_client_id
|
152
|
+
client_secret = Mcpeasy::Config.google_client_secret
|
153
|
+
|
154
|
+
unless client_id && client_secret
|
155
|
+
raise "Google credentials not found. Please save your credentials.json file using: mcpz config set_google_credentials <path_to_credentials.json>"
|
156
|
+
end
|
157
|
+
|
158
|
+
# Create credentials using OAuth2 flow with localhost redirect
|
159
|
+
redirect_uri = "http://localhost:8080"
|
160
|
+
client = Signet::OAuth2::Client.new(
|
161
|
+
client_id: client_id,
|
162
|
+
client_secret: client_secret,
|
163
|
+
scope: SCOPE,
|
164
|
+
redirect_uri: redirect_uri,
|
165
|
+
authorization_uri: "https://accounts.google.com/o/oauth2/auth",
|
166
|
+
token_credential_uri: "https://oauth2.googleapis.com/token"
|
167
|
+
)
|
168
|
+
|
169
|
+
# Generate authorization URL
|
170
|
+
url = client.authorization_uri.to_s
|
171
|
+
|
172
|
+
puts "DEBUG: Client ID: #{client_id[0..20]}..."
|
173
|
+
puts "DEBUG: Scope: #{SCOPE}"
|
174
|
+
puts "DEBUG: Redirect URI: #{redirect_uri}"
|
175
|
+
puts
|
176
|
+
|
177
|
+
# Start callback server to capture OAuth code
|
178
|
+
puts "Starting temporary web server to capture OAuth callback..."
|
179
|
+
puts "Opening authorization URL in your default browser..."
|
180
|
+
puts url
|
181
|
+
puts
|
182
|
+
|
183
|
+
# Automatically open URL in default browser on macOS/Unix
|
184
|
+
if system("which open > /dev/null 2>&1")
|
185
|
+
system("open", url)
|
186
|
+
else
|
187
|
+
puts "Could not automatically open browser. Please copy the URL above manually."
|
188
|
+
end
|
189
|
+
puts
|
190
|
+
puts "Waiting for OAuth callback... (will timeout in 60 seconds)"
|
191
|
+
|
192
|
+
# Wait for the authorization code with timeout
|
193
|
+
code = GoogleAuthServer.capture_auth_code
|
194
|
+
|
195
|
+
unless code
|
196
|
+
raise "Failed to receive authorization code. Please try again."
|
197
|
+
end
|
198
|
+
|
199
|
+
puts "✅ Authorization code received!"
|
200
|
+
client.code = code
|
201
|
+
client.fetch_access_token!
|
202
|
+
|
203
|
+
# Save credentials to config
|
204
|
+
credentials_data = {
|
205
|
+
client_id: client.client_id,
|
206
|
+
client_secret: client.client_secret,
|
207
|
+
scope: client.scope,
|
208
|
+
refresh_token: client.refresh_token,
|
209
|
+
access_token: client.access_token,
|
210
|
+
expires_at: client.expires_at
|
211
|
+
}
|
212
|
+
|
213
|
+
Mcpeasy::Config.save_google_token(credentials_data)
|
214
|
+
puts "✅ Authentication successful! Token saved to config"
|
215
|
+
|
216
|
+
client
|
217
|
+
rescue => e
|
218
|
+
log_error("perform_auth_flow", e)
|
219
|
+
raise "Authentication flow failed: #{e.message}"
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
def authorize
|
225
|
+
credentials_data = Mcpeasy::Config.google_token
|
226
|
+
unless credentials_data
|
227
|
+
raise <<~ERROR
|
228
|
+
Google Drive authentication required!
|
229
|
+
Run the auth command first:
|
230
|
+
mcpz gdrive auth
|
231
|
+
ERROR
|
232
|
+
end
|
233
|
+
|
234
|
+
client = Signet::OAuth2::Client.new(
|
235
|
+
client_id: credentials_data.client_id,
|
236
|
+
client_secret: credentials_data.client_secret,
|
237
|
+
scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
|
238
|
+
refresh_token: credentials_data.refresh_token,
|
239
|
+
access_token: credentials_data.access_token,
|
240
|
+
token_credential_uri: "https://oauth2.googleapis.com/token"
|
241
|
+
)
|
242
|
+
|
243
|
+
# Check if token needs refresh
|
244
|
+
if credentials_data.expires_at
|
245
|
+
expires_at = if credentials_data.expires_at.is_a?(String)
|
246
|
+
Time.parse(credentials_data.expires_at)
|
247
|
+
else
|
248
|
+
Time.at(credentials_data.expires_at)
|
249
|
+
end
|
250
|
+
|
251
|
+
if Time.now >= expires_at
|
252
|
+
client.refresh!
|
253
|
+
# Update saved credentials with new access token
|
254
|
+
updated_data = {
|
255
|
+
client_id: credentials_data.client_id,
|
256
|
+
client_secret: credentials_data.client_secret,
|
257
|
+
scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
|
258
|
+
refresh_token: credentials_data.refresh_token,
|
259
|
+
access_token: client.access_token,
|
260
|
+
expires_at: client.expires_at
|
261
|
+
}
|
262
|
+
Mcpeasy::Config.save_google_token(updated_data)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
client
|
267
|
+
rescue JSON::ParserError
|
268
|
+
raise "Invalid token data. Please re-run: mcpz gdrive auth"
|
269
|
+
rescue => e
|
270
|
+
log_error("authorize", e)
|
271
|
+
raise "Authentication failed: #{e.message}"
|
272
|
+
end
|
273
|
+
|
274
|
+
def ensure_env!
|
275
|
+
unless Mcpeasy::Config.google_client_id && Mcpeasy::Config.google_client_secret
|
276
|
+
raise <<~ERROR
|
277
|
+
Google API credentials not configured!
|
278
|
+
Please save your Google credentials.json file using:
|
279
|
+
mcpz config set_google_credentials <path_to_credentials.json>
|
280
|
+
ERROR
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def log_error(method, error)
|
285
|
+
Mcpeasy::Config.ensure_config_dirs
|
286
|
+
File.write(
|
287
|
+
Mcpeasy::Config.log_file_path("gdrive", "error"),
|
288
|
+
"#{Time.now}: #{method} error: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}\n",
|
289
|
+
mode: "a"
|
290
|
+
)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
data/lib/utilities/gmeet/cli.rb
CHANGED
@@ -2,156 +2,158 @@
|
|
2
2
|
|
3
3
|
require "bundler/setup"
|
4
4
|
require "thor"
|
5
|
-
require_relative "
|
5
|
+
require_relative "service"
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
module Gmeet
|
8
|
+
class CLI < Thor
|
9
|
+
desc "test", "Test the Google Calendar API connection"
|
10
|
+
def test
|
11
|
+
response = tool.test_connection
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
if response[:ok]
|
14
|
+
puts "✅ Successfully connected to Google Calendar"
|
15
|
+
puts " User: #{response[:user]} (#{response[:email]})"
|
16
|
+
else
|
17
|
+
warn "❌ Connection test failed"
|
18
|
+
end
|
19
|
+
rescue RuntimeError => e
|
20
|
+
puts "❌ Failed to connect to Google Calendar: #{e.message}"
|
21
|
+
exit 1
|
17
22
|
end
|
18
|
-
rescue RuntimeError => e
|
19
|
-
puts "❌ Failed to connect to Google Calendar: #{e.message}"
|
20
|
-
exit 1
|
21
|
-
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
24
|
+
desc "meetings", "List Google Meet meetings"
|
25
|
+
method_option :start_date, type: :string, aliases: "-s", desc: "Start date (YYYY-MM-DD)"
|
26
|
+
method_option :end_date, type: :string, aliases: "-e", desc: "End date (YYYY-MM-DD)"
|
27
|
+
method_option :max_results, type: :numeric, default: 20, aliases: "-n", desc: "Max number of meetings"
|
28
|
+
method_option :calendar_id, type: :string, aliases: "-c", desc: "Calendar ID (default: primary)"
|
29
|
+
def meetings
|
30
|
+
result = tool.list_meetings(
|
31
|
+
start_date: options[:start_date],
|
32
|
+
end_date: options[:end_date],
|
33
|
+
max_results: options[:max_results],
|
34
|
+
calendar_id: options[:calendar_id] || "primary"
|
35
|
+
)
|
36
|
+
meetings = result[:meetings]
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
38
|
+
if meetings.empty?
|
39
|
+
puts "🎥 No Google Meet meetings found for the specified date range"
|
40
|
+
else
|
41
|
+
puts "🎥 Found #{result[:count]} Google Meet meeting(s):"
|
42
|
+
meetings.each_with_index do |meeting, index|
|
43
|
+
puts " #{index + 1}. #{meeting[:summary] || "No title"}"
|
44
|
+
puts " Start: #{format_datetime(meeting[:start])}"
|
45
|
+
puts " End: #{format_datetime(meeting[:end])}"
|
46
|
+
puts " Description: #{meeting[:description]}" if meeting[:description]
|
47
|
+
puts " Location: #{meeting[:location]}" if meeting[:location]
|
48
|
+
puts " Attendees: #{meeting[:attendees].join(", ")}" if meeting[:attendees]&.any?
|
49
|
+
puts " Meet Link: #{meeting[:meet_link]}"
|
50
|
+
puts " Calendar Link: #{meeting[:html_link]}"
|
51
|
+
puts
|
52
|
+
end
|
51
53
|
end
|
54
|
+
rescue RuntimeError => e
|
55
|
+
warn "❌ Failed to list meetings: #{e.message}"
|
56
|
+
exit 1
|
52
57
|
end
|
53
|
-
rescue RuntimeError => e
|
54
|
-
warn "❌ Failed to list meetings: #{e.message}"
|
55
|
-
exit 1
|
56
|
-
end
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
59
|
+
desc "upcoming", "List upcoming Google Meet meetings"
|
60
|
+
method_option :max_results, type: :numeric, default: 10, aliases: "-n", desc: "Max number of meetings"
|
61
|
+
method_option :calendar_id, type: :string, aliases: "-c", desc: "Calendar ID (default: primary)"
|
62
|
+
def upcoming
|
63
|
+
result = tool.upcoming_meetings(
|
64
|
+
max_results: options[:max_results],
|
65
|
+
calendar_id: options[:calendar_id] || "primary"
|
66
|
+
)
|
67
|
+
meetings = result[:meetings]
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
69
|
+
if meetings.empty?
|
70
|
+
puts "🎥 No upcoming Google Meet meetings found in the next 24 hours"
|
71
|
+
else
|
72
|
+
puts "🎥 Found #{result[:count]} upcoming Google Meet meeting(s):"
|
73
|
+
meetings.each_with_index do |meeting, index|
|
74
|
+
puts " #{index + 1}. #{meeting[:summary] || "No title"}"
|
75
|
+
puts " Start: #{format_datetime(meeting[:start])} (#{meeting[:time_until_start]})"
|
76
|
+
puts " End: #{format_datetime(meeting[:end])}"
|
77
|
+
puts " Description: #{meeting[:description]}" if meeting[:description]
|
78
|
+
puts " Location: #{meeting[:location]}" if meeting[:location]
|
79
|
+
puts " Attendees: #{meeting[:attendees].join(", ")}" if meeting[:attendees]&.any?
|
80
|
+
puts " Meet Link: #{meeting[:meet_link]}"
|
81
|
+
puts " Calendar Link: #{meeting[:html_link]}"
|
82
|
+
puts
|
83
|
+
end
|
82
84
|
end
|
85
|
+
rescue RuntimeError => e
|
86
|
+
warn "❌ Failed to list upcoming meetings: #{e.message}"
|
87
|
+
exit 1
|
83
88
|
end
|
84
|
-
rescue RuntimeError => e
|
85
|
-
warn "❌ Failed to list upcoming meetings: #{e.message}"
|
86
|
-
exit 1
|
87
|
-
end
|
88
89
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
90
|
+
desc "search QUERY", "Search for Google Meet meetings by text content"
|
91
|
+
method_option :start_date, type: :string, aliases: "-s", desc: "Start date (YYYY-MM-DD)"
|
92
|
+
method_option :end_date, type: :string, aliases: "-e", desc: "End date (YYYY-MM-DD)"
|
93
|
+
method_option :max_results, type: :numeric, default: 10, aliases: "-n", desc: "Max number of meetings"
|
94
|
+
def search(query)
|
95
|
+
result = tool.search_meetings(
|
96
|
+
query,
|
97
|
+
start_date: options[:start_date],
|
98
|
+
end_date: options[:end_date],
|
99
|
+
max_results: options[:max_results]
|
100
|
+
)
|
101
|
+
meetings = result[:meetings]
|
101
102
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
103
|
+
if meetings.empty?
|
104
|
+
puts "🔍 No Google Meet meetings found matching '#{query}'"
|
105
|
+
else
|
106
|
+
puts "🔍 Found #{result[:count]} Google Meet meeting(s) matching '#{query}':"
|
107
|
+
meetings.each_with_index do |meeting, index|
|
108
|
+
puts " #{index + 1}. #{meeting[:summary] || "No title"}"
|
109
|
+
puts " Start: #{format_datetime(meeting[:start])}"
|
110
|
+
puts " End: #{format_datetime(meeting[:end])}"
|
111
|
+
puts " Description: #{meeting[:description]}" if meeting[:description]
|
112
|
+
puts " Location: #{meeting[:location]}" if meeting[:location]
|
113
|
+
puts " Meet Link: #{meeting[:meet_link]}"
|
114
|
+
puts " Calendar Link: #{meeting[:html_link]}"
|
115
|
+
puts
|
116
|
+
end
|
115
117
|
end
|
118
|
+
rescue RuntimeError => e
|
119
|
+
warn "❌ Failed to search meetings: #{e.message}"
|
120
|
+
exit 1
|
116
121
|
end
|
117
|
-
rescue RuntimeError => e
|
118
|
-
warn "❌ Failed to search meetings: #{e.message}"
|
119
|
-
exit 1
|
120
|
-
end
|
121
122
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
123
|
+
desc "url EVENT_ID", "Get the Google Meet URL for a specific event"
|
124
|
+
method_option :calendar_id, type: :string, aliases: "-c", desc: "Calendar ID (default: primary)"
|
125
|
+
def url(event_id)
|
126
|
+
result = tool.get_meeting_url(event_id, calendar_id: options[:calendar_id] || "primary")
|
126
127
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
128
|
+
puts "🎥 #{result[:summary] || "Meeting"}"
|
129
|
+
puts " Start: #{format_datetime(result[:start])}"
|
130
|
+
puts " End: #{format_datetime(result[:end])}"
|
131
|
+
puts " Meet Link: #{result[:meet_link]}"
|
132
|
+
puts " Event ID: #{result[:event_id]}"
|
133
|
+
rescue RuntimeError => e
|
134
|
+
warn "❌ Failed to get meeting URL: #{e.message}"
|
135
|
+
exit 1
|
136
|
+
end
|
136
137
|
|
137
|
-
|
138
|
+
private
|
138
139
|
|
139
|
-
|
140
|
-
|
141
|
-
|
140
|
+
def tool
|
141
|
+
@tool ||= Service.new
|
142
|
+
end
|
142
143
|
|
143
|
-
|
144
|
-
|
144
|
+
def format_datetime(datetime_info)
|
145
|
+
return "Unknown" unless datetime_info
|
145
146
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
147
|
+
if datetime_info[:date]
|
148
|
+
# All-day event
|
149
|
+
datetime_info[:date]
|
150
|
+
elsif datetime_info[:date_time]
|
151
|
+
# Specific time event
|
152
|
+
time = Time.parse(datetime_info[:date_time])
|
153
|
+
time.strftime("%Y-%m-%d %H:%M")
|
154
|
+
else
|
155
|
+
"Unknown"
|
156
|
+
end
|
155
157
|
end
|
156
158
|
end
|
157
159
|
end
|