bundleup-sdk 0.4.1 → 0.4.3
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 +104 -3
- data/lib/bundleup/mcp.rb +197 -1
- data/lib/bundleup/unify/calendar.rb +35 -0
- data/lib/bundleup/unify/chat.rb +20 -0
- data/lib/bundleup/unify/mcp.rb +4 -4
- data/lib/bundleup/unify/me.rb +27 -0
- data/lib/bundleup/unify/ticketing.rb +24 -0
- data/lib/bundleup/unify.rb +14 -0
- data/lib/bundleup/version.rb +1 -1
- data/lib/bundleup.rb +2 -0
- data/sig/bundleup/unify/calendar.rbs +23 -0
- data/sig/bundleup/unify/chat.rbs +11 -0
- data/sig/bundleup/unify/me.rbs +14 -0
- data/sig/bundleup/unify/ticketing.rbs +17 -1
- data/sig/bundleup/unify.rbs +2 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a81131b46ad1f04a1714e8e7a07d87d69ee3419648e966daa9099cba31fb4a9
|
|
4
|
+
data.tar.gz: b1861d43ace1fd90f86e89e8b161b91c135f1fc53215cf0dad3defd95351a048
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13c18248fe861fb462cf3d9019764d13a7925031b49aa40d37f5ab7c3b106e019407140873dda6467057777313bc58cc6cb661daac8f102ff462b318aec4c413
|
|
7
|
+
data.tar.gz: c3bc826ddfb41e39616049a651b180dffd36bb3ee939f27ee315bd350400ae25dbec72666025f22860944d20f82e1945dc20f856b7a2c2cc0302cb671b7b0f9f
|
data/README.md
CHANGED
|
@@ -83,7 +83,7 @@ Runnable examples are available in the [`examples/`](./examples) directory:
|
|
|
83
83
|
|
|
84
84
|
- [`examples/basic_usage.rb`](./examples/basic_usage.rb) - Client setup, connections, integrations, and webhooks
|
|
85
85
|
- [`examples/proxy_api.rb`](./examples/proxy_api.rb) - Proxy API GET request with a connection
|
|
86
|
-
- [`examples/unify_api.rb`](./examples/unify_api.rb) - Unify Chat, Git, PM, Ticketing, and
|
|
86
|
+
- [`examples/unify_api.rb`](./examples/unify_api.rb) - Unify Chat, Git, PM, Ticketing, Drive, and Calendar endpoint usage
|
|
87
87
|
- [`examples/README.md`](./examples/README.md) - Setup and execution instructions
|
|
88
88
|
|
|
89
89
|
## Quick Start
|
|
@@ -146,7 +146,7 @@ client = BundleUp::Client.new(ENV['BUNDLEUP_API_KEY'])
|
|
|
146
146
|
**Example `.env` file:**
|
|
147
147
|
|
|
148
148
|
```bash
|
|
149
|
-
BUNDLEUP_API_KEY=
|
|
149
|
+
BUNDLEUP_API_KEY=Zw7Lt7JacsDyMCEpnZdGptgnJaOdMzFVH9QtIthnL5RviYP5WeH6e9FWP2HzEO
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
**Loading environment variables (using dotenv):**
|
|
@@ -737,6 +737,35 @@ end
|
|
|
737
737
|
puts "Fetched #{all_channels.length} total channels"
|
|
738
738
|
```
|
|
739
739
|
|
|
740
|
+
##### List Messages
|
|
741
|
+
|
|
742
|
+
Messages in one channel, newest first. `author.name` is nil on Slack, which returns only a user id on a message.
|
|
743
|
+
|
|
744
|
+
```ruby
|
|
745
|
+
result = unify.chat.messages('C123', limit: 100, after: nil, include_raw: false)
|
|
746
|
+
|
|
747
|
+
puts "Messages: #{result['data']}"
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
**Response:**
|
|
751
|
+
|
|
752
|
+
```ruby
|
|
753
|
+
{
|
|
754
|
+
'data' => [
|
|
755
|
+
{
|
|
756
|
+
'id' => '1755712345.123456',
|
|
757
|
+
'text' => 'Deploy finished',
|
|
758
|
+
'author' => { 'id' => 'U024BE7LH', 'name' => nil },
|
|
759
|
+
'created_at' => '2026-08-20T18:32:25.123Z',
|
|
760
|
+
'thread_id' => nil
|
|
761
|
+
}
|
|
762
|
+
],
|
|
763
|
+
'metadata' => {
|
|
764
|
+
'next' => 'cursor_def456'
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
740
769
|
##### Send Message
|
|
741
770
|
|
|
742
771
|
Send a message to a channel on the connected chat platform.
|
|
@@ -971,6 +1000,34 @@ open_tickets = result['data'].select { |ticket| ticket['status'] == 'open' }
|
|
|
971
1000
|
sorted_by_date = result['data'].sort_by { |ticket| Time.parse(ticket['created_at']) }.reverse
|
|
972
1001
|
```
|
|
973
1002
|
|
|
1003
|
+
##### Get a Ticket
|
|
1004
|
+
|
|
1005
|
+
Fetch one ticket by ID. Not supported by Basecamp, whose API only serves a to-do underneath its project.
|
|
1006
|
+
|
|
1007
|
+
```ruby
|
|
1008
|
+
result = unify.ticketing.ticket('PROJ-123')
|
|
1009
|
+
|
|
1010
|
+
puts "Ticket: #{result['data']}"
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
**Response:**
|
|
1014
|
+
|
|
1015
|
+
```ruby
|
|
1016
|
+
{
|
|
1017
|
+
'data' => {
|
|
1018
|
+
'id' => 'PROJ-123',
|
|
1019
|
+
'url' => 'https://jira.example.com/browse/PROJ-123',
|
|
1020
|
+
'title' => 'Fix login bug',
|
|
1021
|
+
'status' => 'in_progress',
|
|
1022
|
+
'description' => 'Users are unable to log in',
|
|
1023
|
+
'created_at' => '2024-01-15T10:30:00Z',
|
|
1024
|
+
'updated_at' => '2024-01-20T14:22:00Z'
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
A single resource carries no pagination, so there is no `metadata` on this response.
|
|
1030
|
+
|
|
974
1031
|
#### CRM API
|
|
975
1032
|
|
|
976
1033
|
The CRM API provides a unified interface for CRM platforms like Attio, HubSpot, PipeDrive, Salesforce and Zoho.
|
|
@@ -1071,6 +1128,49 @@ puts "Files: #{result['data']}"
|
|
|
1071
1128
|
}
|
|
1072
1129
|
```
|
|
1073
1130
|
|
|
1131
|
+
#### Calendar API
|
|
1132
|
+
|
|
1133
|
+
The Calendar API provides a unified interface for calendar and scheduling platforms like Google Calendar, Outlook, Calendly and Zoom.
|
|
1134
|
+
|
|
1135
|
+
##### List Events
|
|
1136
|
+
|
|
1137
|
+
`starts_after` and `starts_before` are required — the endpoint refuses an unbounded listing.
|
|
1138
|
+
|
|
1139
|
+
```ruby
|
|
1140
|
+
result = unify.calendar.events(
|
|
1141
|
+
starts_after: '2026-09-01T00:00:00Z',
|
|
1142
|
+
starts_before: '2026-09-08T00:00:00Z',
|
|
1143
|
+
limit: 100,
|
|
1144
|
+
after: nil,
|
|
1145
|
+
include_raw: false
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
puts "Events: #{result['data']}"
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
**Response:**
|
|
1152
|
+
|
|
1153
|
+
```ruby
|
|
1154
|
+
{
|
|
1155
|
+
'data' => [
|
|
1156
|
+
{
|
|
1157
|
+
'id' => 'evt_123',
|
|
1158
|
+
'title' => 'Design review',
|
|
1159
|
+
'description' => 'Walk through the new onboarding flow',
|
|
1160
|
+
'start_date' => '2026-09-01T15:00:00Z',
|
|
1161
|
+
'end_date' => '2026-09-01T16:00:00Z',
|
|
1162
|
+
'status' => 'confirmed',
|
|
1163
|
+
'url' => 'https://calendar.google.com/event?eid=...'
|
|
1164
|
+
}
|
|
1165
|
+
],
|
|
1166
|
+
'metadata' => {
|
|
1167
|
+
'next' => nil
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
Recurring events are expanded into their occurrences. All-day events carry a `YYYY-MM-DD` date rather than a timestamp. Attendees, conferencing links and organizers are available through `include_raw` or the Proxy API.
|
|
1173
|
+
|
|
1074
1174
|
## Error Handling
|
|
1075
1175
|
|
|
1076
1176
|
The SDK raises exceptions for errors. Always wrap SDK calls in rescue blocks for proper error handling.
|
|
@@ -1126,7 +1226,8 @@ lib/
|
|
|
1126
1226
|
│ ├── git.rb # Git Unify API
|
|
1127
1227
|
│ ├── ticketing.rb # Ticketing Unify API
|
|
1128
1228
|
│ ├── crm.rb # CRM Unify API
|
|
1129
|
-
│
|
|
1229
|
+
│ ├── drive.rb # Drive Unify API
|
|
1230
|
+
│ └── calendar.rb # Calendar Unify API
|
|
1130
1231
|
sig/ # RBS type signatures (mirrors lib/)
|
|
1131
1232
|
spec/ # Test files
|
|
1132
1233
|
```
|
data/lib/bundleup/mcp.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require_relative 'mcp_client'
|
|
5
4
|
|
|
6
5
|
module BundleUp
|
|
7
6
|
# Transport for a connection's MCP server.
|
|
@@ -68,4 +67,201 @@ module BundleUp
|
|
|
68
67
|
@connection ||= Faraday.new { |faraday| faraday.adapter Faraday.default_adapter }
|
|
69
68
|
end
|
|
70
69
|
end
|
|
70
|
+
|
|
71
|
+
# A connected MCP session.
|
|
72
|
+
#
|
|
73
|
+
# Tools, resources and prompts are defined by the provider — BundleUp does
|
|
74
|
+
# not rename or normalize them.
|
|
75
|
+
class MCPClient
|
|
76
|
+
PROTOCOL_VERSION = '2025-06-18'
|
|
77
|
+
CLIENT_NAME = 'bundleup-sdk'
|
|
78
|
+
|
|
79
|
+
def initialize(base_url, api_key, connection_id)
|
|
80
|
+
@base_url = base_url
|
|
81
|
+
@api_key = api_key
|
|
82
|
+
@connection_id = connection_id
|
|
83
|
+
@session_id = nil
|
|
84
|
+
@connected = false
|
|
85
|
+
@last_id = 0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# List the provider's tools, following pagination to the end.
|
|
89
|
+
def list_tools
|
|
90
|
+
paginate('tools/list', 'tools')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Call a tool by name, with arguments matching its own input schema.
|
|
94
|
+
def call_tool(name, args = {})
|
|
95
|
+
raise ArgumentError, 'Tool name is required to call a tool.' if blank?(name)
|
|
96
|
+
|
|
97
|
+
connect
|
|
98
|
+
send_message('tools/call', { name: name, arguments: args })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# List the provider's resources, following pagination to the end.
|
|
102
|
+
def list_resources
|
|
103
|
+
paginate('resources/list', 'resources')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Read a resource by URI.
|
|
107
|
+
def read_resource(uri)
|
|
108
|
+
raise ArgumentError, 'Resource URI is required to read a resource.' if blank?(uri)
|
|
109
|
+
|
|
110
|
+
connect
|
|
111
|
+
send_message('resources/read', { uri: uri })
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# List the provider's prompts, following pagination to the end.
|
|
115
|
+
def list_prompts
|
|
116
|
+
paginate('prompts/list', 'prompts')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Get a prompt by name.
|
|
120
|
+
def get_prompt(name, args = {})
|
|
121
|
+
raise ArgumentError, 'Prompt name is required to get a prompt.' if blank?(name)
|
|
122
|
+
|
|
123
|
+
connect
|
|
124
|
+
send_message('prompts/get', { name: name, arguments: args })
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Send any other JSON-RPC method on this session.
|
|
128
|
+
def request(method, params = nil)
|
|
129
|
+
raise ArgumentError, 'Method is required to send a request.' if blank?(method)
|
|
130
|
+
|
|
131
|
+
connect
|
|
132
|
+
send_message(method, params)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# End the session and reset local state.
|
|
136
|
+
def close
|
|
137
|
+
delete_session if @session_id
|
|
138
|
+
|
|
139
|
+
@session_id = nil
|
|
140
|
+
@connected = false
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def blank?(value)
|
|
147
|
+
value.nil? || value.to_s.empty?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def default_headers
|
|
151
|
+
headers = {
|
|
152
|
+
'Authorization' => "Bearer #{@api_key}",
|
|
153
|
+
'Content-Type' => 'application/json',
|
|
154
|
+
'Accept' => 'application/json, text/event-stream',
|
|
155
|
+
'BU-Connection-Id' => @connection_id
|
|
156
|
+
}
|
|
157
|
+
headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
158
|
+
headers
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def connection
|
|
162
|
+
@connection ||= Faraday.new { |faraday| faraday.adapter Faraday.default_adapter }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def post_payload(payload)
|
|
166
|
+
response = connection.post(@base_url, payload.to_json, default_headers)
|
|
167
|
+
session_id = response.headers['mcp-session-id']
|
|
168
|
+
@session_id = session_id if session_id
|
|
169
|
+
|
|
170
|
+
raise error_for(response) unless response.success?
|
|
171
|
+
|
|
172
|
+
response
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def error_for(response)
|
|
176
|
+
fallback = "MCP request failed with status #{response.status}."
|
|
177
|
+
parsed = JSON.parse(response.body.to_s)
|
|
178
|
+
return RuntimeError.new(fallback) unless parsed.is_a?(Hash) && parsed['message']
|
|
179
|
+
|
|
180
|
+
code = parsed['code']
|
|
181
|
+
RuntimeError.new(code ? "#{parsed['message']} (#{code})" : parsed['message'])
|
|
182
|
+
rescue JSON::ParserError
|
|
183
|
+
RuntimeError.new(fallback)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Run the MCP handshake, once. Deferred until the first call.
|
|
187
|
+
def connect
|
|
188
|
+
return if @connected
|
|
189
|
+
|
|
190
|
+
send_message('initialize', handshake_params)
|
|
191
|
+
post_payload({ jsonrpc: '2.0', method: 'notifications/initialized' })
|
|
192
|
+
@connected = true
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def handshake_params
|
|
196
|
+
{
|
|
197
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
198
|
+
capabilities: {},
|
|
199
|
+
clientInfo: { name: CLIENT_NAME, version: BundleUp::VERSION }
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def send_message(method, params = nil)
|
|
204
|
+
@last_id += 1
|
|
205
|
+
payload = { jsonrpc: '2.0', id: @last_id, method: method }
|
|
206
|
+
payload[:params] = params unless params.nil?
|
|
207
|
+
|
|
208
|
+
message = parse(post_payload(payload), @last_id)
|
|
209
|
+
raise "No response received for #{method}." if message.nil?
|
|
210
|
+
raise message['error']['message'].to_s if message['error']
|
|
211
|
+
|
|
212
|
+
message['result'] || {}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Providers may answer a plain request/response over text/event-stream.
|
|
216
|
+
def parse(response, message_id)
|
|
217
|
+
body = response.body.to_s
|
|
218
|
+
return nil if body.empty?
|
|
219
|
+
|
|
220
|
+
content_type = response.headers['content-type'].to_s
|
|
221
|
+
return JSON.parse(body) unless content_type.include?('text/event-stream')
|
|
222
|
+
|
|
223
|
+
parse_stream(body, message_id)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def parse_stream(body, message_id)
|
|
227
|
+
body.gsub("\r\n", "\n").split("\n\n").each do |event|
|
|
228
|
+
data = event_data(event)
|
|
229
|
+
next if data.empty?
|
|
230
|
+
|
|
231
|
+
message = JSON.parse(data)
|
|
232
|
+
# Skip server notifications interleaved on the stream.
|
|
233
|
+
return message if message.is_a?(Hash) && message['id'] == message_id
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def event_data(event)
|
|
240
|
+
event.split("\n")
|
|
241
|
+
.select { |line| line.start_with?('data:') }
|
|
242
|
+
.map { |line| line.sub('data:', '').strip }
|
|
243
|
+
.join("\n")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def paginate(method, key)
|
|
247
|
+
connect
|
|
248
|
+
items = []
|
|
249
|
+
cursor = nil
|
|
250
|
+
|
|
251
|
+
loop do
|
|
252
|
+
result = send_message(method, cursor ? { cursor: cursor } : nil)
|
|
253
|
+
items.concat(result[key] || [])
|
|
254
|
+
cursor = result['nextCursor']
|
|
255
|
+
break unless cursor
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
items
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def delete_session
|
|
262
|
+
connection.delete(@base_url, nil, default_headers)
|
|
263
|
+
rescue Faraday::Error
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
end
|
|
71
267
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BundleUp
|
|
4
|
+
module Unify
|
|
5
|
+
# Client for Calendar-related Unify endpoints.
|
|
6
|
+
class Calendar < Base
|
|
7
|
+
# Fetches events from the connected calendar provider.
|
|
8
|
+
#
|
|
9
|
+
# +params+ must carry +starts_after+ and +starts_before+ (ISO 8601) —
|
|
10
|
+
# the endpoint refuses an unbounded listing.
|
|
11
|
+
def events(params = {})
|
|
12
|
+
raise ArgumentError, 'starts_after and starts_before are required to fetch events.' if window_missing?(params)
|
|
13
|
+
|
|
14
|
+
response = connection.get('calendar/events') do |req|
|
|
15
|
+
req.params = params
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
raise "Failed to fetch calendar/events: #{response.status}" unless response.success?
|
|
19
|
+
|
|
20
|
+
response.body
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def window_missing?(params)
|
|
26
|
+
params ||= {}
|
|
27
|
+
|
|
28
|
+
%i[starts_after starts_before].any? do |key|
|
|
29
|
+
value = params[key] || params[key.to_s]
|
|
30
|
+
value.nil? || value.to_s.empty?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/bundleup/unify/chat.rb
CHANGED
|
@@ -26,6 +26,26 @@ module BundleUp
|
|
|
26
26
|
response.body
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Fetches messages in a channel from the connected chat provider.
|
|
30
|
+
#
|
|
31
|
+
# Newest first. +author.name+ is nil on Slack, which returns only a user
|
|
32
|
+
# id on a message.
|
|
33
|
+
def messages(channel_id, params = {})
|
|
34
|
+
raise ArgumentError, 'channel_id is required to fetch messages.' if channel_id.nil?
|
|
35
|
+
|
|
36
|
+
encoded_channel_id = URI.encode_www_form_component(channel_id)
|
|
37
|
+
|
|
38
|
+
response = connection.get("chat/channels/#{encoded_channel_id}/messages") do |req|
|
|
39
|
+
req.params = params
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
unless response.success?
|
|
43
|
+
raise "Failed to fetch chat/channels/#{encoded_channel_id}/messages: #{response.status}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
response.body
|
|
47
|
+
end
|
|
48
|
+
|
|
29
49
|
# Sends a message to a channel on the connected chat provider.
|
|
30
50
|
def message(channel_id, text)
|
|
31
51
|
raise ArgumentError, 'channel_id is required to send a message.' if channel_id.nil?
|
data/lib/bundleup/unify/mcp.rb
CHANGED
|
@@ -29,15 +29,15 @@ module BundleUp
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# List the available unified tools.
|
|
32
|
-
def
|
|
33
|
-
@client.
|
|
32
|
+
def list_tools
|
|
33
|
+
@client.list_tools
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# Call a unified tool with optional arguments.
|
|
37
|
-
def
|
|
37
|
+
def call_tool(name, args = {})
|
|
38
38
|
raise ArgumentError, 'Tool name is required to call a tool.' if name.nil? || name.to_s.empty?
|
|
39
39
|
|
|
40
|
-
@client.
|
|
40
|
+
@client.call_tool(name, args)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BundleUp
|
|
4
|
+
module Unify
|
|
5
|
+
# Client for the root-level identity endpoint.
|
|
6
|
+
#
|
|
7
|
+
# `me` is the one unified method every provider implements, so the API mounts
|
|
8
|
+
# it at the root rather than under a vertical. It is exposed on the Unify
|
|
9
|
+
# client as `unify.me` rather than as a namespace.
|
|
10
|
+
class Me < Base
|
|
11
|
+
# Fetches the account the connection is authenticated as.
|
|
12
|
+
#
|
|
13
|
+
# Providers that authorize per workspace, portal, tenant or company return
|
|
14
|
+
# that account instead of a user, and fields the provider does not expose
|
|
15
|
+
# come back as nil.
|
|
16
|
+
def get(params = {})
|
|
17
|
+
response = connection.get('me') do |req|
|
|
18
|
+
req.params = params
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
raise "Failed to fetch me: #{response.status}" unless response.success?
|
|
22
|
+
|
|
23
|
+
response.body
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -14,6 +14,30 @@ module BundleUp
|
|
|
14
14
|
|
|
15
15
|
response.body
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
# Fetches a single ticket by ID from the connected ticketing tool.
|
|
19
|
+
#
|
|
20
|
+
# Not supported by Basecamp, whose API only serves a to-do underneath its
|
|
21
|
+
# project — an id on its own cannot address one.
|
|
22
|
+
def ticket(ticket_id, params = {})
|
|
23
|
+
raise ArgumentError, 'ticket_id is required to fetch a ticket.' if blank?(ticket_id)
|
|
24
|
+
|
|
25
|
+
encoded_id = URI.encode_www_form_component(ticket_id)
|
|
26
|
+
|
|
27
|
+
response = connection.get("ticketing/tickets/#{encoded_id}") do |req|
|
|
28
|
+
req.params = params
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
raise "Failed to fetch ticketing/tickets/#{encoded_id}: #{response.status}" unless response.success?
|
|
32
|
+
|
|
33
|
+
response.body
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def blank?(value)
|
|
39
|
+
value.nil? || value.to_s.empty?
|
|
40
|
+
end
|
|
17
41
|
end
|
|
18
42
|
end
|
|
19
43
|
end
|
data/lib/bundleup/unify.rb
CHANGED
|
@@ -12,6 +12,7 @@ module BundleUp
|
|
|
12
12
|
@git = BundleUp::Unify::Git.new(api_key, connection_id)
|
|
13
13
|
@crm = BundleUp::Unify::CRM.new(api_key, connection_id)
|
|
14
14
|
@drive = BundleUp::Unify::Drive.new(api_key, connection_id)
|
|
15
|
+
@calendar = BundleUp::Unify::Calendar.new(api_key, connection_id)
|
|
15
16
|
@api_key = api_key
|
|
16
17
|
@connection_id = connection_id
|
|
17
18
|
end
|
|
@@ -41,6 +42,19 @@ module BundleUp
|
|
|
41
42
|
@drive ||= BundleUp::Unify::Drive.new(api_key, connection_id)
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
# Access the Calendar API for the connection.
|
|
46
|
+
def calendar
|
|
47
|
+
@calendar ||= BundleUp::Unify::Calendar.new(api_key, connection_id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Fetch the account this connection is authenticated as.
|
|
51
|
+
#
|
|
52
|
+
# `me` is the one unified method every provider implements, so it hangs off
|
|
53
|
+
# the Unify client directly instead of a vertical namespace.
|
|
54
|
+
def me(params = {})
|
|
55
|
+
(@me ||= BundleUp::Unify::Me.new(api_key, connection_id)).get(params)
|
|
56
|
+
end
|
|
57
|
+
|
|
44
58
|
# Access the Unified MCP server for the connection.
|
|
45
59
|
def mcp
|
|
46
60
|
@mcp ||= BundleUp::Unify::MCP.new(api_key, connection_id)
|
data/lib/bundleup/version.rb
CHANGED
data/lib/bundleup.rb
CHANGED
|
@@ -21,7 +21,9 @@ require_relative 'bundleup/unify/git'
|
|
|
21
21
|
require_relative 'bundleup/unify/ticketing'
|
|
22
22
|
require_relative 'bundleup/unify/crm'
|
|
23
23
|
require_relative 'bundleup/unify/drive'
|
|
24
|
+
require_relative 'bundleup/unify/calendar'
|
|
24
25
|
require_relative 'bundleup/unify/mcp'
|
|
26
|
+
require_relative 'bundleup/unify/me'
|
|
25
27
|
|
|
26
28
|
# Main module for the BundleUp SDK.
|
|
27
29
|
module BundleUp
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module BundleUp
|
|
2
|
+
module Unify
|
|
3
|
+
class Calendar < Base
|
|
4
|
+
type calendar_event = {
|
|
5
|
+
id: String,
|
|
6
|
+
title: String?,
|
|
7
|
+
description: String?,
|
|
8
|
+
start_date: String?,
|
|
9
|
+
end_date: String?,
|
|
10
|
+
status: String?,
|
|
11
|
+
url: String?
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type metadata = { next: String? }
|
|
15
|
+
|
|
16
|
+
def events: (?Hash[Symbol, untyped] params) -> { data: Array[calendar_event], metadata: metadata }
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def window_missing?: (Hash[Symbol, untyped]?) -> bool
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/sig/bundleup/unify/chat.rbs
CHANGED
|
@@ -3,10 +3,21 @@ module BundleUp
|
|
|
3
3
|
class Chat < Base
|
|
4
4
|
type chat_user = { id: String, name: String }
|
|
5
5
|
type chat_channel = { id: String, name: String }
|
|
6
|
+
type chat_author = { id: String?, name: String? }
|
|
7
|
+
|
|
8
|
+
type chat_message = {
|
|
9
|
+
id: String,
|
|
10
|
+
text: String?,
|
|
11
|
+
author: chat_author?,
|
|
12
|
+
created_at: String?,
|
|
13
|
+
thread_id: String?
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
type metadata = { next: String? }
|
|
7
17
|
|
|
8
18
|
def users: (?Hash[Symbol, untyped] params) -> { data: Array[chat_user], metadata: metadata }
|
|
9
19
|
def channels: (?Hash[Symbol, untyped] params) -> { data: Array[chat_channel], metadata: metadata }
|
|
20
|
+
def messages: (String channel_id, ?Hash[Symbol, untyped] params) -> { data: Array[chat_message], metadata: metadata }
|
|
10
21
|
def message: (String channel_id, String text) -> { data: Hash[String, untyped] }
|
|
11
22
|
end
|
|
12
23
|
end
|
|
@@ -11,9 +11,25 @@ module BundleUp
|
|
|
11
11
|
updated_at: String
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
type ticket = {
|
|
15
|
+
id: String,
|
|
16
|
+
title: String,
|
|
17
|
+
status: String?,
|
|
18
|
+
url: String?,
|
|
19
|
+
description: String?,
|
|
20
|
+
created_at: String?,
|
|
21
|
+
updated_at: String?
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
type metadata = { next: String? }
|
|
15
25
|
|
|
16
|
-
def
|
|
26
|
+
def tickets: (?Hash[Symbol, untyped] params) -> { data: Array[ticketing_ticket], metadata: metadata }
|
|
27
|
+
|
|
28
|
+
def ticket: (String ticket_id, ?Hash[Symbol, untyped] params) -> { data: ticket }
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def blank?: (untyped) -> bool
|
|
17
33
|
end
|
|
18
34
|
end
|
|
19
35
|
end
|
data/sig/bundleup/unify.rbs
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundleup-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BundleUp
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -177,19 +177,23 @@ files:
|
|
|
177
177
|
- lib/bundleup/resources/webhook.rb
|
|
178
178
|
- lib/bundleup/unify.rb
|
|
179
179
|
- lib/bundleup/unify/base.rb
|
|
180
|
+
- lib/bundleup/unify/calendar.rb
|
|
180
181
|
- lib/bundleup/unify/chat.rb
|
|
181
182
|
- lib/bundleup/unify/crm.rb
|
|
182
183
|
- lib/bundleup/unify/drive.rb
|
|
183
184
|
- lib/bundleup/unify/git.rb
|
|
184
185
|
- lib/bundleup/unify/mcp.rb
|
|
186
|
+
- lib/bundleup/unify/me.rb
|
|
185
187
|
- lib/bundleup/unify/ticketing.rb
|
|
186
188
|
- lib/bundleup/version.rb
|
|
187
189
|
- sig/bundleup/unify.rbs
|
|
188
190
|
- sig/bundleup/unify/base.rbs
|
|
191
|
+
- sig/bundleup/unify/calendar.rbs
|
|
189
192
|
- sig/bundleup/unify/chat.rbs
|
|
190
193
|
- sig/bundleup/unify/crm.rbs
|
|
191
194
|
- sig/bundleup/unify/drive.rbs
|
|
192
195
|
- sig/bundleup/unify/git.rbs
|
|
196
|
+
- sig/bundleup/unify/me.rbs
|
|
193
197
|
- sig/bundleup/unify/ticketing.rbs
|
|
194
198
|
homepage: https://github.com/bundleup/bundleup-sdk-ruby
|
|
195
199
|
licenses:
|