bundleup-sdk 0.4.2 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e98412b5b8723a8137da05f6996554d0247bfa7947e2d9ec3955a1656cb1d386
4
- data.tar.gz: 8639a58d6d076123db499bfc30bababe63374821189e586aeaaaf415c23192ff
3
+ metadata.gz: ab7e53fa4d67aa688569b213a6b9f090cce9b8e557203e598ef4ce946e4ac508
4
+ data.tar.gz: 5226846b47d962f8c616eb14e6c47ee8836bde8a63f610cfc155fda1eec879df
5
5
  SHA512:
6
- metadata.gz: b46029fe10adf813158115754593ed3d0e64ac44ce1ddc31c648a40a8580e35e93944a8701599ac777864ab0f605a0e014197b06af9830190cff01c76bb31602
7
- data.tar.gz: 49d99e240a23e1e3ca9e961b1b7a06b9383bfb71e5a27cf552755db0d20ef1f2f42939c6ffbe58273ea630ad86c25d2f9e0c3dacb9d7bad7bb7054f4918f7b58
6
+ metadata.gz: 0a8085e498cfd804bbd5d7b93840997f0d719a4d040d4d0639f2d51621976217b1cfb1038db537e0ca46d917c0440a8ed6b7ab83b3e362c34d45c064dff7a1df
7
+ data.tar.gz: 823c5ebb016f7c2f31cdae1a838556f2ea7d62a25d0d6193aead188dfe045392cbf940892f258d24a2167fbc5f620816d03274154d07535031c66535c232909d
data/README.md CHANGED
@@ -72,6 +72,7 @@ The BundleUp SDK is tested and supported on:
72
72
  - 🔌 **100+ Integrations** - Connect to Slack, GitHub, Jira, Linear, and many more
73
73
  - ðŸŽŊ **Unified API** - Consistent interface across all integrations via Unify API
74
74
  - 🔑 **Proxy API** - Direct access to underlying integration APIs
75
+ - ðŸĪ– **MCP** - Connect agents to a provider's own MCP server or to BundleUp's Unified MCP
75
76
  - ðŸŠķ **Lightweight** - Minimal dependencies
76
77
  - ðŸ›Ąïļ **Error Handling** - Comprehensive error messages and validation
77
78
  - 📚 **Well Documented** - Extensive documentation and examples
@@ -83,7 +84,7 @@ Runnable examples are available in the [`examples/`](./examples) directory:
83
84
 
84
85
  - [`examples/basic_usage.rb`](./examples/basic_usage.rb) - Client setup, connections, integrations, and webhooks
85
86
  - [`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 Drive endpoint usage
87
+ - [`examples/unify_api.rb`](./examples/unify_api.rb) - Unify Chat, Git, PM, Ticketing, Drive, and Calendar endpoint usage
87
88
  - [`examples/README.md`](./examples/README.md) - Setup and execution instructions
88
89
 
89
90
  ## Quick Start
@@ -175,6 +176,10 @@ BUNDLEUP_CLIENT = BundleUp::Client.new(ENV['BUNDLEUP_API_KEY'])
175
176
 
176
177
  ## Core Concepts
177
178
 
179
+ ### Auth API
180
+
181
+ The **Auth API** runs the hosted authorization flow: build the URL that sends a user to connect an integration, then exchange the one-time `code` from the redirect for a `connection_id`. See [Authorization Flow](https://docs.bundleup.io/authorization-flow).
182
+
178
183
  ### Platform API
179
184
 
180
185
  The **Platform API** provides access to core BundleUp features like managing connections and integrations. Use this API to list, retrieve, and delete connections, as well as discover available integrations.
@@ -187,6 +192,10 @@ The **Proxy API** allows you to make direct HTTP requests to the underlying inte
187
192
 
188
193
  The **Unify API** provides a standardized, normalized interface across different integrations. For example, you can fetch chat channels from Slack, Discord, or Microsoft Teams using the same API call.
189
194
 
195
+ ### MCP API
196
+
197
+ The **MCP API** reaches a provider's own MCP server using a connection's stored credentials. Tools are defined by the provider, not by BundleUp. Because the connection is chosen per client, one agent can serve many end users without ever handling a token.
198
+
190
199
  ## API Reference
191
200
 
192
201
  ### Connections
@@ -515,6 +524,62 @@ class WebhooksController < ApplicationController
515
524
  end
516
525
  ```
517
526
 
527
+ ### Auth API
528
+
529
+ Connect an end user's account and get back a `connection_id`. See [Authorization Flow](https://docs.bundleup.io/authorization-flow) for the full flow.
530
+
531
+ #### Build the Authorization URL
532
+
533
+ ```ruby
534
+ url = client.auth.build_authorization_url(
535
+ client_id: 'your-client-id',
536
+ integration_id: 'github',
537
+ redirect_uri: 'https://app.example.com/callback',
538
+ external_id: 'user_42', # optional
539
+ state: 'random-csrf-token' # optional
540
+ )
541
+
542
+ # Redirect the user to `url`
543
+ ```
544
+
545
+ **Parameters:**
546
+
547
+ - `client_id` (String, required): Your workspace client ID
548
+ - `integration_id` (String, required): The integration to connect
549
+ - `redirect_uri` (String, required): Must exactly match a redirect URI registered in your dashboard
550
+ - `external_id` (String, optional): Your own reference, stored on the connection
551
+ - `state` (String, optional): Returned unchanged on the redirect
552
+
553
+ #### Exchange the Code for a Connection
554
+
555
+ BundleUp redirects back to `redirect_uri` with a one-time `code`. Exchange it server-side:
556
+
557
+ ```ruby
558
+ connection = client.auth.get_connection_from_code(
559
+ code: params[:code],
560
+ redirect_uri: 'https://app.example.com/callback'
561
+ )
562
+
563
+ puts connection['connection_id']
564
+ ```
565
+
566
+ **Parameters:**
567
+
568
+ - `code` (String, required): The `code` query parameter from the redirect
569
+ - `redirect_uri` (String, required): The same redirect URI used to build the authorization URL
570
+
571
+ **Response:**
572
+
573
+ ```ruby
574
+ {
575
+ 'connection_id' => 'conn_abc123',
576
+ 'external_id' => 'user_42',
577
+ 'integration_id' => 'github'
578
+ }
579
+ ```
580
+
581
+ The code expires after 5 minutes and can only be exchanged once. An invalid, expired or reused code raises a `RuntimeError` that includes the API's error body. Missing arguments raise `ArgumentError`.
582
+
518
583
  ### Proxy API
519
584
 
520
585
  Make direct HTTP requests to integration APIs through BundleUp.
@@ -737,6 +802,35 @@ end
737
802
  puts "Fetched #{all_channels.length} total channels"
738
803
  ```
739
804
 
805
+ ##### List Messages
806
+
807
+ Messages in one channel, newest first. `author.name` is nil on Slack, which returns only a user id on a message.
808
+
809
+ ```ruby
810
+ result = unify.chat.messages('C123', limit: 100, after: nil, include_raw: false)
811
+
812
+ puts "Messages: #{result['data']}"
813
+ ```
814
+
815
+ **Response:**
816
+
817
+ ```ruby
818
+ {
819
+ 'data' => [
820
+ {
821
+ 'id' => '1755712345.123456',
822
+ 'text' => 'Deploy finished',
823
+ 'author' => { 'id' => 'U024BE7LH', 'name' => nil },
824
+ 'created_at' => '2026-08-20T18:32:25.123Z',
825
+ 'thread_id' => nil
826
+ }
827
+ ],
828
+ 'metadata' => {
829
+ 'next' => 'cursor_def456'
830
+ }
831
+ }
832
+ ```
833
+
740
834
  ##### Send Message
741
835
 
742
836
  Send a message to a channel on the connected chat platform.
@@ -844,6 +938,38 @@ puts "Pull Requests: #{result['data']}"
844
938
  }
845
939
  ```
846
940
 
941
+ ##### List Issues
942
+
943
+ ```ruby
944
+ result = unify.git.issues('organization/repo-name', limit: 20)
945
+
946
+ puts "Issues: #{result['data']}"
947
+ ```
948
+
949
+ **Response:**
950
+
951
+ ```ruby
952
+ {
953
+ 'data' => [
954
+ {
955
+ 'id' => 67890,
956
+ 'number' => 17,
957
+ 'title' => 'Timestamps drift on retry',
958
+ 'description' => 'Retried requests report the first attempt time',
959
+ 'state' => 'open',
960
+ 'url' => 'https://github.com/org/repo/issues/17',
961
+ 'user' => 'john-doe',
962
+ 'created_at' => '2024-01-15T10:30:00Z',
963
+ 'updated_at' => '2024-01-20T14:22:00Z',
964
+ 'closed_at' => nil
965
+ }
966
+ ],
967
+ 'metadata' => {
968
+ 'next' => nil
969
+ }
970
+ }
971
+ ```
972
+
847
973
  ##### List Tags
848
974
 
849
975
  ```ruby
@@ -927,6 +1053,37 @@ puts "Branches: #{result['data']}"
927
1053
  }
928
1054
  ```
929
1055
 
1056
+ ##### List Commits
1057
+
1058
+ ```ruby
1059
+ result = unify.git.commits('organization/repo-name', branch: 'main', limit: 20)
1060
+
1061
+ puts "Commits: #{result['data']}"
1062
+ ```
1063
+
1064
+ `branch` is optional and accepts a branch name, tag or commit SHA. When it is omitted the
1065
+ provider's default branch is used.
1066
+
1067
+ **Response:**
1068
+
1069
+ ```ruby
1070
+ {
1071
+ 'data' => [
1072
+ {
1073
+ 'sha' => 'abc123def4567890abc123def4567890abc123de',
1074
+ 'message' => 'Add commits endpoint',
1075
+ 'url' => 'https://github.com/org/repo/commit/abc123def4567890abc123def4567890abc123de',
1076
+ 'author' => 'Jane Doe',
1077
+ 'author_email' => 'jane@example.com',
1078
+ 'committed_at' => '2024-01-15T10:30:00Z'
1079
+ }
1080
+ ],
1081
+ 'metadata' => {
1082
+ 'next' => nil
1083
+ }
1084
+ }
1085
+ ```
1086
+
930
1087
  #### Ticketing API
931
1088
 
932
1089
  The Ticketing API provides a unified interface for ticketing and project management platforms like Jira, Linear, and Asana.
@@ -971,6 +1128,69 @@ open_tickets = result['data'].select { |ticket| ticket['status'] == 'open' }
971
1128
  sorted_by_date = result['data'].sort_by { |ticket| Time.parse(ticket['created_at']) }.reverse
972
1129
  ```
973
1130
 
1131
+ ##### Get a Ticket
1132
+
1133
+ Fetch one ticket by ID. Not supported by Basecamp, whose API only serves a to-do underneath its project.
1134
+
1135
+ ```ruby
1136
+ result = unify.ticketing.ticket('PROJ-123')
1137
+
1138
+ puts "Ticket: #{result['data']}"
1139
+ ```
1140
+
1141
+ **Response:**
1142
+
1143
+ ```ruby
1144
+ {
1145
+ 'data' => {
1146
+ 'id' => 'PROJ-123',
1147
+ 'url' => 'https://jira.example.com/browse/PROJ-123',
1148
+ 'title' => 'Fix login bug',
1149
+ 'status' => 'in_progress',
1150
+ 'description' => 'Users are unable to log in',
1151
+ 'created_at' => '2024-01-15T10:30:00Z',
1152
+ 'updated_at' => '2024-01-20T14:22:00Z'
1153
+ }
1154
+ }
1155
+ ```
1156
+
1157
+ A single resource carries no pagination, so there is no `metadata` on this response.
1158
+
1159
+ ##### List Projects
1160
+
1161
+ ```ruby
1162
+ result = unify.ticketing.projects(
1163
+ limit: 100,
1164
+ after: nil,
1165
+ include_raw: false
1166
+ )
1167
+
1168
+ puts "Projects: #{result['data']}"
1169
+ ```
1170
+
1171
+ **Response:**
1172
+
1173
+ ```ruby
1174
+ {
1175
+ 'data' => [
1176
+ {
1177
+ 'id' => '10001',
1178
+ 'name' => 'Website Redesign',
1179
+ 'status' => 'active',
1180
+ 'url' => 'https://jira.example.com/browse/PROJ',
1181
+ 'description' => 'All the work for the new marketing site',
1182
+ 'created_at' => '2024-01-15T10:30:00Z',
1183
+ 'updated_at' => '2024-01-20T14:22:00Z'
1184
+ }
1185
+ ],
1186
+ 'metadata' => {
1187
+ 'next' => 'cursor_def456'
1188
+ }
1189
+ }
1190
+ ```
1191
+
1192
+ **Note:** Not every platform returns every field. Jira does not expose creation or update timestamps for projects, so `created_at` and `updated_at` are `nil` for Jira connections, and `status` is only set when Jira reports whether the project is archived.
1193
+
974
1194
  #### CRM API
975
1195
 
976
1196
  The CRM API provides a unified interface for CRM platforms like Attio, HubSpot, PipeDrive, Salesforce and Zoho.
@@ -1071,6 +1291,261 @@ puts "Files: #{result['data']}"
1071
1291
  }
1072
1292
  ```
1073
1293
 
1294
+ #### Calendar API
1295
+
1296
+ The Calendar API provides a unified interface for calendar and scheduling platforms like Google Calendar, Outlook, Calendly and Zoom.
1297
+
1298
+ ##### List Events
1299
+
1300
+ `starts_after` and `starts_before` are required — the endpoint refuses an unbounded listing.
1301
+
1302
+ ```ruby
1303
+ result = unify.calendar.events(
1304
+ starts_after: '2026-09-01T00:00:00Z',
1305
+ starts_before: '2026-09-08T00:00:00Z',
1306
+ limit: 100,
1307
+ after: nil,
1308
+ include_raw: false
1309
+ )
1310
+
1311
+ puts "Events: #{result['data']}"
1312
+ ```
1313
+
1314
+ **Response:**
1315
+
1316
+ ```ruby
1317
+ {
1318
+ 'data' => [
1319
+ {
1320
+ 'id' => 'evt_123',
1321
+ 'title' => 'Design review',
1322
+ 'description' => 'Walk through the new onboarding flow',
1323
+ 'start_date' => '2026-09-01T15:00:00Z',
1324
+ 'end_date' => '2026-09-01T16:00:00Z',
1325
+ 'status' => 'confirmed',
1326
+ 'url' => 'https://calendar.google.com/event?eid=...'
1327
+ }
1328
+ ],
1329
+ 'metadata' => {
1330
+ 'next' => nil
1331
+ }
1332
+ }
1333
+ ```
1334
+
1335
+ 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.
1336
+
1337
+ ### MCP API
1338
+
1339
+ Reach a provider's own MCP server using a connection's credentials. BundleUp injects and refreshes the access token, so the connection ID is the only thing your agent needs to know about a user.
1340
+
1341
+ Supported for providers that run a first-party MCP server — see the [integrations page](https://www.bundleup.io/integrations). Others return an `mcp_not_supported` error.
1342
+
1343
+ `post` and `delete` are transport only, like the Proxy API — responses come back untouched as `Faraday::Response` objects. `connect` layers a managed session on top when you would rather not drive the protocol yourself.
1344
+
1345
+ #### Creating an MCP Client
1346
+
1347
+ ```ruby
1348
+ mcp = client.mcp('conn_123abc')
1349
+ ```
1350
+
1351
+ #### Managed Sessions
1352
+
1353
+ `connect` returns a client that handles the handshake, session ID and response decoding, and exposes what the provider offers.
1354
+
1355
+ ```ruby
1356
+ mcp = client.mcp('conn_123abc').connect
1357
+
1358
+ tools = mcp.list_tools
1359
+ result = mcp.call_tool('create_issue', { title: 'Login broken' })
1360
+
1361
+ mcp.close
1362
+ ```
1363
+
1364
+ Resources and prompts follow the same shape:
1365
+
1366
+ ```ruby
1367
+ resources = mcp.list_resources
1368
+ contents = mcp.read_resource('file:///readme.md')
1369
+
1370
+ prompts = mcp.list_prompts
1371
+ messages = mcp.get_prompt('summarize', { id: '123' })
1372
+ ```
1373
+
1374
+ Anything else in the protocol:
1375
+
1376
+ ```ruby
1377
+ mcp.request('logging/setLevel', { level: 'debug' })
1378
+ ```
1379
+
1380
+ The handshake runs lazily on the first call and once per client, list methods follow `nextCursor` to the end, and `text/event-stream` responses are decoded for you. Results are hashes with string keys. Errors raise a `RuntimeError` with the provider's message, or BundleUp's with its code appended — `Missing or invalid connection ID (connection_invalid)`.
1381
+
1382
+ Call `close` when you are done to end the session upstream.
1383
+
1384
+ #### Model-Hosted MCP
1385
+
1386
+ OpenAI and Anthropic can connect to an MCP server themselves, with no tool mapping or dispatch loop on your side. Both accept only a single credential and no custom headers, so `hosted` returns the server URL alongside the API key and connection joined into one bearer.
1387
+
1388
+ ```ruby
1389
+ hosted = client.mcp('conn_123abc').hosted
1390
+
1391
+ response = openai.responses.create(
1392
+ model: 'gpt-4o',
1393
+ input: 'What issues are assigned to me?',
1394
+ tools: [
1395
+ {
1396
+ type: 'mcp',
1397
+ server_label: 'linear',
1398
+ server_url: hosted[:url],
1399
+ authorization: hosted[:token],
1400
+ require_approval: 'never'
1401
+ }
1402
+ ]
1403
+ )
1404
+ ```
1405
+
1406
+ Anthropic's connector takes the same pair as `url` and `authorization_token`. `client.unify('conn_123abc').mcp.hosted` returns them for Unified MCP.
1407
+
1408
+ `server_url` must be exactly the URL `hosted` returns — the proxy rebuilds the upstream URL from the provider's own base, so any path or query you append is ignored rather than rejected.
1409
+
1410
+ This sends your API key to the model provider, whose servers make the request. Use an MCP client in your own backend if that is not acceptable.
1411
+
1412
+ #### Using an MCP Client Library
1413
+
1414
+ `transport` returns the URL and headers if you would rather use an existing MCP client. Pass them to any client that supports the streamable HTTP transport.
1415
+
1416
+ ```ruby
1417
+ transport = client.mcp('conn_123abc').transport
1418
+
1419
+ transport[:url] # => "https://mcp.bundleup.io"
1420
+ transport[:headers] # => { "Authorization" => "Bearer ...", "BU-Connection-Id" => "conn_123abc", ... }
1421
+ ```
1422
+
1423
+ #### Sending JSON-RPC Directly
1424
+
1425
+ MCP requires an `initialize` handshake before any other method. The session ID comes back on that first response and must be sent on every call after it.
1426
+
1427
+ ```ruby
1428
+ mcp = client.mcp('conn_123abc')
1429
+
1430
+ # 1. Handshake
1431
+ init = mcp.post({
1432
+ jsonrpc: '2.0',
1433
+ id: 1,
1434
+ method: 'initialize',
1435
+ params: {
1436
+ protocolVersion: '2025-06-18',
1437
+ capabilities: {},
1438
+ clientInfo: { name: 'my-agent', version: '1.0.0' }
1439
+ }
1440
+ })
1441
+
1442
+ session_id = init.headers['mcp-session-id']
1443
+ session = session_id ? { 'Mcp-Session-Id' => session_id } : {}
1444
+
1445
+ # 2. Confirm the handshake (a notification — no id, no response body)
1446
+ mcp.post({ jsonrpc: '2.0', method: 'notifications/initialized' }, headers: session)
1447
+
1448
+ # 3. List tools
1449
+ response = mcp.post({ jsonrpc: '2.0', id: 2, method: 'tools/list' }, headers: session)
1450
+ result = parse(response)['result']
1451
+
1452
+ puts result['tools']
1453
+ ```
1454
+
1455
+ Providers may answer with `text/event-stream` rather than JSON, so responses need unwrapping either way:
1456
+
1457
+ ```ruby
1458
+ require 'json'
1459
+
1460
+ def parse(response)
1461
+ return JSON.parse(response.body) unless response.headers['content-type'].to_s.include?('text/event-stream')
1462
+
1463
+ data = response.body.lines
1464
+ .select { |line| line.start_with?('data:') }
1465
+ .map { |line| line[5..].strip }
1466
+ .join("\n")
1467
+
1468
+ JSON.parse(data)
1469
+ end
1470
+ ```
1471
+
1472
+ Tool lists can be paginated. If `result['nextCursor']` is set, call `tools/list` again with `params: { cursor: result['nextCursor'] }` until it comes back empty.
1473
+
1474
+ Calling a tool follows the same shape:
1475
+
1476
+ ```ruby
1477
+ response = mcp.post({
1478
+ jsonrpc: '2.0',
1479
+ id: 3,
1480
+ method: 'tools/call',
1481
+ params: { name: 'create_issue', arguments: { title: 'Login broken' } }
1482
+ }, headers: session)
1483
+ ```
1484
+
1485
+ #### Sessions
1486
+
1487
+ MCP sessions live on the provider's server — BundleUp holds no session state. Close one when you are done:
1488
+
1489
+ ```ruby
1490
+ mcp.delete(headers: { 'Mcp-Session-Id' => session_id })
1491
+ ```
1492
+
1493
+ #### Errors
1494
+
1495
+ BundleUp rejects a request before it reaches the provider by returning an HTTP error with a JSON body — the response is passed straight through, so check `response.success?` yourself.
1496
+
1497
+ ```ruby
1498
+ response = mcp.post(body)
1499
+
1500
+ unless response.success?
1501
+ error = JSON.parse(response.body)
1502
+ # connection_invalid, connection_refresh_failed, mcp_not_supported, rate_limit
1503
+ warn "#{error['code']}: #{error['message']}"
1504
+ end
1505
+ ```
1506
+
1507
+ Every JSON-RPC message counts toward the rate limit of 100 requests per 60 seconds, per connection — including the `initialize` handshake.
1508
+
1509
+ #### Merging Several Connections
1510
+
1511
+ An agent often needs more than one provider for the same end user. There is no merge helper in the SDK — how tools are namespaced, filtered and recovered from differs enough per agent that it is better written where you can see it:
1512
+
1513
+ ```ruby
1514
+ clients = {
1515
+ 'slack' => client.mcp(user.slack_connection).connect,
1516
+ 'linear' => client.mcp(user.linear_connection).connect,
1517
+ 'crm' => client.unify(user.hubspot_connection).mcp
1518
+ }
1519
+
1520
+ # One namespaced list: slack__send_message, linear__create_issue, â€Ķ
1521
+ tools = clients.flat_map do |label, mcp|
1522
+ mcp.list_tools.map { |tool| tool.merge('name' => "#{label}__#{tool['name']}") }
1523
+ end
1524
+
1525
+ # Route a call back to the client that owns it
1526
+ call = lambda do |name, args|
1527
+ label, tool_name = name.split('__', 2)
1528
+ clients.fetch(label).call_tool(tool_name, args)
1529
+ end
1530
+ ```
1531
+
1532
+ Anything that exposes `list_tools` and `call_tool(name, args)` fits the same shape, so an internal tool layer of your own can sit in that map alongside BundleUp connections.
1533
+
1534
+ Two things worth handling that the sketch above skips. **Filter before you hand the list to a model** — three providers is easily sixty tools, and accuracy drops as that list grows, so select the ones the agent actually needs rather than passing everything. And decide what an unreachable provider should do: as written, one failing `list_tools` raises and fails the whole list, while a `rescue` per client lets the others through.
1535
+
1536
+ #### Unified MCP
1537
+
1538
+ BundleUp's normalized tools instead of the provider's, on the same protocol. Tools only — Unified MCP exposes no resources or prompts.
1539
+
1540
+ ```ruby
1541
+ mcp = client.unify('conn_123abc').mcp
1542
+
1543
+ tools = mcp.list_tools
1544
+ result = mcp.call_tool('send_message', { text: 'Deploy finished' })
1545
+ ```
1546
+
1547
+ `unify.mcp` is memoized per Unify client, so the handshake runs once no matter how often you call it. The server itself is stateless and POST-only, so there is no session to close.
1548
+
1074
1549
  ## Error Handling
1075
1550
 
1076
1551
  The SDK raises exceptions for errors. Always wrap SDK calls in rescue blocks for proper error handling.
@@ -1112,7 +1587,9 @@ lib/
1112
1587
  ├── bundleup.rb # Main entry point
1113
1588
  ├── bundleup/
1114
1589
  │ ├── client.rb # Main client class
1590
+ │ ├── auth.rb # Auth API (authorization URL + code exchange)
1115
1591
  │ ├── proxy.rb # Proxy API implementation
1592
+ │ ├── mcp.rb # MCP API (transport + managed sessions)
1116
1593
  │ ├── unify.rb # Unify API client wrapper
1117
1594
  │ ├── version.rb # Gem version
1118
1595
  │ ├── resources/
@@ -1126,7 +1603,8 @@ lib/
1126
1603
  │ ├── git.rb # Git Unify API
1127
1604
  │ ├── ticketing.rb # Ticketing Unify API
1128
1605
  │ ├── crm.rb # CRM Unify API
1129
- │ └── drive.rb # Drive Unify API
1606
+ │ ├── drive.rb # Drive Unify API
1607
+ │ └── calendar.rb # Calendar Unify API
1130
1608
  sig/ # RBS type signatures (mirrors lib/)
1131
1609
  spec/ # Test files
1132
1610
  ```
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module BundleUp
6
+ # Client for the hosted authorization flow.
7
+ class Auth
8
+ BASE_URL = 'https://auth.bundleup.io'
9
+
10
+ attr_reader :api_key
11
+
12
+ def initialize(api_key)
13
+ @api_key = api_key
14
+ end
15
+
16
+ # Builds the hosted authorization URL to send the end user to.
17
+ def build_authorization_url(client_id:, integration_id:, redirect_uri:, external_id: nil, state: nil)
18
+ raise ArgumentError, 'client_id is required' if blank?(client_id)
19
+ raise ArgumentError, 'integration_id is required' if blank?(integration_id)
20
+ raise ArgumentError, 'redirect_uri is required' if blank?(redirect_uri)
21
+
22
+ params = {
23
+ client_id: client_id,
24
+ integration_id: integration_id,
25
+ redirect_uri: redirect_uri
26
+ }
27
+ params[:external_id] = external_id unless blank?(external_id)
28
+ params[:state] = state unless blank?(state)
29
+
30
+ "#{BASE_URL}/authorize?#{URI.encode_www_form(params)}"
31
+ end
32
+
33
+ # Exchanges the one-time code from the redirect for the connection.
34
+ # Call this server-side; the code expires after 5 minutes and works once.
35
+ # Returns { 'connection_id', 'external_id', 'integration_id' }.
36
+ def get_connection_from_code(code:, redirect_uri:)
37
+ raise ArgumentError, 'code is required' if blank?(code)
38
+ raise ArgumentError, 'redirect_uri is required' if blank?(redirect_uri)
39
+
40
+ response = connection.post('connection', { code: code, redirect_uri: redirect_uri })
41
+
42
+ raise "Failed to get connection: #{response.status}: #{response.body}" unless response.success?
43
+
44
+ response.body
45
+ end
46
+
47
+ private
48
+
49
+ def blank?(value)
50
+ value.nil? || value.to_s.empty?
51
+ end
52
+
53
+ # Memoize the Faraday connection to reuse it across requests
54
+ def connection
55
+ @connection ||= Faraday.new(url: BASE_URL) do |faraday|
56
+ faraday.headers = {
57
+ 'Authorization' => "Bearer #{@api_key}",
58
+ 'Content-Type' => 'application/json'
59
+ }
60
+
61
+ faraday.request :json
62
+ faraday.response :json, content_type: /\bjson$/
63
+ faraday.adapter Faraday.default_adapter
64
+ end
65
+ end
66
+ end
67
+ end
@@ -11,6 +11,10 @@ module BundleUp
11
11
  @api_key = api_key
12
12
  end
13
13
 
14
+ def auth
15
+ @auth ||= BundleUp::Auth.new(@api_key)
16
+ end
17
+
14
18
  def connections
15
19
  @connections ||= BundleUp::Resources::Connection.new(@api_key)
16
20
  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
@@ -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?
@@ -30,6 +30,21 @@ module BundleUp
30
30
  response.body
31
31
  end
32
32
 
33
+ # Fetches issues for a specific repository from the connected Git provider.
34
+ def issues(repo_name, params = {})
35
+ raise ArgumentError, 'repo_name is required to fetch issues.' if blank?(repo_name)
36
+
37
+ encoded_repo_name = URI.encode_www_form_component(repo_name)
38
+
39
+ response = connection.get("git/repos/#{encoded_repo_name}/issues") do |req|
40
+ req.params = params
41
+ end
42
+
43
+ raise "Failed to fetch git/repos/#{encoded_repo_name}/issues: #{response.status}" unless response.success?
44
+
45
+ response.body
46
+ end
47
+
33
48
  # Fetches tags for a specific repository from the connected Git provider.
34
49
  def tags(repo_name, params = {})
35
50
  raise ArgumentError, 'repo_name is required to fetch tags.' if blank?(repo_name)
@@ -14,6 +14,41 @@ 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
+ # Fetches projects from the connected ticketing tool.
37
+ def projects(params = {})
38
+ response = connection.get('ticketing/projects') do |req|
39
+ req.params = params
40
+ end
41
+
42
+ raise "Failed to fetch ticketing/projects: #{response.status}" unless response.success?
43
+
44
+ response.body
45
+ end
46
+
47
+ private
48
+
49
+ def blank?(value)
50
+ value.nil? || value.to_s.empty?
51
+ end
17
52
  end
18
53
  end
19
54
  end
@@ -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,11 @@ 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
+
44
50
  # Fetch the account this connection is authenticated as.
45
51
  #
46
52
  # `me` is the one unified method every provider implements, so it hangs off
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BundleUp
4
- VERSION = '0.4.2'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/bundleup.rb CHANGED
@@ -4,6 +4,7 @@ require 'faraday'
4
4
 
5
5
  require_relative 'bundleup/version'
6
6
  require_relative 'bundleup/client'
7
+ require_relative 'bundleup/auth'
7
8
  require_relative 'bundleup/proxy'
8
9
  require_relative 'bundleup/unify'
9
10
  require_relative 'bundleup/mcp'
@@ -21,6 +22,7 @@ require_relative 'bundleup/unify/git'
21
22
  require_relative 'bundleup/unify/ticketing'
22
23
  require_relative 'bundleup/unify/crm'
23
24
  require_relative 'bundleup/unify/drive'
25
+ require_relative 'bundleup/unify/calendar'
24
26
  require_relative 'bundleup/unify/mcp'
25
27
  require_relative 'bundleup/unify/me'
26
28
 
@@ -0,0 +1,31 @@
1
+ module BundleUp
2
+ class Auth
3
+ BASE_URL: String
4
+
5
+ type connection = {
6
+ "connection_id" => String,
7
+ "external_id" => String?,
8
+ "integration_id" => String
9
+ }
10
+
11
+ attr_reader api_key: String
12
+
13
+ def initialize: (String api_key) -> void
14
+
15
+ def build_authorization_url: (
16
+ client_id: String,
17
+ integration_id: String,
18
+ redirect_uri: String,
19
+ ?external_id: String?,
20
+ ?state: String?
21
+ ) -> String
22
+
23
+ def get_connection_from_code: (code: String, redirect_uri: String) -> connection
24
+
25
+ private
26
+
27
+ def blank?: (untyped value) -> bool
28
+
29
+ def connection: () -> untyped
30
+ end
31
+ end
@@ -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
@@ -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
@@ -26,6 +26,19 @@ module BundleUp
26
26
  merged_at: String?
27
27
  }
28
28
 
29
+ type git_issue = {
30
+ id: Integer,
31
+ number: Integer,
32
+ title: String,
33
+ description: String?,
34
+ state: String,
35
+ url: String,
36
+ user: String?,
37
+ created_at: String,
38
+ updated_at: String,
39
+ closed_at: String?
40
+ }
41
+
29
42
  type git_tag = { name: String, commit_sha: String }
30
43
 
31
44
  type git_release = {
@@ -41,13 +54,24 @@ module BundleUp
41
54
 
42
55
  type git_branch = { name: String, commit_sha: String, protected: bool }
43
56
 
57
+ type git_commit = {
58
+ sha: String,
59
+ message: String?,
60
+ url: String,
61
+ author: String?,
62
+ author_email: String?,
63
+ committed_at: String?
64
+ }
65
+
44
66
  type metadata = { next: String? }
45
67
 
46
68
  def repos: (?Hash[Symbol, untyped] params) -> { data: Array[git_repo], metadata: metadata }
47
69
  def pulls: (String repo_name, ?Hash[Symbol, untyped] params) -> { data: Array[git_pull], metadata: metadata }
70
+ def issues: (String repo_name, ?Hash[Symbol, untyped] params) -> { data: Array[git_issue], metadata: metadata }
48
71
  def tags: (String repo_name, ?Hash[Symbol, untyped] params) -> { data: Array[git_tag], metadata: metadata }
49
72
  def releases: (String repo_name, ?Hash[Symbol, untyped] params) -> { data: Array[git_release], metadata: metadata }
50
73
  def branches: (String repo_name, ?Hash[Symbol, untyped] params) -> { data: Array[git_branch], metadata: metadata }
74
+ def commits: (String repo_name, ?Hash[Symbol, untyped] params) -> { data: Array[git_commit], metadata: metadata }
51
75
  end
52
76
  end
53
77
  end
@@ -11,9 +11,37 @@ 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
+
24
+ type ticketing_project = {
25
+ id: String,
26
+ name: String,
27
+ status: String?,
28
+ url: String?,
29
+ description: String?,
30
+ created_at: String?,
31
+ updated_at: String?
32
+ }
33
+
14
34
  type metadata = { next: String? }
15
35
 
16
- def issues: (?Hash[Symbol, untyped] params) -> { data: Array[ticketing_ticket], metadata: metadata }
36
+ def tickets: (?Hash[Symbol, untyped] params) -> { data: Array[ticketing_ticket], metadata: metadata }
37
+
38
+ def ticket: (String ticket_id, ?Hash[Symbol, untyped] params) -> { data: ticket }
39
+
40
+ def projects: (?Hash[Symbol, untyped] params) -> { data: Array[ticketing_project], metadata: metadata }
41
+
42
+ private
43
+
44
+ def blank?: (untyped) -> bool
17
45
  end
18
46
  end
19
47
  end
@@ -8,9 +8,10 @@ module BundleUp
8
8
 
9
9
  def chat: () -> Chat
10
10
  def git: () -> Git
11
- def pm: () -> PM
11
+ def ticketing: () -> Ticketing
12
12
  def crm: () -> CRM
13
13
  def drive: () -> Drive
14
+ def calendar: () -> Calendar
14
15
  def me: (?Hash[Symbol, untyped] params) -> { data: Me::account }
15
16
  end
16
17
  end
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.2
4
+ version: 0.5.0
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-25 00:00:00.000000000 Z
11
+ date: 2026-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -167,9 +167,9 @@ files:
167
167
  - LICENSE
168
168
  - README.md
169
169
  - lib/bundleup.rb
170
+ - lib/bundleup/auth.rb
170
171
  - lib/bundleup/client.rb
171
172
  - lib/bundleup/mcp.rb
172
- - lib/bundleup/mcp_client.rb
173
173
  - lib/bundleup/proxy.rb
174
174
  - lib/bundleup/resources/base.rb
175
175
  - lib/bundleup/resources/connection.rb
@@ -177,6 +177,7 @@ 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
@@ -185,8 +186,10 @@ files:
185
186
  - lib/bundleup/unify/me.rb
186
187
  - lib/bundleup/unify/ticketing.rb
187
188
  - lib/bundleup/version.rb
189
+ - sig/bundleup/auth.rbs
188
190
  - sig/bundleup/unify.rbs
189
191
  - sig/bundleup/unify/base.rbs
192
+ - sig/bundleup/unify/calendar.rbs
190
193
  - sig/bundleup/unify/chat.rbs
191
194
  - sig/bundleup/unify/crm.rbs
192
195
  - sig/bundleup/unify/drive.rbs
@@ -1,202 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module BundleUp
6
- # A connected MCP session.
7
- #
8
- # Tools, resources and prompts are defined by the provider — BundleUp does
9
- # not rename or normalize them.
10
- class MCPClient
11
- PROTOCOL_VERSION = '2025-06-18'
12
- CLIENT_NAME = 'bundleup-sdk'
13
-
14
- def initialize(base_url, api_key, connection_id)
15
- @base_url = base_url
16
- @api_key = api_key
17
- @connection_id = connection_id
18
- @session_id = nil
19
- @connected = false
20
- @last_id = 0
21
- end
22
-
23
- # List the provider's tools, following pagination to the end.
24
- def tools
25
- paginate('tools/list', 'tools')
26
- end
27
-
28
- # Call a tool by name, with arguments matching its own input schema.
29
- def tool(name, args = {})
30
- raise ArgumentError, 'Tool name is required to call a tool.' if blank?(name)
31
-
32
- connect
33
- send_message('tools/call', { name: name, arguments: args })
34
- end
35
-
36
- # List the provider's resources, following pagination to the end.
37
- def resources
38
- paginate('resources/list', 'resources')
39
- end
40
-
41
- # Read a resource by URI.
42
- def resource(uri)
43
- raise ArgumentError, 'Resource URI is required to read a resource.' if blank?(uri)
44
-
45
- connect
46
- send_message('resources/read', { uri: uri })
47
- end
48
-
49
- # List the provider's prompts, following pagination to the end.
50
- def prompts
51
- paginate('prompts/list', 'prompts')
52
- end
53
-
54
- # Get a prompt by name.
55
- def prompt(name, args = {})
56
- raise ArgumentError, 'Prompt name is required to get a prompt.' if blank?(name)
57
-
58
- connect
59
- send_message('prompts/get', { name: name, arguments: args })
60
- end
61
-
62
- # Send any other JSON-RPC method on this session.
63
- def request(method, params = nil)
64
- raise ArgumentError, 'Method is required to send a request.' if blank?(method)
65
-
66
- connect
67
- send_message(method, params)
68
- end
69
-
70
- # End the session and reset local state.
71
- def close
72
- delete_session if @session_id
73
-
74
- @session_id = nil
75
- @connected = false
76
- nil
77
- end
78
-
79
- private
80
-
81
- def blank?(value)
82
- value.nil? || value.to_s.empty?
83
- end
84
-
85
- def default_headers
86
- headers = {
87
- 'Authorization' => '******',
88
- 'Content-Type' => 'application/json',
89
- 'Accept' => 'application/json, text/event-stream',
90
- 'BU-Connection-Id' => @connection_id
91
- }
92
- headers['Mcp-Session-Id'] = @session_id if @session_id
93
- headers
94
- end
95
-
96
- def connection
97
- @connection ||= Faraday.new { |faraday| faraday.adapter Faraday.default_adapter }
98
- end
99
-
100
- def post_payload(payload)
101
- response = connection.post(@base_url, payload.to_json, default_headers)
102
- session_id = response.headers['mcp-session-id']
103
- @session_id = session_id if session_id
104
-
105
- raise error_for(response) unless response.success?
106
-
107
- response
108
- end
109
-
110
- def error_for(response)
111
- fallback = "MCP request failed with status #{response.status}."
112
- parsed = JSON.parse(response.body.to_s)
113
- return RuntimeError.new(fallback) unless parsed.is_a?(Hash) && parsed['message']
114
-
115
- code = parsed['code']
116
- RuntimeError.new(code ? "#{parsed['message']} (#{code})" : parsed['message'])
117
- rescue JSON::ParserError
118
- RuntimeError.new(fallback)
119
- end
120
-
121
- # Run the MCP handshake, once. Deferred until the first call.
122
- def connect
123
- return if @connected
124
-
125
- send_message('initialize', handshake_params)
126
- post_payload({ jsonrpc: '2.0', method: 'notifications/initialized' })
127
- @connected = true
128
- end
129
-
130
- def handshake_params
131
- {
132
- protocolVersion: PROTOCOL_VERSION,
133
- capabilities: {},
134
- clientInfo: { name: CLIENT_NAME, version: BundleUp::VERSION }
135
- }
136
- end
137
-
138
- def send_message(method, params = nil)
139
- @last_id += 1
140
- payload = { jsonrpc: '2.0', id: @last_id, method: method }
141
- payload[:params] = params unless params.nil?
142
-
143
- message = parse(post_payload(payload), @last_id)
144
- raise "No response received for #{method}." if message.nil?
145
- raise message['error']['message'].to_s if message['error']
146
-
147
- message['result'] || {}
148
- end
149
-
150
- # Providers may answer a plain request/response over text/event-stream.
151
- def parse(response, message_id)
152
- body = response.body.to_s
153
- return nil if body.empty?
154
-
155
- content_type = response.headers['content-type'].to_s
156
- return JSON.parse(body) unless content_type.include?('text/event-stream')
157
-
158
- parse_stream(body, message_id)
159
- end
160
-
161
- def parse_stream(body, message_id)
162
- body.gsub("\r\n", "\n").split("\n\n").each do |event|
163
- data = event_data(event)
164
- next if data.empty?
165
-
166
- message = JSON.parse(data)
167
- # Skip server notifications interleaved on the stream.
168
- return message if message.is_a?(Hash) && message['id'] == message_id
169
- end
170
-
171
- nil
172
- end
173
-
174
- def event_data(event)
175
- event.split("\n")
176
- .select { |line| line.start_with?('data:') }
177
- .map { |line| line.sub('data:', '').strip }
178
- .join("\n")
179
- end
180
-
181
- def paginate(method, key)
182
- connect
183
- items = []
184
- cursor = nil
185
-
186
- loop do
187
- result = send_message(method, cursor ? { cursor: cursor } : nil)
188
- items.concat(result[key] || [])
189
- cursor = result['nextCursor']
190
- break unless cursor
191
- end
192
-
193
- items
194
- end
195
-
196
- def delete_session
197
- connection.delete(@base_url, nil, default_headers)
198
- rescue Faraday::Error
199
- nil
200
- end
201
- end
202
- end