basecamp-sdk 0.25.0 → 0.26.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: 7713550a3bc2d6b5dd732e3bdadb679d5016316e9f4004677c028a30daa65371
4
- data.tar.gz: 71b74a2b499c484f245669d9b317df4f826c799f2d2bbda0cce140a75b22884f
3
+ metadata.gz: 1fd42c71ec94ed3fca5d41b7f015309154da9cba442bd5d7e1a4daa41fcc47f7
4
+ data.tar.gz: 38b36b58f82eb91498ec501a1d497ea18210ad07a7f4548820350a3f69c08724
5
5
  SHA512:
6
- metadata.gz: a5f430f32c8ef3e13f0b3690ec31223bc942f06bc907708fe33fbfa84b99a86dcfe56a2734ef0e8224f5cfdfdc41ce4c28703e8321c6ba88330bba4bedf775f0
7
- data.tar.gz: 834ea9c748c9d64ce67089e765875bf8c6c56a52ce0d895629b1c02f4d6e2acbdcb63b566a563c19823ce1c3199016b02fba3c92136bbbd5cb67cf6d950a0744
6
+ metadata.gz: 89dd7bc9af9f11a0365f768ffec3a6335b801f4278834dfadbbfe5963046f4da5f33bece26b8b112798896128e2def06c405d31f04b7f07b29e7432a384b9d83
7
+ data.tar.gz: 716839ec940b554a08f9049ec03caa88090e7965575dd29567545a38e04f8cd46176daf2de2882df079c32684f339e1b4c7ad09be523e0840854b0e0240919c3
data/README.md CHANGED
@@ -348,6 +348,7 @@ The SDK provides the full account-scoped service set documented in SPEC §5. The
348
348
  | `card_columns` | Card table columns |
349
349
  | `card_steps` | Card workflow steps |
350
350
  | `subtasks` | Subtasks (checklist items) on to-dos and cards |
351
+ | `pings` | Pings (direct and group chats) |
351
352
  | `wormholes` | Card table wormholes (cross-project moves) |
352
353
  | `lineup` | Card lineup view |
353
354
  | `tools` | Project dock tools |
@@ -8,12 +8,15 @@ module Basecamp
8
8
  # Candidates come from two sources, tried in order and each cached {TTL}
9
9
  # seconds:
10
10
  #
11
- # 1. The bucket's project dock, whose "chat" tool is the project's Campfire:
12
- # one project read per bucket, and the answer for every line posted in a
13
- # project. Cached per bucket.
11
+ # 1. The bucket's own Campfire. For a project, its dock's "chat" tool: one
12
+ # project read per bucket, and the answer for every line posted in a
13
+ # project. For a bucket that is not a project, its Ping's Campfire, since
14
+ # a Ping's bucket type is "Circle". Cached per bucket.
14
15
  # 2. The account-wide Campfire listing (BC3 has no per-bucket one), filtered
15
- # to the bucket, for buckets that are not projects or whose dock did not
16
- # hold the line. Cached per account, so a burst of lines costs one listing.
16
+ # to the bucket, for buckets that are neither or whose own Campfire did not
17
+ # hold the line. BC3 lists only dock-positioned Campfires there, so a
18
+ # ping's is never in it. Cached per account, so a burst of lines costs one
19
+ # listing.
17
20
  #
18
21
  # This class holds the caching only; the reads themselves are passed in as
19
22
  # blocks by +RecordingsExtensions+, so every request still goes through a
@@ -28,7 +31,7 @@ module Basecamp
28
31
  # buckets and accounts consulted within the last TTL, and never more than the
29
32
  # bound, whatever the Client has seen.
30
33
  class CampfireIndex
31
- # How long a cached discovery source — a bucket's project dock, the
34
+ # How long a cached discovery source — a bucket's own Campfire, the
32
35
  # account's Campfire listing — is reused before it is read again.
33
36
  TTL = 600.0
34
37
 
@@ -40,12 +43,13 @@ module Basecamp
40
43
 
41
44
  # Caps the account-wide Campfire listing the fallback source reads. A
42
45
  # listing that overflows it is not cached and the call reports
43
- # {CampfireDiscoveryIncompleteError}: the dock covers every project, so the
44
- # listing only ever serves the leftover, and an account with more Campfires
45
- # than this should not pay a full walk per TTL for it.
46
+ # {CampfireDiscoveryIncompleteError}: the bucket's own Campfire covers every
47
+ # project and every ping, so the listing only ever serves the leftover, and
48
+ # an account with more Campfires than this should not pay a full walk per
49
+ # TTL for it.
46
50
  MAX_LISTING = 1000
47
51
 
48
- # Bounds each cache's entry count. The dock cache holds one snapshot per
52
+ # Bounds each cache's entry count. The bucket cache holds one snapshot per
49
53
  # bucket consulted within the TTL: a connector listening across every
50
54
  # project an agent can see touches hundreds of buckets, not thousands, and a
51
55
  # snapshot is a handful of ids, so this is generous headroom at a few
@@ -75,12 +79,13 @@ module Basecamp
75
79
  @listings = TTLCache.new(ttl: TTL, floor: MIN_REFRESH, max_items: MAX_ITEMS, clock: clock)
76
80
  end
77
81
 
78
- # The Campfire ids a bucket's project dock names.
82
+ # The Campfire ids a bucket's own source names: a project's dock, or a
83
+ # Ping's room.
79
84
  #
80
85
  # @param account_id [String]
81
86
  # @param bucket_id [Integer]
82
87
  # @param refresh [Boolean] re-read a cached snapshot older than {MIN_REFRESH}
83
- # @yieldreturn [Array<Integer>] the dock's Campfire ids
88
+ # @yieldreturn [Array<Integer>] the bucket's own Campfire ids
84
89
  # @return [SourceRead]
85
90
  def dock_campfires(account_id:, bucket_id:, refresh: false, &load)
86
91
  hit = @docks.get([ account_id, bucket_id ], refresh: refresh, &load)
@@ -92,7 +97,8 @@ module Basecamp
92
97
  #
93
98
  # It exists so a caller can consult what the source already holds before
94
99
  # deciding whether to pay for the listing fetch — the expensive, slow
95
- # request, which the dock (including its refresh) should get to pre-empt.
100
+ # request, which the bucket's own source (including its refresh) should get
101
+ # to pre-empt.
96
102
  #
97
103
  # @param account_id [String]
98
104
  # @param bucket_id [Integer]
@@ -612,6 +612,11 @@ module Basecamp
612
612
  service(:subtasks) { Services::SubtasksService.new(self) }
613
613
  end
614
614
 
615
+ # @return [Services::PingsService]
616
+ def pings
617
+ service(:pings) { Services::PingsService.new(self) }
618
+ end
619
+
615
620
  # @return [Services::AccountService]
616
621
  def account
617
622
  service(:account) { Services::AccountService.new(self) }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://basecamp.com/schemas/sdk-metadata.json",
3
3
  "version": "1.0.0",
4
- "generated": "2026-10-15T16:04:42Z",
4
+ "generated": "2026-10-19T21:23:14Z",
5
5
  "operations": {
6
6
  "GetAccount": {
7
7
  "retry": {
@@ -78,6 +78,20 @@
78
78
  ]
79
79
  }
80
80
  },
81
+ "UpdateBoost": {
82
+ "retry": {
83
+ "maxAttempts": 3,
84
+ "baseDelayMs": 1000,
85
+ "backoff": "exponential",
86
+ "retryOn": [
87
+ 429,
88
+ 503
89
+ ]
90
+ },
91
+ "idempotent": {
92
+ "natural": true
93
+ }
94
+ },
81
95
  "DeleteBoost": {
82
96
  "retry": {
83
97
  "maxAttempts": 3,
@@ -919,6 +933,28 @@
919
933
  "maxPageSize": 50
920
934
  }
921
935
  },
936
+ "ListPings": {
937
+ "retry": {
938
+ "maxAttempts": 3,
939
+ "baseDelayMs": 1000,
940
+ "backoff": "exponential",
941
+ "retryOn": [
942
+ 429,
943
+ 503
944
+ ]
945
+ }
946
+ },
947
+ "CreatePing": {
948
+ "retry": {
949
+ "maxAttempts": 2,
950
+ "baseDelayMs": 1000,
951
+ "backoff": "exponential",
952
+ "retryOn": [
953
+ 429,
954
+ 503
955
+ ]
956
+ }
957
+ },
922
958
  "ListPingablePeople": {
923
959
  "retry": {
924
960
  "maxAttempts": 3,
@@ -935,6 +971,17 @@
935
971
  "maxPageSize": 50
936
972
  }
937
973
  },
974
+ "GetPing": {
975
+ "retry": {
976
+ "maxAttempts": 3,
977
+ "baseDelayMs": 1000,
978
+ "backoff": "exponential",
979
+ "retryOn": [
980
+ 429,
981
+ 503
982
+ ]
983
+ }
984
+ },
938
985
  "GetClientApproval": {
939
986
  "retry": {
940
987
  "maxAttempts": 3,
@@ -3730,6 +3777,9 @@
3730
3777
  "CreateMessage": [
3731
3778
  "creator"
3732
3779
  ],
3780
+ "CreatePing": [
3781
+ "people.[]"
3782
+ ],
3733
3783
  "CreateQuestion": [
3734
3784
  "creator"
3735
3785
  ],
@@ -4077,6 +4127,9 @@
4077
4127
  "events.[].creator",
4078
4128
  "person"
4079
4129
  ],
4130
+ "GetPing": [
4131
+ "people.[]"
4132
+ ],
4080
4133
  "GetProgressReport": [
4081
4134
  "[].attachments.[].creator",
4082
4135
  "[].creator"
@@ -4251,6 +4304,9 @@
4251
4304
  "ListPingablePeople": [
4252
4305
  "[]"
4253
4306
  ],
4307
+ "ListPings": [
4308
+ "[].people.[]"
4309
+ ],
4254
4310
  "ListProjectPeople": [
4255
4311
  "[]"
4256
4312
  ],
@@ -4340,6 +4396,9 @@
4340
4396
  "Subscribe": [
4341
4397
  "subscribers.[]"
4342
4398
  ],
4399
+ "UpdateBoost": [
4400
+ "booster"
4401
+ ],
4343
4402
  "UpdateCard": [
4344
4403
  "assignees.[]",
4345
4404
  "completer",
@@ -16,6 +16,16 @@ module Basecamp
16
16
  end
17
17
  end
18
18
 
19
+ # Replace the content of a boost. Only the boost's creator may update it: an
20
+ # @param boost_id [Integer] boost id ID
21
+ # @param content [String] content
22
+ # @return [Hash] response data
23
+ def update_boost(boost_id:, content:)
24
+ with_operation(service: "boosts", operation: "update_boost", is_mutation: true, resource_id: boost_id) do
25
+ http_put("/boosts/#{boost_id}", body: compact_params(content: content)).json(operation: "UpdateBoost")
26
+ end
27
+ end
28
+
19
29
  # Delete a boost
20
30
  # @param boost_id [Integer] boost id ID
21
31
  # @return [void]
@@ -29,9 +29,10 @@ module Basecamp
29
29
  end
30
30
  end
31
31
 
32
- # Move a card to a different column
32
+ # Move a card to any column in its project, including on another card table there, or through a linked wormhole to another project (queued: the card gets a new id, and position, if sent, must still be positive but doesn't set placement).
33
33
  # @param card_id [Integer] card id ID
34
- # @param column_id [Integer] column id
34
+ # @param column_id [Integer] Any column in the card's project, on this or another card table, or a
35
+ # wormhole id to send the card to another project.
35
36
  # @param position [Integer, nil] 1-indexed position within the destination column. Defaults to 1 (top).
36
37
  # @return [void]
37
38
  def move(card_id:, column_id:, position: nil)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for Pings operations
6
+ #
7
+ # @generated from OpenAPI spec
8
+ class PingsService < BaseService
9
+
10
+ # Get Pings: the caller's active Pings that have a line in them, most
11
+ # @return [Array<Hash>] response data
12
+ def list()
13
+ with_operation(service: "pings", operation: "list", is_mutation: false) do
14
+ http_get("/circles.json", operation: "ListPings").json(operation: "ListPings")
15
+ end
16
+ end
17
+
18
+ # Create a Ping with people, or return the one that already holds exactly
19
+ # @param user_ids [Array] Person ids of the people to ping. People the caller cannot reach are
20
+ # dropped.
21
+ # @param new_group [Boolean, nil] Create a new group even when a Ping with exactly these people exists.
22
+ # @return [Hash] response data
23
+ def create(user_ids:, new_group: nil)
24
+ with_operation(service: "pings", operation: "create", is_mutation: true) do
25
+ http_post("/circles.json", body: compact_params(user_ids: user_ids, new_group: new_group)).json(operation: "CreatePing")
26
+ end
27
+ end
28
+
29
+ # Get a Ping by its id, which is its bucket's id.
30
+ # @param ping_id [Integer] A Ping's id is the id of the bucket that holds it (the bucket type is
31
+ # "Circle", which is also the wire path), so the `bucket.id` a Ping's
32
+ # recordings carry is the id GetPing takes.
33
+ # @return [Hash] response data
34
+ def get(ping_id:)
35
+ with_operation(service: "pings", operation: "get", is_mutation: false, resource_id: ping_id) do
36
+ http_get("/circles/#{ping_id}", operation: "GetPing").json(operation: "GetPing")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -7,7 +7,7 @@ module Basecamp
7
7
  # @generated from OpenAPI spec
8
8
  class WormholesService < BaseService
9
9
 
10
- # Update a wormhole's destination column
10
+ # Point a card table's wormhole, its shortcut for sending cards to another card table, at a different destination column
11
11
  # @param bucket_id [Integer] bucket id ID
12
12
  # @param wormhole_id [Integer] wormhole id ID
13
13
  # @param destination_recording_id [Integer] Id of the new destination column (on another accessible card table).
@@ -18,7 +18,7 @@ module Basecamp
18
18
  end
19
19
  end
20
20
 
21
- # Delete a wormhole
21
+ # Permanently delete a card table's wormhole, the shortcut that sends cards to another card table.
22
22
  # @param bucket_id [Integer] bucket id ID
23
23
  # @param wormhole_id [Integer] wormhole id ID
24
24
  # @return [void]
@@ -29,7 +29,7 @@ module Basecamp
29
29
  end
30
30
  end
31
31
 
32
- # Create a wormhole linking this card table to a column on another card table.
32
+ # Add a wormhole to a card table: a standing shortcut that sends cards to a column on another card table, usually in another project.
33
33
  # @param bucket_id [Integer] bucket id ID
34
34
  # @param card_table_id [Integer] card table id ID
35
35
  # @param destination_recording_id [Integer] Id of the destination column (on another accessible card table) to link to.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Auto-generated from OpenAPI spec. Do not edit manually.
4
- # Generated: 2026-10-15T16:04:42Z
4
+ # Generated: 2026-10-19T21:23:14Z
5
5
 
6
6
  require "json"
7
7
  require "time"
@@ -3208,6 +3208,84 @@ module Basecamp
3208
3208
  end
3209
3209
  end
3210
3210
 
3211
+ # Ping
3212
+ class Ping
3213
+ include TypeHelpers
3214
+ attr_accessor :app_url, :avatar_url, :bookmark_url, :created_at, :id, :links, :named, :people, :room_url, :title, :updated_at, :url, :edit_title_url, :image_url
3215
+
3216
+ # @return [Array<Symbol>]
3217
+ def self.required_fields
3218
+ %i[app_url avatar_url bookmark_url created_at id links named people room_url title updated_at url].freeze
3219
+ end
3220
+
3221
+ def initialize(data = {})
3222
+ @app_url = data["app_url"]
3223
+ @avatar_url = data["avatar_url"]
3224
+ @bookmark_url = data["bookmark_url"]
3225
+ @created_at = parse_datetime(data["created_at"])
3226
+ @id = parse_integer(data["id"])
3227
+ @links = parse_type(data["links"], "PingLinks")
3228
+ @named = parse_boolean(data["named"])
3229
+ @people = parse_array(data["people"], "Person")
3230
+ @room_url = data["room_url"]
3231
+ @title = data["title"]
3232
+ @updated_at = parse_datetime(data["updated_at"])
3233
+ @url = data["url"]
3234
+ @edit_title_url = data["edit_title_url"]
3235
+ @image_url = data["image_url"]
3236
+ end
3237
+
3238
+ def to_h
3239
+ {
3240
+ "app_url" => @app_url,
3241
+ "avatar_url" => @avatar_url,
3242
+ "bookmark_url" => @bookmark_url,
3243
+ "created_at" => @created_at,
3244
+ "id" => @id,
3245
+ "links" => @links,
3246
+ "named" => @named,
3247
+ "people" => @people,
3248
+ "room_url" => @room_url,
3249
+ "title" => @title,
3250
+ "updated_at" => @updated_at,
3251
+ "url" => @url,
3252
+ "edit_title_url" => @edit_title_url,
3253
+ "image_url" => @image_url,
3254
+ }.compact
3255
+ end
3256
+
3257
+ def to_json(*args)
3258
+ to_h.to_json(*args)
3259
+ end
3260
+ end
3261
+
3262
+ # PingLinks
3263
+ class PingLinks
3264
+ include TypeHelpers
3265
+ attr_accessor :files, :lines
3266
+
3267
+ # @return [Array<Symbol>]
3268
+ def self.required_fields
3269
+ %i[files lines].freeze
3270
+ end
3271
+
3272
+ def initialize(data = {})
3273
+ @files = data["files"]
3274
+ @lines = data["lines"]
3275
+ end
3276
+
3277
+ def to_h
3278
+ {
3279
+ "files" => @files,
3280
+ "lines" => @lines,
3281
+ }.compact
3282
+ end
3283
+
3284
+ def to_json(*args)
3285
+ to_h.to_json(*args)
3286
+ end
3287
+ end
3288
+
3211
3289
  # Preferences
3212
3290
  class Preferences
3213
3291
  include TypeHelpers
@@ -49,6 +49,8 @@ module Basecamp
49
49
  "The recording summary reads this field to decide what the recording is and which " \
50
50
  "project it belongs to, so a value of the wrong type cannot be used."
51
51
 
52
+ ROOM_URL_CAMPFIRE = %r{/chats/([0-9]+)(?:\.json)?\z}
53
+
52
54
  DISCOVERY_HINT =
53
55
  "The recording summary reads this field to decide which Campfires a chat line could " \
54
56
  "be in, so a value of the wrong type cannot be used."
@@ -133,10 +135,10 @@ module Basecamp
133
135
  RICH_TEXT_CHAT_LINE_TYPES = [ "Chat::Lines::RichText", "Chat::Lines::Integration" ].freeze
134
136
 
135
137
  # Bounds how many Campfires one {#summarize} call tries, across both
136
- # discovery sources and the refresh. A project has one Campfire and a
137
- # handful of pings; a bucket past this bound is not a shape BC3 produces,
138
- # and the call reports {Basecamp::CampfireDiscoveryIncompleteError} rather
139
- # than calling the rest absent.
138
+ # discovery sources and the refresh. A project or a Ping has one
139
+ # Campfire; a bucket past this bound is not a shape BC3 produces, and the
140
+ # call reports {Basecamp::CampfireDiscoveryIncompleteError} rather than
141
+ # calling the rest absent.
140
142
  MAX_CAMPFIRE_CANDIDATES = 50
141
143
 
142
144
  # Where #project leaves the validated bucket id for the cross-bucket
@@ -1029,14 +1031,14 @@ module Basecamp
1029
1031
  account_id = @client.account_id
1030
1032
  search = ChatLineSearch.new(campfires: @client.campfires, line_id: line_id)
1031
1033
 
1032
- # Pass 1: what the sources already hold — the dock (read if it must be),
1033
- # then the listing only if it is cached. A listing fetch is the
1034
- # expensive, slow request, and it is not made until the dock — including
1035
- # its refresh — has had its say, so a listing that is down, over its cap,
1036
- # or stalled never stands between a project's line and the one project
1037
- # read that finds it.
1034
+ # Pass 1: what the sources already hold — the bucket's own Campfire
1035
+ # (read if it must be), then the listing only if it is cached. A listing
1036
+ # fetch is the expensive, slow request, and it is not made until the
1037
+ # bucket's own source — including its refresh — has had its say, so a
1038
+ # listing that is down, over its cap, or stalled never stands between a
1039
+ # project's line and the one project read that finds it.
1038
1040
  dock = index.dock_campfires(account_id: account_id, bucket_id: bucket_id) do
1039
- dock_campfire_ids(bucket_id)
1041
+ bucket_campfire_ids(bucket_id)
1040
1042
  end
1041
1043
  found = search.try(dock.ids)
1042
1044
  return found if found
@@ -1048,7 +1050,7 @@ module Basecamp
1048
1050
  return found if found
1049
1051
  end
1050
1052
 
1051
- # Pass 2: re-read the dock if it was served from cache (the floor may
1053
+ # Pass 2: re-read the bucket's own source if it was served from cache (the floor may
1052
1054
  # decline), then fetch or refresh the listing. Whatever comes back is the
1053
1055
  # current snapshot of that source, whoever loaded it — another caller may
1054
1056
  # have populated or refreshed it in the meantime — so it always replaces
@@ -1071,7 +1073,7 @@ module Basecamp
1071
1073
 
1072
1074
  if search.budget_left? && dock.cached?
1073
1075
  again = index.dock_campfires(account_id: account_id, bucket_id: bucket_id, refresh: true) do
1074
- dock_campfire_ids(bucket_id)
1076
+ bucket_campfire_ids(bucket_id)
1075
1077
  end
1076
1078
  refreshed = true if again.fetched > dock.fetched || !again.cached?
1077
1079
  dock = again
@@ -1119,17 +1121,52 @@ module Basecamp
1119
1121
  )
1120
1122
  end
1121
1123
 
1122
- # The Campfire ids a bucket's project dock names. A bucket that is not a
1123
- # project (a 404 on the project read) has none; any other failure of the
1124
- # read is raised.
1125
- def dock_campfire_ids(bucket_id)
1124
+ # The bucket's own Campfire ids: a project's dock "chat" tools or, when
1125
+ # the project read 404s, its Ping's Campfire. A bucket that is neither
1126
+ # has none; any other failure of either read is raised, so a 403 "Pings
1127
+ # have been disabled" is a failed read and never an unresolved line.
1128
+ def bucket_campfire_ids(bucket_id)
1126
1129
  project = begin
1127
1130
  @client.projects.get(project_id: bucket_id)
1131
+ rescue NotFoundError
1132
+ return ping_campfire_ids(bucket_id)
1133
+ end
1134
+
1135
+ dock_campfire_ids(read_record(project))
1136
+ end
1137
+
1138
+ # The one Campfire a Ping holds, named by its +room_url+. A Ping whose
1139
+ # room_url names no Campfire has none.
1140
+ def ping_campfire_ids(bucket_id)
1141
+ ping = begin
1142
+ @client.pings.get(ping_id: bucket_id)
1128
1143
  rescue NotFoundError
1129
1144
  return []
1130
1145
  end
1131
1146
 
1132
- project = read_record(project)
1147
+ room_url = read_record(ping)["room_url"]
1148
+ return [] if room_url.nil?
1149
+
1150
+ unless room_url.is_a?(String)
1151
+ raise malformed_response("the Ping's \"room_url\" is #{MergeSafe.describe(room_url)}, not a string",
1152
+ hint: DISCOVERY_HINT)
1153
+ end
1154
+
1155
+ id = campfire_id_from_room_url(room_url)
1156
+ id ? [ id ] : []
1157
+ end
1158
+
1159
+ # Strict: the path must end in +/chats/{digits}+, optionally +.json+, so a
1160
+ # Campfire's lines, a to-do or a garbled URL name no Campfire.
1161
+ def campfire_id_from_room_url(room_url)
1162
+ match = ROOM_URL_CAMPFIRE.match(room_url.split(/[?#]/, 2).first)
1163
+ return nil if match.nil?
1164
+
1165
+ id = Ids.bounded_decimal(match[1], signed: false)
1166
+ id.is_a?(Integer) && id.positive? ? id : nil
1167
+ end
1168
+
1169
+ def dock_campfire_ids(project)
1133
1170
  dock = project["dock"]
1134
1171
  # An absent dock is genuinely none — a project need not have one, and the
1135
1172
  # reference reads its zero value. Anything else that is not an array is a
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basecamp
4
- VERSION = "0.25.0"
5
- API_VERSION = "2026-09-15"
4
+ VERSION = "0.26.0"
5
+ API_VERSION = "2026-10-19"
6
6
  end
@@ -83,6 +83,7 @@ class ServiceGenerator
83
83
  'ClientFeatures' => 'ClientFeatures',
84
84
  'Boosts' => 'Boosts',
85
85
  'Subtasks' => 'Subtasks',
86
+ 'Pings' => 'Pings',
86
87
  'Untagged' => 'Miscellaneous'
87
88
  }.freeze
88
89
 
@@ -363,7 +364,7 @@ class ServiceGenerator
363
364
  lineupmarker clientapproval clientapprovals clientcorrespondence
364
365
  clientcorrespondences clientreply clientreplies forwardreply
365
366
  forwardreplies campfireline campfirelines todolistgroup todolistgroups
366
- todolistorgroup uploadversions hillchart hillcharts subtask subtasks
367
+ todolistorgroup uploadversions hillchart hillcharts subtask subtasks ping pings
367
368
  wormhole wormholes
368
369
  ].freeze
369
370
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basecamp-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.25.0
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Basecamp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-15 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -218,6 +218,7 @@ files:
218
218
  - lib/basecamp/generated/services/my_notes_service.rb
219
219
  - lib/basecamp/generated/services/my_notifications_service.rb
220
220
  - lib/basecamp/generated/services/people_service.rb
221
+ - lib/basecamp/generated/services/pings_service.rb
221
222
  - lib/basecamp/generated/services/projects_service.rb
222
223
  - lib/basecamp/generated/services/recordings_service.rb
223
224
  - lib/basecamp/generated/services/reports_service.rb