diff --git a/behavior-model.json b/behavior-model.json index 0446eab0..01a727b1 100644 --- a/behavior-model.json +++ b/behavior-model.json @@ -15,17 +15,6 @@ ] } }, - "CloneTool": { - "retry": { - "max": 2, - "base_delay_ms": 1000, - "backoff": "exponential", - "retry_on": [ - 429, - 503 - ] - } - }, "CompleteTodo": { "idempotent": true, "retry": { @@ -324,6 +313,17 @@ ] } }, + "CreateTool": { + "retry": { + "max": 2, + "base_delay_ms": 1000, + "backoff": "exponential", + "retry_on": [ + 429, + 503 + ] + } + }, "CreateUpload": { "retry": { "max": 2, diff --git a/conformance/runner/go/main.go b/conformance/runner/go/main.go index 2b50e9ff..11935b3c 100644 --- a/conformance/runner/go/main.go +++ b/conformance/runner/go/main.go @@ -493,14 +493,15 @@ func executeOperation(ctx context.Context, account *basecamp.AccountClient, tc T _, err := account.Tools().Get(ctx, toolID) return operationResult{err: err} - case "CloneTool": - sourceRecordingID := getInt64Param(tc.RequestBody, "source_recording_id") + case "CreateTool": + bucketID := getInt64Param(tc.PathParams, "bucketId") + toolType := getStringParam(tc.RequestBody, "tool_type") title := getStringParam(tc.RequestBody, "title") - var opts *basecamp.CloneToolOptions + var opts *basecamp.CreateToolOptions if title != "" { - opts = &basecamp.CloneToolOptions{Title: title} + opts = &basecamp.CreateToolOptions{Title: title} } - _, err := account.Tools().Create(ctx, sourceRecordingID, opts) + _, err := account.Tools().Create(ctx, bucketID, toolType, opts) return operationResult{err: err} case "EnableTool": diff --git a/conformance/runner/python/runner.py b/conformance/runner/python/runner.py index fdf94cd9..939f6760 100644 --- a/conformance/runner/python/runner.py +++ b/conformance/runner/python/runner.py @@ -114,8 +114,12 @@ def __call__(self, operation: str, *, path_params: dict, query_params: dict, bod return self._account.reports.person_progress(person_id=path_params["personId"]) case "GetTool": return self._account.tools.get(tool_id=path_params["toolId"]) - case "CloneTool": - return self._account.tools.clone(source_recording_id=body["source_recording_id"], title=body["title"]) + case "CreateTool": + return self._account.tools.create( + bucket_id=path_params["bucketId"], + tool_type=body["tool_type"], + title=body.get("title"), + ) case "EnableTool": return self._account.tools.enable(tool_id=path_params["toolId"]) case "UploadsDownload": diff --git a/conformance/runner/ruby/runner.rb b/conformance/runner/ruby/runner.rb index 8983f8a6..da75af65 100644 --- a/conformance/runner/ruby/runner.rb +++ b/conformance/runner/ruby/runner.rb @@ -139,9 +139,10 @@ def call(operation, path_params: {}, query_params: {}, body: nil, path: "") @account.tools.get( tool_id: path_params["toolId"] ) - when "CloneTool" - @account.tools.clone( - source_recording_id: body["source_recording_id"], + when "CreateTool" + @account.tools.create( + bucket_id: path_params["bucketId"], + tool_type: body["tool_type"], title: body["title"] ) when "EnableTool" diff --git a/conformance/runner/typescript/runner.test.ts b/conformance/runner/typescript/runner.test.ts index 55a40d63..194d3b2a 100644 --- a/conformance/runner/typescript/runner.test.ts +++ b/conformance/runner/typescript/runner.test.ts @@ -209,10 +209,10 @@ async function executeOperation( await client.tools.get(Number(params.toolId)); break; - case "CloneTool": - await client.tools.clone({ - sourceRecordingId: Number(body.source_recording_id), - title: String(body.title || "Conformance Test"), + case "CreateTool": + await client.tools.create(Number(params.bucketId), { + toolType: String(body.tool_type), + title: body.title === undefined ? undefined : String(body.title), }); break; diff --git a/conformance/tests/paths.json b/conformance/tests/paths.json index 26edb9d2..7e4ba4a7 100644 --- a/conformance/tests/paths.json +++ b/conformance/tests/paths.json @@ -145,17 +145,35 @@ "tags": ["path", "tools"] }, { - "name": "CloneTool uses flat /dock/tools.json path (no bucketId)", - "description": "Verifies that CloneTool constructs the flat URL path /dock/tools.json without bucket scoping", - "operation": "CloneTool", + "name": "CreateTool uses bucket-scoped /buckets/{bucketId}/dock/tools.json path", + "description": "Verifies that CreateTool constructs the bucket-scoped URL path /buckets/{bucketId}/dock/tools.json", + "operation": "CreateTool", "method": "POST", - "path": "/dock/tools.json", - "requestBody": {"source_recording_id": 789, "title": "To-dos (Copy)"}, + "path": "/buckets/{bucketId}/dock/tools.json", + "pathParams": {"bucketId": 456}, + "requestBody": {"tool_type": "Message::Board", "title": "Message Board (Copy)"}, + "mockResponses": [ + {"status": 201, "body": {"id": 800, "name": "message_board", "title": "Message Board (Copy)", "status": "active", "enabled": true, "position": 5, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "url": "https://3.basecampapi.com/999/buckets/456/message_boards/800.json", "app_url": "https://3.basecamp.com/999/buckets/456/message_boards/800", "bucket": {"id": 456, "name": "Test", "type": "Project"}}} + ], + "assertions": [ + {"type": "requestPath", "expected": "/999/buckets/456/dock/tools.json"}, + {"type": "noError"} + ], + "tags": ["path", "tools"] + }, + { + "name": "CreateTool accepts omitted title on bucket-scoped path", + "description": "Verifies that CreateTool can omit the optional title while using /buckets/{bucketId}/dock/tools.json", + "operation": "CreateTool", + "method": "POST", + "path": "/buckets/{bucketId}/dock/tools.json", + "pathParams": {"bucketId": 456}, + "requestBody": {"tool_type": "Message::Board"}, "mockResponses": [ - {"status": 201, "body": {"id": 800, "name": "todoset", "title": "To-dos (Copy)", "status": "active", "enabled": true, "position": 5, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "url": "https://3.basecampapi.com/999/buckets/456/todosets/800.json", "app_url": "https://3.basecamp.com/999/buckets/456/todosets/800", "bucket": {"id": 456, "name": "Test", "type": "Project"}}} + {"status": 201, "body": {"id": 801, "name": "message_board", "title": "Message Board", "status": "active", "enabled": true, "position": 6, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "url": "https://3.basecampapi.com/999/buckets/456/message_boards/801.json", "app_url": "https://3.basecamp.com/999/buckets/456/message_boards/801", "bucket": {"id": 456, "name": "Test", "type": "Project"}}} ], "assertions": [ - {"type": "requestPath", "expected": "/999/dock/tools.json"}, + {"type": "requestPath", "expected": "/999/buckets/456/dock/tools.json"}, {"type": "noError"} ], "tags": ["path", "tools"] diff --git a/go/pkg/basecamp/api-provenance.json b/go/pkg/basecamp/api-provenance.json index 73008529..adb16d44 100644 --- a/go/pkg/basecamp/api-provenance.json +++ b/go/pkg/basecamp/api-provenance.json @@ -1,6 +1,6 @@ { "bc3": { - "revision": "056a356ee0d3b018362adbc8b44703df0567adbf", - "date": "2026-03-23" + "revision": "ae96a694deda4a0a5791be0a3b92acbc3509b61b", + "date": "2026-06-01" } } diff --git a/go/pkg/basecamp/tools.go b/go/pkg/basecamp/tools.go index 056fd5bc..9da87734 100644 --- a/go/pkg/basecamp/tools.go +++ b/go/pkg/basecamp/tools.go @@ -23,9 +23,9 @@ type Tool struct { Bucket *Bucket `json:"bucket,omitempty"` } -// CloneToolOptions specifies optional parameters for cloning a tool. -type CloneToolOptions struct { - // Title for the cloned tool. If empty, the source tool's title is used. +// CreateToolOptions specifies optional parameters for creating a tool. +type CreateToolOptions struct { + // Title for the new tool. If empty, Basecamp assigns the next available default title for the tool type. Title string } @@ -77,14 +77,14 @@ func (s *ToolsService) Get(ctx context.Context, toolID int64) (result *Tool, err return &tool, nil } -// Create clones an existing tool to create a new one. -// An optional title can be provided; if empty, the source tool's title is used. +// Create adds a tool to the destination bucket. +// An optional title can be provided; if empty, Basecamp assigns the next available default title for the tool type. // Returns the newly created tool. -func (s *ToolsService) Create(ctx context.Context, sourceToolID int64, opts *CloneToolOptions) (result *Tool, err error) { +func (s *ToolsService) Create(ctx context.Context, bucketID int64, toolType string, opts *CreateToolOptions) (result *Tool, err error) { op := OperationInfo{ Service: "Tools", Operation: "Create", ResourceType: "tool", IsMutation: true, - ResourceID: sourceToolID, + ResourceID: bucketID, } if gater, ok := s.client.parent.hooks.(GatingHooks); ok { if ctx, err = gater.OnOperationGate(ctx, op); err != nil { @@ -95,14 +95,14 @@ func (s *ToolsService) Create(ctx context.Context, sourceToolID int64, opts *Clo ctx = s.client.parent.hooks.OnOperationStart(ctx, op) defer func() { s.client.parent.hooks.OnOperationEnd(ctx, op, err, time.Since(start)) }() - body := generated.CloneToolJSONRequestBody{ - SourceRecordingId: sourceToolID, + body := generated.CreateToolJSONRequestBody{ + ToolType: toolType, } if opts != nil && opts.Title != "" { body.Title = opts.Title } - resp, err := s.client.parent.gen.CloneToolWithResponse(ctx, s.client.accountID, body) + resp, err := s.client.parent.gen.CreateToolWithResponse(ctx, s.client.accountID, bucketID, body) if err != nil { return nil, err } diff --git a/go/pkg/basecamp/tools_test.go b/go/pkg/basecamp/tools_test.go index 8495f0fe..13580f18 100644 --- a/go/pkg/basecamp/tools_test.go +++ b/go/pkg/basecamp/tools_test.go @@ -1,7 +1,11 @@ package basecamp import ( + "context" "encoding/json" + "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -77,6 +81,63 @@ func TestTool_UnmarshalGet(t *testing.T) { } } +func TestToolsServiceCreatePostsToBucketDock(t *testing.T) { + const ( + accountID = "5245563" + bucketID = int64(33861629) + toolType = "Message::Board" + title = "Intervention Log / Journal" + ) + + expectedPath := fmt.Sprintf("/%s/buckets/%d/dock/tools.json", accountID, bucketID) + + var capturedPath string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedPath = r.URL.Path + if r.Method != http.MethodPost || r.URL.Path != expectedPath { + http.NotFound(w, r) + return + } + + body := decodeRequestBody(t, r) + if got := body["tool_type"]; got != toolType { + t.Fatalf("tool_type = %v, want %q", got, toolType) + } + if got := body["title"]; got != title { + t.Fatalf("title = %v, want %q", got, title) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write(loadToolsFixture(t, "create.json")) + })) + defer server.Close() + + cfg := DefaultConfig() + cfg.BaseURL = server.URL + var capturedOp OperationInfo + hooks := &testHooks{ + onOperationStart: func(ctx context.Context, op OperationInfo) context.Context { + capturedOp = op + return ctx + }, + } + client := NewClient(cfg, &StaticTokenProvider{Token: "test-token"}, WithHooks(hooks)) + + _, err := client.ForAccount(accountID).Tools().Create( + context.Background(), + bucketID, + toolType, + &CreateToolOptions{Title: title}, + ) + if err != nil { + t.Fatalf("Create() error = %v; request path = %s; want bucket %d dock tools endpoint", err, capturedPath, bucketID) + } + if capturedOp.ResourceID != bucketID { + t.Fatalf("Create() operation ResourceID = %d, want destination bucket %d", capturedOp.ResourceID, bucketID) + } +} + func TestTool_UnmarshalCreate(t *testing.T) { data := loadToolsFixture(t, "create.json") diff --git a/go/pkg/basecamp/url-routes.json b/go/pkg/basecamp/url-routes.json index d5780ecf..728fbec8 100644 --- a/go/pkg/basecamp/url-routes.json +++ b/go/pkg/basecamp/url-routes.json @@ -117,6 +117,23 @@ } } }, + { + "pattern": "/{accountId}/buckets/{bucketId}/dock/tools", + "resource": "Automation", + "operations": { + "POST": "CreateTool" + }, + "params": { + "accountId": { + "role": "account", + "type": "string" + }, + "bucketId": { + "role": "parent", + "type": "int64" + } + } + }, { "pattern": "/{accountId}/buckets/{bucketId}/webhooks", "resource": "Automation", @@ -635,19 +652,6 @@ } } }, - { - "pattern": "/{accountId}/dock/tools", - "resource": "Automation", - "operations": { - "POST": "CloneTool" - }, - "params": { - "accountId": { - "role": "account", - "type": "string" - } - } - }, { "pattern": "/{accountId}/dock/tools/{toolId}", "resource": "Automation", diff --git a/go/pkg/basecamp/url_test.go b/go/pkg/basecamp/url_test.go index cd6ef109..0a1c2673 100644 --- a/go/pkg/basecamp/url_test.go +++ b/go/pkg/basecamp/url_test.go @@ -361,6 +361,24 @@ func TestMatchAPI(t *testing.T) { t.Errorf("Operation = %q, want ListRecordingBoosts", m.Operation) } + // CreateTool uses the bucket-scoped API route. + m = r.MatchAPI("https://3.basecampapi.com/123/buckets/456/dock/tools.json") + if m == nil { + t.Fatal("MatchAPI should match bucket-scoped CreateTool route") + } + if m.Operation != "CreateTool" { + t.Errorf("Operation = %q, want CreateTool", m.Operation) + } + if m.Params["bucketId"] != "456" { + t.Errorf("Params[bucketId] = %q, want 456", m.Params["bucketId"]) + } + + // The flat CreateTool route is not an API route. + m = r.MatchAPI("https://3.basecampapi.com/123/dock/tools.json") + if m != nil { + t.Errorf("MatchAPI should return nil for flat CreateTool route, got %+v", m) + } + // Web-only URL should NOT match m = r.MatchAPI("https://3.basecamp.com/123/buckets/456/sometype") if m != nil { diff --git a/go/pkg/basecamp/version.go b/go/pkg/basecamp/version.go index 829e8e25..e3bfdeab 100644 --- a/go/pkg/basecamp/version.go +++ b/go/pkg/basecamp/version.go @@ -4,4 +4,4 @@ package basecamp const Version = "0.7.3" // APIVersion is the Basecamp API version this SDK targets. -const APIVersion = "2026-03-23" +const APIVersion = "2026-06-01" diff --git a/go/pkg/generated/client.gen.go b/go/pkg/generated/client.gen.go index 9b5334e5..692f66ec 100644 --- a/go/pkg/generated/client.gen.go +++ b/go/pkg/generated/client.gen.go @@ -398,15 +398,6 @@ type ClientSide struct { Url string `json:"url,omitempty"` } -// CloneToolRequestContent defines model for CloneToolRequestContent. -type CloneToolRequestContent struct { - SourceRecordingId int64 `json:"source_recording_id"` - Title string `json:"title,omitempty"` -} - -// CloneToolResponseContent defines model for CloneToolResponseContent. -type CloneToolResponseContent = Tool - // Comment defines model for Comment. type Comment struct { AppUrl string `json:"app_url"` @@ -683,6 +674,18 @@ type CreateTodolistRequestContent struct { // CreateTodolistResponseContent defines model for CreateTodolistResponseContent. type CreateTodolistResponseContent = Todolist +// CreateToolRequestContent defines model for CreateToolRequestContent. +type CreateToolRequestContent struct { + // Title Title for the new tool. When omitted, Basecamp assigns the next available default title for the tool type. + Title string `json:"title,omitempty"` + + // ToolType Tool type to add to the project dock. Values: Chat::Transcript|Inbox|Kanban::Board|Message::Board|Questionnaire|Schedule|Todoset|Vault. + ToolType string `json:"tool_type"` +} + +// CreateToolResponseContent defines model for CreateToolResponseContent. +type CreateToolResponseContent = Tool + // CreateUploadRequestContent defines model for CreateUploadRequestContent. type CreateUploadRequestContent struct { AttachableSgid string `json:"attachable_sgid"` @@ -2732,6 +2735,9 @@ type UpdateAccountNameJSONRequestBody = UpdateAccountNameRequestContent // SetCardColumnColorJSONRequestBody defines body for SetCardColumnColor for application/json ContentType. type SetCardColumnColorJSONRequestBody = SetCardColumnColorRequestContent +// CreateToolJSONRequestBody defines body for CreateTool for application/json ContentType. +type CreateToolJSONRequestBody = CreateToolRequestContent + // CreateWebhookJSONRequestBody defines body for CreateWebhook for application/json ContentType. type CreateWebhookJSONRequestBody = CreateWebhookRequestContent @@ -2783,9 +2789,6 @@ type CreateCampfireLineJSONRequestBody = CreateCampfireLineRequestContent // UpdateCommentJSONRequestBody defines body for UpdateComment for application/json ContentType. type UpdateCommentJSONRequestBody = UpdateCommentRequestContent -// CloneToolJSONRequestBody defines body for CloneTool for application/json ContentType. -type CloneToolJSONRequestBody = CloneToolRequestContent - // UpdateToolJSONRequestBody defines body for UpdateTool for application/json ContentType. type UpdateToolJSONRequestBody = UpdateToolRequestContent @@ -3260,6 +3263,11 @@ type ClientInterface interface { // EnableCardColumnOnHold request EnableCardColumnOnHold(ctx context.Context, accountId string, bucketId int64, columnId int64, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateToolWithBody request with any body + CreateToolWithBody(ctx context.Context, accountId string, bucketId int64, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateTool(ctx context.Context, accountId string, bucketId int64, body CreateToolJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListWebhooks request ListWebhooks(ctx context.Context, accountId string, bucketId int64, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -3432,11 +3440,6 @@ type ClientInterface interface { UpdateComment(ctx context.Context, accountId string, commentId int64, body UpdateCommentJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // CloneToolWithBody request with any body - CloneToolWithBody(ctx context.Context, accountId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - CloneTool(ctx context.Context, accountId string, body CloneToolJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // DeleteTool request DeleteTool(ctx context.Context, accountId string, toolId int64, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -4104,6 +4107,36 @@ func (c *Client) EnableCardColumnOnHold(ctx context.Context, accountId string, b } +// CreateToolWithBody executes the CreateTool operation. + +func (c *Client) CreateToolWithBody(ctx context.Context, accountId string, bucketId int64, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + + req, err := NewCreateToolRequestWithBody(c.Server, accountId, bucketId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) + +} + +func (c *Client) CreateTool(ctx context.Context, accountId string, bucketId int64, body CreateToolJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + + req, err := NewCreateToolRequest(c.Server, accountId, bucketId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) + +} + // ListWebhooks is marked as idempotent and will be retried on transient failures. func (c *Client) ListWebhooks(ctx context.Context, accountId string, bucketId int64, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -4826,36 +4859,6 @@ func (c *Client) UpdateComment(ctx context.Context, accountId string, commentId } -// CloneToolWithBody executes the CloneTool operation. - -func (c *Client) CloneToolWithBody(ctx context.Context, accountId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - - req, err := NewCloneToolRequestWithBody(c.Server, accountId, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) - -} - -func (c *Client) CloneTool(ctx context.Context, accountId string, body CloneToolJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - - req, err := NewCloneToolRequest(c.Server, accountId, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) - -} - // DeleteTool is marked as idempotent and will be retried on transient failures. func (c *Client) DeleteTool(ctx context.Context, accountId string, toolId int64, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -7430,6 +7433,60 @@ func NewEnableCardColumnOnHoldRequest(server string, accountId string, bucketId return req, nil } +// NewCreateToolRequest calls the generic CreateTool builder with application/json body +func NewCreateToolRequest(server string, accountId string, bucketId int64, body CreateToolJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateToolRequestWithBody(server, accountId, bucketId, "application/json", bodyReader) +} + +// NewCreateToolRequestWithBody generates requests for CreateTool with any type of body +func NewCreateToolRequestWithBody(server string, accountId string, bucketId int64, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "accountId", runtime.ParamLocationPath, accountId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "bucketId", runtime.ParamLocationPath, bucketId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/%s/buckets/%s/dock/tools.json", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewListWebhooksRequest generates requests for ListWebhooks func NewListWebhooksRequest(server string, accountId string, bucketId int64) (*http.Request, error) { var err error @@ -9709,53 +9766,6 @@ func NewUpdateCommentRequestWithBody(server string, accountId string, commentId return req, nil } -// NewCloneToolRequest calls the generic CloneTool builder with application/json body -func NewCloneToolRequest(server string, accountId string, body CloneToolJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewCloneToolRequestWithBody(server, accountId, "application/json", bodyReader) -} - -// NewCloneToolRequestWithBody generates requests for CloneTool with any type of body -func NewCloneToolRequestWithBody(server string, accountId string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "accountId", runtime.ParamLocationPath, accountId) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/%s/dock/tools.json", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - // NewDeleteToolRequest generates requests for DeleteTool func NewDeleteToolRequest(server string, accountId string, toolId int64) (*http.Request, error) { var err error @@ -16894,6 +16904,7 @@ var operationMetadata = map[string]OperationMetadata{ "SetCardColumnColor": {Idempotent: true, HasSensitiveParams: false}, "DisableCardColumnOnHold": {Idempotent: true, HasSensitiveParams: false}, "EnableCardColumnOnHold": {Idempotent: false, HasSensitiveParams: false}, + "CreateTool": {Idempotent: false, HasSensitiveParams: false}, "ListWebhooks": {Idempotent: true, HasSensitiveParams: false}, "CreateWebhook": {Idempotent: false, HasSensitiveParams: false}, "GetCard": {Idempotent: true, HasSensitiveParams: false}, @@ -16940,7 +16951,6 @@ var operationMetadata = map[string]OperationMetadata{ "GetClientReply": {Idempotent: true, HasSensitiveParams: false}, "GetComment": {Idempotent: true, HasSensitiveParams: false}, "UpdateComment": {Idempotent: true, HasSensitiveParams: false}, - "CloneTool": {Idempotent: false, HasSensitiveParams: false}, "DeleteTool": {Idempotent: true, HasSensitiveParams: false}, "GetTool": {Idempotent: true, HasSensitiveParams: false}, "UpdateTool": {Idempotent: true, HasSensitiveParams: false}, @@ -18056,6 +18066,11 @@ type ClientWithResponsesInterface interface { // EnableCardColumnOnHoldWithResponse request EnableCardColumnOnHoldWithResponse(ctx context.Context, accountId string, bucketId int64, columnId int64, reqEditors ...RequestEditorFn) (*EnableCardColumnOnHoldResponse, error) + // CreateToolWithBodyWithResponse request with any body + CreateToolWithBodyWithResponse(ctx context.Context, accountId string, bucketId int64, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateToolResponse, error) + + CreateToolWithResponse(ctx context.Context, accountId string, bucketId int64, body CreateToolJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateToolResponse, error) + // ListWebhooksWithResponse request ListWebhooksWithResponse(ctx context.Context, accountId string, bucketId int64, reqEditors ...RequestEditorFn) (*ListWebhooksResponse, error) @@ -18228,11 +18243,6 @@ type ClientWithResponsesInterface interface { UpdateCommentWithResponse(ctx context.Context, accountId string, commentId int64, body UpdateCommentJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateCommentResponse, error) - // CloneToolWithBodyWithResponse request with any body - CloneToolWithBodyWithResponse(ctx context.Context, accountId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CloneToolResponse, error) - - CloneToolWithResponse(ctx context.Context, accountId string, body CloneToolJSONRequestBody, reqEditors ...RequestEditorFn) (*CloneToolResponse, error) - // DeleteToolWithResponse request DeleteToolWithResponse(ctx context.Context, accountId string, toolId int64, reqEditors ...RequestEditorFn) (*DeleteToolResponse, error) @@ -19035,6 +19045,33 @@ func (r EnableCardColumnOnHoldResponse) StatusCode() int { return 0 } +type CreateToolResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *CreateToolResponseContent + JSON401 *UnauthorizedErrorResponseContent + JSON403 *ForbiddenErrorResponseContent + JSON422 *ValidationErrorResponseContent + JSON429 *RateLimitErrorResponseContent + JSON500 *InternalServerErrorResponseContent +} + +// Status returns HTTPResponse.Status +func (r CreateToolResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateToolResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ListWebhooksResponse struct { Body []byte HTTPResponse *http.Response @@ -20243,33 +20280,6 @@ func (r UpdateCommentResponse) StatusCode() int { return 0 } -type CloneToolResponse struct { - Body []byte - HTTPResponse *http.Response - JSON201 *CloneToolResponseContent - JSON401 *UnauthorizedErrorResponseContent - JSON403 *ForbiddenErrorResponseContent - JSON422 *ValidationErrorResponseContent - JSON429 *RateLimitErrorResponseContent - JSON500 *InternalServerErrorResponseContent -} - -// Status returns HTTPResponse.Status -func (r CloneToolResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r CloneToolResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - type DeleteToolResponse struct { Body []byte HTTPResponse *http.Response @@ -24206,6 +24216,23 @@ func (c *ClientWithResponses) EnableCardColumnOnHoldWithResponse(ctx context.Con return ParseEnableCardColumnOnHoldResponse(rsp) } +// CreateToolWithBodyWithResponse request with arbitrary body returning *CreateToolResponse +func (c *ClientWithResponses) CreateToolWithBodyWithResponse(ctx context.Context, accountId string, bucketId int64, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateToolResponse, error) { + rsp, err := c.CreateToolWithBody(ctx, accountId, bucketId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateToolResponse(rsp) +} + +func (c *ClientWithResponses) CreateToolWithResponse(ctx context.Context, accountId string, bucketId int64, body CreateToolJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateToolResponse, error) { + rsp, err := c.CreateTool(ctx, accountId, bucketId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateToolResponse(rsp) +} + // ListWebhooksWithResponse request returning *ListWebhooksResponse func (c *ClientWithResponses) ListWebhooksWithResponse(ctx context.Context, accountId string, bucketId int64, reqEditors ...RequestEditorFn) (*ListWebhooksResponse, error) { rsp, err := c.ListWebhooks(ctx, accountId, bucketId, reqEditors...) @@ -24756,23 +24783,6 @@ func (c *ClientWithResponses) UpdateCommentWithResponse(ctx context.Context, acc return ParseUpdateCommentResponse(rsp) } -// CloneToolWithBodyWithResponse request with arbitrary body returning *CloneToolResponse -func (c *ClientWithResponses) CloneToolWithBodyWithResponse(ctx context.Context, accountId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CloneToolResponse, error) { - rsp, err := c.CloneToolWithBody(ctx, accountId, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCloneToolResponse(rsp) -} - -func (c *ClientWithResponses) CloneToolWithResponse(ctx context.Context, accountId string, body CloneToolJSONRequestBody, reqEditors ...RequestEditorFn) (*CloneToolResponse, error) { - rsp, err := c.CloneTool(ctx, accountId, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseCloneToolResponse(rsp) -} - // DeleteToolWithResponse request returning *DeleteToolResponse func (c *ClientWithResponses) DeleteToolWithResponse(ctx context.Context, accountId string, toolId int64, reqEditors ...RequestEditorFn) (*DeleteToolResponse, error) { rsp, err := c.DeleteTool(ctx, accountId, toolId, reqEditors...) @@ -27048,6 +27058,67 @@ func ParseEnableCardColumnOnHoldResponse(rsp *http.Response) (*EnableCardColumnO return response, nil } +// ParseCreateToolResponse parses an HTTP response from a CreateToolWithResponse call +func ParseCreateToolResponse(rsp *http.Response) (*CreateToolResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateToolResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest CreateToolResponseContent + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest UnauthorizedErrorResponseContent + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest ForbiddenErrorResponseContent + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest ValidationErrorResponseContent + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: + var dest RateLimitErrorResponseContent + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON429 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerErrorResponseContent + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseListWebhooksResponse parses an HTTP response from a ListWebhooksWithResponse call func ParseListWebhooksResponse(rsp *http.Response) (*ListWebhooksResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -29616,67 +29687,6 @@ func ParseUpdateCommentResponse(rsp *http.Response) (*UpdateCommentResponse, err return response, nil } -// ParseCloneToolResponse parses an HTTP response from a CloneToolWithResponse call -func ParseCloneToolResponse(rsp *http.Response) (*CloneToolResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &CloneToolResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest CloneToolResponseContent - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON201 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: - var dest UnauthorizedErrorResponseContent - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON401 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest ForbiddenErrorResponseContent - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest ValidationErrorResponseContent - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: - var dest RateLimitErrorResponseContent - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON429 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: - var dest InternalServerErrorResponseContent - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON500 = &dest - - } - - return response, nil -} - // ParseDeleteToolResponse parses an HTTP response from a DeleteToolWithResponse call func ParseDeleteToolResponse(rsp *http.Response) (*DeleteToolResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/kotlin/conformance/src/main/kotlin/com/basecamp/sdk/conformance/Main.kt b/kotlin/conformance/src/main/kotlin/com/basecamp/sdk/conformance/Main.kt index d4045041..1aa1ffa1 100644 --- a/kotlin/conformance/src/main/kotlin/com/basecamp/sdk/conformance/Main.kt +++ b/kotlin/conformance/src/main/kotlin/com/basecamp/sdk/conformance/Main.kt @@ -668,10 +668,11 @@ private suspend fun dispatchOperation(tc: TestCase, account: AccountClient): Dis DispatchResult() } - "CloneTool" -> { - val sourceRecordingId = tc.requestBody!!["source_recording_id"]!!.jsonPrimitive.long + "CreateTool" -> { + val bucketId = tc.pathParams!!["bucketId"]!!.jsonPrimitive.long + val toolType = tc.requestBody!!["tool_type"]!!.jsonPrimitive.content val title = tc.requestBody?.get("title")?.jsonPrimitive?.contentOrNull - account.tools.clone(CloneToolBody(sourceRecordingId = sourceRecordingId, title = title)) + account.tools.create(bucketId, CreateToolBody(toolType = toolType, title = title)) DispatchResult() } diff --git a/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt b/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt index 3dab957a..631d7e4c 100644 --- a/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt +++ b/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt @@ -53,7 +53,7 @@ val SERVICE_SPLITS: Map>> = mapOf( "Documents" to listOf("GetDocument", "UpdateDocument", "ListDocuments", "CreateDocument"), ), "Automation" to mapOf( - "Tools" to listOf("GetTool", "UpdateTool", "DeleteTool", "CloneTool", "EnableTool", "DisableTool", "RepositionTool"), + "Tools" to listOf("GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"), "Recordings" to listOf("GetRecording", "ArchiveRecording", "UnarchiveRecording", "TrashRecording", "ListRecordings"), "Webhooks" to listOf("ListWebhooks", "CreateWebhook", "GetWebhook", "UpdateWebhook", "DeleteWebhook"), "Events" to listOf("ListEvents"), diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt index 59e73d6d..d4f622e5 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt @@ -29,7 +29,7 @@ data class BasecampConfig( ) { companion object { const val VERSION = "0.7.3" - const val API_VERSION = "2026-03-23" + const val API_VERSION = "2026-06-01" const val DEFAULT_BASE_URL = "https://3.basecampapi.com" const val DEFAULT_USER_AGENT = "basecamp-sdk-kotlin/$VERSION (api:$API_VERSION)" const val DEFAULT_MAX_RETRIES = 3 diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/Metadata.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/Metadata.kt index b79d7fd0..da15fa36 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/Metadata.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/Metadata.kt @@ -21,7 +21,6 @@ object Metadata { val operations: Map = mapOf( "ArchiveRecording" to OperationConfig(true, RetryConfig(3, 1000L, "exponential", setOf(429, 503))), - "CloneTool" to OperationConfig(false, RetryConfig(2, 1000L, "exponential", setOf(429, 503))), "CompleteTodo" to OperationConfig(true, RetryConfig(3, 1000L, "exponential", setOf(429, 503))), "CreateAnswer" to OperationConfig(false, RetryConfig(2, 1000L, "exponential", setOf(429, 503))), "CreateAttachment" to OperationConfig(false, RetryConfig(3, 2000L, "exponential", setOf(429, 503))), @@ -49,6 +48,7 @@ object Metadata { "CreateTodo" to OperationConfig(false, RetryConfig(3, 1000L, "exponential", setOf(429, 503))), "CreateTodolist" to OperationConfig(false, RetryConfig(2, 1000L, "exponential", setOf(429, 503))), "CreateTodolistGroup" to OperationConfig(false, RetryConfig(2, 1000L, "exponential", setOf(429, 503))), + "CreateTool" to OperationConfig(false, RetryConfig(2, 1000L, "exponential", setOf(429, 503))), "CreateUpload" to OperationConfig(false, RetryConfig(2, 1000L, "exponential", setOf(429, 503))), "CreateVault" to OperationConfig(false, RetryConfig(2, 1000L, "exponential", setOf(429, 503))), "CreateWebhook" to OperationConfig(false, RetryConfig(2, 1000L, "exponential", setOf(429, 503))), diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/Types.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/Types.kt index c36f5c2f..883e8718 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/Types.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/Types.kt @@ -584,9 +584,9 @@ data class RepositionTodoBody( val parentId: Long? = null ) -/** Request body for CloneTool. */ -data class CloneToolBody( - val sourceRecordingId: Long, +/** Request body for CreateTool. */ +data class CreateToolBody( + val toolType: String, val title: String? = null ) diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/tools.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/tools.kt index e10fb937..4915b49a 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/tools.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/tools.kt @@ -13,21 +13,22 @@ import kotlinx.serialization.json.JsonElement class ToolsService(client: AccountClient) : BaseService(client) { /** - * Clone an existing tool to create a new one + * Create a tool in a project dock + * @param bucketId The bucket ID * @param body Request body */ - suspend fun clone(body: CloneToolBody): Tool { + suspend fun create(bucketId: Long, body: CreateToolBody): Tool { val info = OperationInfo( service = "Tools", - operation = "CloneTool", + operation = "CreateTool", resourceType = "tool", isMutation = true, projectId = null, - resourceId = null, + resourceId = bucketId, ) return request(info, { - httpPost("/dock/tools.json", json.encodeToString(kotlinx.serialization.json.buildJsonObject { - put("source_recording_id", kotlinx.serialization.json.JsonPrimitive(body.sourceRecordingId)) + httpPost("/buckets/${bucketId}/dock/tools.json", json.encodeToString(kotlinx.serialization.json.buildJsonObject { + put("tool_type", kotlinx.serialization.json.JsonPrimitive(body.toolType)) body.title?.let { put("title", kotlinx.serialization.json.JsonPrimitive(it)) } }), operationName = info.operation) }) { body -> diff --git a/openapi.json b/openapi.json index 0c6282e8..0c8494f7 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Basecamp", - "version": "2026-03-23", + "version": "2026-06-01", "description": "Basecamp API", "contact": { "name": "Basecamp", @@ -1027,6 +1027,118 @@ } } }, + "/{accountId}/buckets/{bucketId}/dock/tools.json": { + "post": { + "description": "Create a tool in a project dock", + "operationId": "CreateTool", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateToolRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "accountId", + "in": "path", + "description": "Basecamp account ID (numeric string)", + "schema": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Basecamp account ID (numeric string)" + }, + "required": true + }, + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateTool 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateToolResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "422": { + "description": "ValidationError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponseContent" + } + } + } + }, + "429": { + "description": "RateLimitError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Automation" + ], + "x-basecamp-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, "/{accountId}/buckets/{bucketId}/webhooks.json": { "get": { "description": "List all webhooks for a project\n\n**Pagination**: Uses Link header (RFC5988). Follow the `next` rel URL\nto fetch additional pages. X-Total-Count header provides total count.", @@ -5714,109 +5826,6 @@ } } }, - "/{accountId}/dock/tools.json": { - "post": { - "description": "Clone an existing tool to create a new one", - "operationId": "CloneTool", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CloneToolRequestContent" - } - } - }, - "required": true - }, - "parameters": [ - { - "name": "accountId", - "in": "path", - "description": "Basecamp account ID (numeric string)", - "schema": { - "type": "string", - "pattern": "^[0-9]+$", - "description": "Basecamp account ID (numeric string)" - }, - "required": true - } - ], - "responses": { - "201": { - "description": "CloneTool 201 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CloneToolResponseContent" - } - } - } - }, - "401": { - "description": "UnauthorizedError 401 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" - } - } - } - }, - "403": { - "description": "ForbiddenError 403 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenErrorResponseContent" - } - } - } - }, - "422": { - "description": "ValidationError 422 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationErrorResponseContent" - } - } - } - }, - "429": { - "description": "RateLimitError 429 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RateLimitErrorResponseContent" - } - } - } - }, - "500": { - "description": "InternalServerError 500 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InternalServerErrorResponseContent" - } - } - } - } - }, - "tags": [ - "Automation" - ], - "x-basecamp-retry": { - "maxAttempts": 2, - "baseDelayMs": 1000, - "backoff": "exponential", - "retryOn": [ - 429, - 503 - ] - } - } - }, "/{accountId}/dock/tools/{toolId}": { "delete": { "description": "Delete a tool (trash it)", @@ -22053,25 +22062,6 @@ } } }, - "CloneToolRequestContent": { - "type": "object", - "properties": { - "source_recording_id": { - "type": "integer", - "format": "int64", - "x-go-type-skip-optional-pointer": false - }, - "title": { - "type": "string" - } - }, - "required": [ - "source_recording_id" - ] - }, - "CloneToolResponseContent": { - "$ref": "#/components/schemas/Tool" - }, "Comment": { "type": "object", "properties": { @@ -22730,6 +22720,25 @@ "CreateTodolistResponseContent": { "$ref": "#/components/schemas/Todolist" }, + "CreateToolRequestContent": { + "type": "object", + "properties": { + "tool_type": { + "type": "string", + "description": "Tool type to add to the project dock. Values: Chat::Transcript|Inbox|Kanban::Board|Message::Board|Questionnaire|Schedule|Todoset|Vault." + }, + "title": { + "type": "string", + "description": "Title for the new tool. When omitted, Basecamp assigns the next available default title for the tool type." + } + }, + "required": [ + "tool_type" + ] + }, + "CreateToolResponseContent": { + "$ref": "#/components/schemas/Tool" + }, "CreateUploadRequestContent": { "type": "object", "properties": { diff --git a/python/scripts/generate_services.py b/python/scripts/generate_services.py index d0c032d6..97ee228e 100644 --- a/python/scripts/generate_services.py +++ b/python/scripts/generate_services.py @@ -65,7 +65,7 @@ "Documents": ["GetDocument", "UpdateDocument", "ListDocuments", "CreateDocument"], }, "Automation": { - "Tools": ["GetTool", "UpdateTool", "DeleteTool", "CloneTool", "EnableTool", "DisableTool", "RepositionTool"], + "Tools": ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], "Recordings": ["GetRecording", "ArchiveRecording", "UnarchiveRecording", "TrashRecording", "ListRecordings"], "Webhooks": ["ListWebhooks", "CreateWebhook", "GetWebhook", "UpdateWebhook", "DeleteWebhook"], "Events": ["ListEvents"], diff --git a/python/src/basecamp/_version.py b/python/src/basecamp/_version.py index b3668af7..657c1246 100644 --- a/python/src/basecamp/_version.py +++ b/python/src/basecamp/_version.py @@ -1,2 +1,2 @@ VERSION = "0.7.3" -API_VERSION = "2026-03-23" +API_VERSION = "2026-06-01" diff --git a/python/src/basecamp/generated/metadata.json b/python/src/basecamp/generated/metadata.json index 2c28ac63..b4df133f 100644 --- a/python/src/basecamp/generated/metadata.json +++ b/python/src/basecamp/generated/metadata.json @@ -11,17 +11,6 @@ ] } }, - "CloneTool": { - "retry": { - "backoff": "exponential", - "base_delay_ms": 1000, - "max": 2, - "retry_on": [ - 429, - 503 - ] - } - }, "CompleteTodo": { "idempotent": true, "retry": { @@ -320,6 +309,17 @@ ] } }, + "CreateTool": { + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, "CreateUpload": { "retry": { "backoff": "exponential", diff --git a/python/src/basecamp/generated/services/tools.py b/python/src/basecamp/generated/services/tools.py index 89d33a3e..04377630 100644 --- a/python/src/basecamp/generated/services/tools.py +++ b/python/src/basecamp/generated/services/tools.py @@ -11,13 +11,13 @@ class ToolsService(BaseService): - def clone(self, *, source_recording_id: int, title: str | None = None) -> dict[str, Any]: + def create(self, *, bucket_id: int, tool_type: str, title: str | None = None) -> dict[str, Any]: return self._request( - OperationInfo(service="tools", operation="clone", is_mutation=True), + OperationInfo(service="tools", operation="create", is_mutation=True, resource_id=bucket_id), "POST", - "/dock/tools.json", - json_body=self._compact(source_recording_id=source_recording_id, title=title), - operation="CloneTool", + f"/buckets/{bucket_id}/dock/tools.json", + json_body=self._compact(tool_type=tool_type, title=title), + operation="CreateTool", ) def get(self, *, tool_id: int) -> dict[str, Any]: @@ -71,13 +71,13 @@ def disable(self, *, tool_id: int) -> None: class AsyncToolsService(AsyncBaseService): - async def clone(self, *, source_recording_id: int, title: str | None = None) -> dict[str, Any]: + async def create(self, *, bucket_id: int, tool_type: str, title: str | None = None) -> dict[str, Any]: return await self._request( - OperationInfo(service="tools", operation="clone", is_mutation=True), + OperationInfo(service="tools", operation="create", is_mutation=True, resource_id=bucket_id), "POST", - "/dock/tools.json", - json_body=self._compact(source_recording_id=source_recording_id, title=title), - operation="CloneTool", + f"/buckets/{bucket_id}/dock/tools.json", + json_body=self._compact(tool_type=tool_type, title=title), + operation="CreateTool", ) async def get(self, *, tool_id: int) -> dict[str, Any]: diff --git a/python/src/basecamp/generated/types.py b/python/src/basecamp/generated/types.py index b2946e5f..e86d76fb 100644 --- a/python/src/basecamp/generated/types.py +++ b/python/src/basecamp/generated/types.py @@ -346,11 +346,6 @@ class ClientSide(TypedDict): url: NotRequired[str] -class CloneToolRequestContent(TypedDict): - source_recording_id: int - title: NotRequired[str] - - class Comment(TypedDict): app_url: str bookmark_url: NotRequired[str] @@ -514,6 +509,11 @@ class CreateTodolistRequestContent(TypedDict): name: str +class CreateToolRequestContent(TypedDict): + title: NotRequired[str] + tool_type: str + + class CreateUploadRequestContent(TypedDict): attachable_sgid: str base_name: NotRequired[str] diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index 00d12b94..6de47f4b 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -221,4 +221,4 @@ CHECKSUMS zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 BUNDLED WITH - 4.0.4 + 4.0.12 diff --git a/ruby/lib/basecamp/generated/metadata.json b/ruby/lib/basecamp/generated/metadata.json index d5cd3625..38808c2a 100644 --- a/ruby/lib/basecamp/generated/metadata.json +++ b/ruby/lib/basecamp/generated/metadata.json @@ -1,7 +1,7 @@ { "$schema": "https://basecamp.com/schemas/sdk-metadata.json", "version": "1.0.0", - "generated": "2026-04-29T18:03:49Z", + "generated": "2026-06-01T20:21:47Z", "operations": { "GetAccount": { "retry": { @@ -131,6 +131,17 @@ "natural": true } }, + "CreateTool": { + "retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, "ListWebhooks": { "retry": { "maxAttempts": 3, @@ -728,17 +739,6 @@ "natural": true } }, - "CloneTool": { - "retry": { - "maxAttempts": 2, - "baseDelayMs": 1000, - "backoff": "exponential", - "retryOn": [ - 429, - 503 - ] - } - }, "GetTool": { "retry": { "maxAttempts": 3, diff --git a/ruby/lib/basecamp/generated/services/tools_service.rb b/ruby/lib/basecamp/generated/services/tools_service.rb index 78ea9bc7..844f81e9 100644 --- a/ruby/lib/basecamp/generated/services/tools_service.rb +++ b/ruby/lib/basecamp/generated/services/tools_service.rb @@ -7,13 +7,14 @@ module Services # @generated from OpenAPI spec class ToolsService < BaseService - # Clone an existing tool to create a new one - # @param source_recording_id [Integer] source recording id - # @param title [String, nil] title + # Create a tool in a project dock + # @param bucket_id [Integer] bucket id ID + # @param tool_type [String] Tool type to add to the project dock. Values: Chat::Transcript|Inbox|Kanban::Board|Message::Board|Questionnaire|Schedule|Todoset|Vault. + # @param title [String, nil] Title for the new tool. When omitted, Basecamp assigns the next available default title for the tool type. # @return [Hash] response data - def clone(source_recording_id:, title: nil) - with_operation(service: "tools", operation: "clone", is_mutation: true) do - http_post("/dock/tools.json", body: compact_params(source_recording_id: source_recording_id, title: title)).json + def create(bucket_id:, tool_type:, title: nil) + with_operation(service: "tools", operation: "create", is_mutation: true, resource_id: bucket_id) do + http_post("/buckets/#{bucket_id}/dock/tools.json", body: compact_params(tool_type: tool_type, title: title)).json end end diff --git a/ruby/lib/basecamp/generated/types.rb b/ruby/lib/basecamp/generated/types.rb index 8cc6ed85..9cc54d5f 100644 --- a/ruby/lib/basecamp/generated/types.rb +++ b/ruby/lib/basecamp/generated/types.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Auto-generated from OpenAPI spec. Do not edit manually. -# Generated: 2026-04-29T18:03:49Z +# Generated: 2026-06-01T20:21:47Z require "json" require "time" diff --git a/ruby/lib/basecamp/version.rb b/ruby/lib/basecamp/version.rb index 7cd56455..75f7cfed 100644 --- a/ruby/lib/basecamp/version.rb +++ b/ruby/lib/basecamp/version.rb @@ -2,5 +2,5 @@ module Basecamp VERSION = "0.7.3" - API_VERSION = "2026-03-23" + API_VERSION = "2026-06-01" end diff --git a/ruby/scripts/generate-services.rb b/ruby/scripts/generate-services.rb index deb8fa49..a9f90d7b 100644 --- a/ruby/scripts/generate-services.rb +++ b/ruby/scripts/generate-services.rb @@ -69,7 +69,7 @@ class ServiceGenerator 'Documents' => %w[GetDocument UpdateDocument ListDocuments CreateDocument] }, 'Automation' => { - 'Tools' => %w[GetTool UpdateTool DeleteTool CloneTool EnableTool DisableTool RepositionTool], + 'Tools' => %w[GetTool UpdateTool DeleteTool CreateTool EnableTool DisableTool RepositionTool], 'Recordings' => %w[GetRecording ArchiveRecording UnarchiveRecording TrashRecording ListRecordings], 'Webhooks' => %w[ListWebhooks CreateWebhook GetWebhook UpdateWebhook DeleteWebhook], 'Events' => %w[ListEvents], diff --git a/ruby/test/basecamp/services/tools_service_test.rb b/ruby/test/basecamp/services/tools_service_test.rb index 649e5a3d..c5f11829 100644 --- a/ruby/test/basecamp/services/tools_service_test.rb +++ b/ruby/test/basecamp/services/tools_service_test.rb @@ -20,14 +20,14 @@ def test_get assert_equal true, result["enabled"] end - def test_clone + def test_create response = { "id" => 2, "name" => "Message Board (Copy)" } - stub_request(:post, %r{https://3\.basecampapi\.com/12345/dock/tools\.json}) - .with(body: hash_including("source_recording_id" => 2, "title" => "Message Board (Copy)")) + stub_request(:post, %r{https://3\.basecampapi\.com/12345/buckets/456/dock/tools\.json}) + .with(body: hash_including("tool_type" => "Message::Board", "title" => "Message Board (Copy)")) .to_return(status: 201, body: response.to_json, headers: { "Content-Type" => "application/json" }) - result = @account.tools.clone(source_recording_id: 2, title: "Message Board (Copy)") + result = @account.tools.create(bucket_id: 456, tool_type: "Message::Board", title: "Message Board (Copy)") assert_equal "Message Board (Copy)", result["name"] end diff --git a/spec/api-provenance.json b/spec/api-provenance.json index 73008529..adb16d44 100644 --- a/spec/api-provenance.json +++ b/spec/api-provenance.json @@ -1,6 +1,6 @@ { "bc3": { - "revision": "056a356ee0d3b018362adbc8b44703df0567adbf", - "date": "2026-03-23" + "revision": "ae96a694deda4a0a5791be0a3b92acbc3509b61b", + "date": "2026-06-01" } } diff --git a/spec/basecamp.smithy b/spec/basecamp.smithy index f1db530b..4e40f97c 100644 --- a/spec/basecamp.smithy +++ b/spec/basecamp.smithy @@ -50,7 +50,7 @@ use basecamp.traits#basecampAuthRoutableUrl /// Basecamp API @restJson1 service Basecamp { - version: "2026-03-23" + version: "2026-06-01" rename: { "smithy.api#Document": "JsonDocument" } @@ -235,7 +235,7 @@ service Basecamp { CreateProjectFromTemplate, GetProjectConstruction, GetTool, - CloneTool, + CreateTool, UpdateTool, DeleteTool, EnableTool, @@ -6792,27 +6792,33 @@ structure GetToolOutput { tool: Tool } -/// Clone an existing tool to create a new one +/// Create a tool in a project dock @basecampRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) -@http(method: "POST", uri: "/{accountId}/dock/tools.json", code: 201) -operation CloneTool { - input: CloneToolInput - output: CloneToolOutput +@http(method: "POST", uri: "/{accountId}/buckets/{bucketId}/dock/tools.json", code: 201) +operation CreateTool { + input: CreateToolInput + output: CreateToolOutput errors: [ValidationError, UnauthorizedError, ForbiddenError, RateLimitError, InternalServerError] } -structure CloneToolInput { +structure CreateToolInput { @required @httpLabel accountId: AccountId @required - source_recording_id: ToolId + @httpLabel + bucketId: ProjectId + + /// Tool type to add to the project dock. Values: Chat::Transcript|Inbox|Kanban::Board|Message::Board|Questionnaire|Schedule|Todoset|Vault. + @required + tool_type: String + /// Title for the new tool. When omitted, Basecamp assigns the next available default title for the tool type. title: String } -structure CloneToolOutput { +structure CreateToolOutput { tool: Tool } diff --git a/spec/overlays/tags.smithy b/spec/overlays/tags.smithy index 97d60f6b..1c4aa25a 100644 --- a/spec/overlays/tags.smithy +++ b/spec/overlays/tags.smithy @@ -164,7 +164,7 @@ apply DeleteTemplate @tags(["Automation"]) apply CreateProjectFromTemplate @tags(["Automation"]) apply GetProjectConstruction @tags(["Automation"]) apply GetTool @tags(["Automation"]) -apply CloneTool @tags(["Automation"]) +apply CreateTool @tags(["Automation"]) apply UpdateTool @tags(["Automation"]) apply DeleteTool @tags(["Automation"]) apply EnableTool @tags(["Automation"]) diff --git a/swift/Sources/Basecamp/BasecampConfig.swift b/swift/Sources/Basecamp/BasecampConfig.swift index d4cba884..3488f2e3 100644 --- a/swift/Sources/Basecamp/BasecampConfig.swift +++ b/swift/Sources/Basecamp/BasecampConfig.swift @@ -27,7 +27,7 @@ public struct BasecampConfig: Sendable { public static let version = "0.7.3" /// Basecamp API version this SDK targets. - public static let apiVersion = "2026-03-23" + public static let apiVersion = "2026-06-01" /// Default User-Agent header value. public static let defaultUserAgent = "basecamp-sdk-swift/\(version) (api:\(apiVersion))" diff --git a/swift/Sources/Basecamp/Generated/Metadata.swift b/swift/Sources/Basecamp/Generated/Metadata.swift index d6511d56..cc48853e 100644 --- a/swift/Sources/Basecamp/Generated/Metadata.swift +++ b/swift/Sources/Basecamp/Generated/Metadata.swift @@ -4,7 +4,6 @@ import Foundation enum Metadata { private static let configs: [String: RetryConfig] = [ "ArchiveRecording": RetryConfig(maxAttempts: 3, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), - "CloneTool": RetryConfig(maxAttempts: 2, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), "CompleteTodo": RetryConfig(maxAttempts: 3, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), "CreateAnswer": RetryConfig(maxAttempts: 2, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), "CreateAttachment": RetryConfig(maxAttempts: 3, baseDelayMs: 2000, backoff: .exponential, retryOn: [429, 503]), @@ -32,6 +31,7 @@ enum Metadata { "CreateTodo": RetryConfig(maxAttempts: 3, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), "CreateTodolist": RetryConfig(maxAttempts: 2, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), "CreateTodolistGroup": RetryConfig(maxAttempts: 2, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), + "CreateTool": RetryConfig(maxAttempts: 2, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), "CreateUpload": RetryConfig(maxAttempts: 2, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), "CreateVault": RetryConfig(maxAttempts: 2, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), "CreateWebhook": RetryConfig(maxAttempts: 2, baseDelayMs: 1000, backoff: .exponential, retryOn: [429, 503]), diff --git a/swift/Sources/Basecamp/Generated/Models/CloneToolRequest.swift b/swift/Sources/Basecamp/Generated/Models/CloneToolRequest.swift deleted file mode 100644 index 75c654e0..00000000 --- a/swift/Sources/Basecamp/Generated/Models/CloneToolRequest.swift +++ /dev/null @@ -1,12 +0,0 @@ -// @generated from OpenAPI spec — do not edit directly -import Foundation - -public struct CloneToolRequest: Codable, Sendable { - public let sourceRecordingId: Int - public var title: String? - - public init(sourceRecordingId: Int, title: String? = nil) { - self.sourceRecordingId = sourceRecordingId - self.title = title - } -} diff --git a/swift/Sources/Basecamp/Generated/Models/CreateToolRequest.swift b/swift/Sources/Basecamp/Generated/Models/CreateToolRequest.swift new file mode 100644 index 00000000..9af46aba --- /dev/null +++ b/swift/Sources/Basecamp/Generated/Models/CreateToolRequest.swift @@ -0,0 +1,12 @@ +// @generated from OpenAPI spec — do not edit directly +import Foundation + +public struct CreateToolRequest: Codable, Sendable { + public var title: String? + public let toolType: String + + public init(title: String? = nil, toolType: String) { + self.title = title + self.toolType = toolType + } +} diff --git a/swift/Sources/Basecamp/Generated/Services/ToolsService.swift b/swift/Sources/Basecamp/Generated/Services/ToolsService.swift index 9873af3f..dc487946 100644 --- a/swift/Sources/Basecamp/Generated/Services/ToolsService.swift +++ b/swift/Sources/Basecamp/Generated/Services/ToolsService.swift @@ -2,13 +2,13 @@ import Foundation public final class ToolsService: BaseService, @unchecked Sendable { - public func clone(req: CloneToolRequest) async throws -> Tool { + public func create(bucketId: Int, req: CreateToolRequest) async throws -> Tool { return try await request( - OperationInfo(service: "Tools", operation: "CloneTool", resourceType: "tool", isMutation: true), + OperationInfo(service: "Tools", operation: "CreateTool", resourceType: "tool", isMutation: true, resourceId: bucketId), method: "POST", - path: "/dock/tools.json", + path: "/buckets/\(bucketId)/dock/tools.json", body: req, - retryConfig: Metadata.retryConfig(for: "CloneTool") + retryConfig: Metadata.retryConfig(for: "CreateTool") ) } diff --git a/swift/Sources/BasecampGenerator/ServiceGrouper.swift b/swift/Sources/BasecampGenerator/ServiceGrouper.swift index 0af6f60e..3d60e7fa 100644 --- a/swift/Sources/BasecampGenerator/ServiceGrouper.swift +++ b/swift/Sources/BasecampGenerator/ServiceGrouper.swift @@ -52,7 +52,7 @@ let serviceSplits: [String: [String: [String]]] = [ "Documents": ["GetDocument", "UpdateDocument", "ListDocuments", "CreateDocument"], ], "Automation": [ - "Tools": ["GetTool", "UpdateTool", "DeleteTool", "CloneTool", "EnableTool", "DisableTool", "RepositionTool"], + "Tools": ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], "Recordings": ["GetRecording", "ArchiveRecording", "UnarchiveRecording", "TrashRecording", "ListRecordings"], "Webhooks": ["ListWebhooks", "CreateWebhook", "GetWebhook", "UpdateWebhook", "DeleteWebhook"], "Events": ["ListEvents"], diff --git a/typescript/scripts/generate-services.ts b/typescript/scripts/generate-services.ts index 6c1ab45a..b38c64eb 100644 --- a/typescript/scripts/generate-services.ts +++ b/typescript/scripts/generate-services.ts @@ -200,7 +200,7 @@ const SERVICE_SPLITS: Record> = { Documents: ["GetDocument", "UpdateDocument", "ListDocuments", "CreateDocument"], }, Automation: { - Tools: ["GetTool", "UpdateTool", "DeleteTool", "CloneTool", "EnableTool", "DisableTool", "RepositionTool"], + Tools: ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], Recordings: ["GetRecording", "ArchiveRecording", "UnarchiveRecording", "TrashRecording", "ListRecordings"], Webhooks: ["ListWebhooks", "CreateWebhook", "GetWebhook", "UpdateWebhook", "DeleteWebhook"], Events: ["ListEvents"], diff --git a/typescript/src/client.ts b/typescript/src/client.ts index 6e53a650..eaa61379 100644 --- a/typescript/src/client.ts +++ b/typescript/src/client.ts @@ -224,7 +224,7 @@ export interface BasecampClientOptions { } export const VERSION = "0.7.3"; -export const API_VERSION = "2026-03-23"; +export const API_VERSION = "2026-06-01"; const DEFAULT_USER_AGENT = `basecamp-sdk-ts/${VERSION} (api:${API_VERSION})`; /** diff --git a/typescript/src/generated/metadata.json b/typescript/src/generated/metadata.json index 940586b0..b24e5bc1 100644 --- a/typescript/src/generated/metadata.json +++ b/typescript/src/generated/metadata.json @@ -1,7 +1,7 @@ { "$schema": "https://basecamp.com/schemas/sdk-metadata.json", "version": "1.0.0", - "generated": "2026-04-29T18:03:48.821Z", + "generated": "2026-06-01T20:21:46.965Z", "operations": { "GetAccount": { "retry": { @@ -131,6 +131,17 @@ "natural": true } }, + "CreateTool": { + "retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, "ListWebhooks": { "retry": { "maxAttempts": 3, @@ -728,17 +739,6 @@ "natural": true } }, - "CloneTool": { - "retry": { - "maxAttempts": 2, - "baseDelayMs": 1000, - "backoff": "exponential", - "retryOn": [ - 429, - 503 - ] - } - }, "GetTool": { "retry": { "maxAttempts": 3, diff --git a/typescript/src/generated/openapi-stripped.json b/typescript/src/generated/openapi-stripped.json index 496f978d..051c58ed 100644 --- a/typescript/src/generated/openapi-stripped.json +++ b/typescript/src/generated/openapi-stripped.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Basecamp", - "version": "2026-03-23", + "version": "2026-06-01", "description": "Basecamp API", "contact": { "name": "Basecamp", @@ -913,6 +913,107 @@ } } }, + "/buckets/{bucketId}/dock/tools.json": { + "post": { + "description": "Create a tool in a project dock", + "operationId": "CreateTool", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateToolRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateTool 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateToolResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "422": { + "description": "ValidationError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponseContent" + } + } + } + }, + "429": { + "description": "RateLimitError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Automation" + ], + "x-basecamp-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, "/buckets/{bucketId}/webhooks.json": { "get": { "description": "List all webhooks for a project\n\n**Pagination**: Uses Link header (RFC5988). Follow the `next` rel URL\nto fetch additional pages. X-Total-Count header provides total count.", @@ -5090,97 +5191,6 @@ } } }, - "/dock/tools.json": { - "post": { - "description": "Clone an existing tool to create a new one", - "operationId": "CloneTool", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CloneToolRequestContent" - } - } - }, - "required": true - }, - "parameters": [], - "responses": { - "201": { - "description": "CloneTool 201 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CloneToolResponseContent" - } - } - } - }, - "401": { - "description": "UnauthorizedError 401 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" - } - } - } - }, - "403": { - "description": "ForbiddenError 403 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenErrorResponseContent" - } - } - } - }, - "422": { - "description": "ValidationError 422 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationErrorResponseContent" - } - } - } - }, - "429": { - "description": "RateLimitError 429 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RateLimitErrorResponseContent" - } - } - } - }, - "500": { - "description": "InternalServerError 500 response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InternalServerErrorResponseContent" - } - } - } - } - }, - "tags": [ - "Automation" - ], - "x-basecamp-retry": { - "maxAttempts": 2, - "baseDelayMs": 1000, - "backoff": "exponential", - "retryOn": [ - 429, - 503 - ] - } - } - }, "/dock/tools/{toolId}": { "delete": { "description": "Delete a tool (trash it)", @@ -19729,25 +19739,6 @@ } } }, - "CloneToolRequestContent": { - "type": "object", - "properties": { - "source_recording_id": { - "type": "integer", - "format": "int64", - "x-go-type-skip-optional-pointer": false - }, - "title": { - "type": "string" - } - }, - "required": [ - "source_recording_id" - ] - }, - "CloneToolResponseContent": { - "$ref": "#/components/schemas/Tool" - }, "Comment": { "type": "object", "properties": { @@ -20406,6 +20397,25 @@ "CreateTodolistResponseContent": { "$ref": "#/components/schemas/Todolist" }, + "CreateToolRequestContent": { + "type": "object", + "properties": { + "tool_type": { + "type": "string", + "description": "Tool type to add to the project dock. Values: Chat::Transcript|Inbox|Kanban::Board|Message::Board|Questionnaire|Schedule|Todoset|Vault." + }, + "title": { + "type": "string", + "description": "Title for the new tool. When omitted, Basecamp assigns the next available default title for the tool type." + } + }, + "required": [ + "tool_type" + ] + }, + "CreateToolResponseContent": { + "$ref": "#/components/schemas/Tool" + }, "CreateUploadRequestContent": { "type": "object", "properties": { diff --git a/typescript/src/generated/path-mapping.ts b/typescript/src/generated/path-mapping.ts index 1720967e..7a7c21f5 100644 --- a/typescript/src/generated/path-mapping.ts +++ b/typescript/src/generated/path-mapping.ts @@ -16,6 +16,7 @@ export const PATH_TO_OPERATION: Record = { "PUT:/{accountId}/buckets/{bucketId}/card_tables/columns/{columnId}/color.json": "SetCardColumnColor", "DELETE:/{accountId}/buckets/{bucketId}/card_tables/columns/{columnId}/on_hold.json": "DisableCardColumnOnHold", "POST:/{accountId}/buckets/{bucketId}/card_tables/columns/{columnId}/on_hold.json": "EnableCardColumnOnHold", + "POST:/{accountId}/buckets/{bucketId}/dock/tools.json": "CreateTool", "GET:/{accountId}/buckets/{bucketId}/webhooks.json": "ListWebhooks", "POST:/{accountId}/buckets/{bucketId}/webhooks.json": "CreateWebhook", "GET:/{accountId}/card_tables/{cardTableId}": "GetCardTable", @@ -61,7 +62,6 @@ export const PATH_TO_OPERATION: Record = { "GET:/{accountId}/client/recordings/{recordingId}/replies/{replyId}": "GetClientReply", "GET:/{accountId}/comments/{commentId}": "GetComment", "PUT:/{accountId}/comments/{commentId}": "UpdateComment", - "POST:/{accountId}/dock/tools.json": "CloneTool", "DELETE:/{accountId}/dock/tools/{toolId}": "DeleteTool", "GET:/{accountId}/dock/tools/{toolId}": "GetTool", "PUT:/{accountId}/dock/tools/{toolId}": "UpdateTool", diff --git a/typescript/src/generated/schema.d.ts b/typescript/src/generated/schema.d.ts index ad185afd..70078b61 100644 --- a/typescript/src/generated/schema.d.ts +++ b/typescript/src/generated/schema.d.ts @@ -130,6 +130,23 @@ export interface paths { patch?: never; trace?: never; }; + "/buckets/{bucketId}/dock/tools.json": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Create a tool in a project dock */ + post: operations["CreateTool"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/buckets/{bucketId}/webhooks.json": { parameters: { query?: never; @@ -711,23 +728,6 @@ export interface paths { patch?: never; trace?: never; }; - "/dock/tools.json": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** @description Clone an existing tool to create a new one */ - post: operations["CloneTool"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/dock/tools/{toolId}": { parameters: { query?: never; @@ -2866,12 +2866,6 @@ export interface components { url?: string; app_url?: string; }; - CloneToolRequestContent: { - /** Format: int64 */ - source_recording_id: number; - title?: string; - }; - CloneToolResponseContent: components["schemas"]["Tool"]; Comment: { /** Format: int64 */ id: number; @@ -3051,6 +3045,13 @@ export interface components { description?: string; }; CreateTodolistResponseContent: components["schemas"]["Todolist"]; + CreateToolRequestContent: { + /** @description Tool type to add to the project dock. Values: Chat::Transcript|Inbox|Kanban::Board|Message::Board|Questionnaire|Schedule|Todoset|Vault. */ + tool_type: string; + /** @description Title for the new tool. When omitted, Basecamp assigns the next available default title for the tool type. */ + title?: string; + }; + CreateToolResponseContent: components["schemas"]["Tool"]; CreateUploadRequestContent: { attachable_sgid: string; description?: string; @@ -5153,6 +5154,77 @@ export interface operations { }; }; }; + CreateTool: { + parameters: { + query?: never; + header?: never; + path: { + bucketId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateToolRequestContent"]; + }; + }; + responses: { + /** @description CreateTool 201 response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateToolResponseContent"]; + }; + }; + /** @description UnauthorizedError 401 response */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UnauthorizedErrorResponseContent"]; + }; + }; + /** @description ForbiddenError 403 response */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ForbiddenErrorResponseContent"]; + }; + }; + /** @description ValidationError 422 response */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ValidationErrorResponseContent"]; + }; + }; + /** @description RateLimitError 429 response */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RateLimitErrorResponseContent"]; + }; + }; + /** @description InternalServerError 500 response */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InternalServerErrorResponseContent"]; + }; + }; + }; + }; ListWebhooks: { parameters: { query?: never; @@ -8074,75 +8146,6 @@ export interface operations { }; }; }; - CloneTool: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CloneToolRequestContent"]; - }; - }; - responses: { - /** @description CloneTool 201 response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CloneToolResponseContent"]; - }; - }; - /** @description UnauthorizedError 401 response */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UnauthorizedErrorResponseContent"]; - }; - }; - /** @description ForbiddenError 403 response */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ForbiddenErrorResponseContent"]; - }; - }; - /** @description ValidationError 422 response */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ValidationErrorResponseContent"]; - }; - }; - /** @description RateLimitError 429 response */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["RateLimitErrorResponseContent"]; - }; - }; - /** @description InternalServerError 500 response */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["InternalServerErrorResponseContent"]; - }; - }; - }; - }; GetTool: { parameters: { query?: never; diff --git a/typescript/src/generated/services/index.ts b/typescript/src/generated/services/index.ts index 65eca51c..b96f69d2 100644 --- a/typescript/src/generated/services/index.ts +++ b/typescript/src/generated/services/index.ts @@ -2,6 +2,7 @@ export { AccountService } from "./account.js"; export { AttachmentsService } from "./attachments.js"; export { BoostsService } from "./boosts.js"; export { CardColumnsService } from "./card-columns.js"; +export { ToolsService } from "./tools.js"; export { WebhooksService } from "./webhooks.js"; export { CardsService } from "./cards.js"; export { CardStepsService } from "./card-steps.js"; @@ -13,7 +14,6 @@ export { ClientApprovalsService } from "./client-approvals.js"; export { ClientCorrespondencesService } from "./client-correspondences.js"; export { ClientRepliesService } from "./client-replies.js"; export { CommentsService } from "./comments.js"; -export { ToolsService } from "./tools.js"; export { DocumentsService } from "./documents.js"; export { GaugesService } from "./gauges.js"; export { ForwardsService } from "./forwards.js"; diff --git a/typescript/src/generated/services/tools.ts b/typescript/src/generated/services/tools.ts index 2577f653..99431dc8 100644 --- a/typescript/src/generated/services/tools.ts +++ b/typescript/src/generated/services/tools.ts @@ -16,12 +16,12 @@ import { Errors } from "../../errors.js"; export type Tool = components["schemas"]["Tool"]; /** - * Request parameters for clone. + * Request parameters for create. */ -export interface CloneToolRequest { - /** Source recording id */ - sourceRecordingId: number; - /** Title */ +export interface CreateToolRequest { + /** Tool type to add to the project dock. Values: Chat::Transcript|Inbox|Kanban::Board|Message::Board|Questionnaire|Schedule|Todoset|Vault. */ + toolType: string; + /** Title for the new tool. When omitted, Basecamp assigns the next available default title for the tool type. */ title?: string; } @@ -52,28 +52,36 @@ export interface RepositionToolRequest { export class ToolsService extends BaseService { /** - * Clone an existing tool to create a new one - * @param req - Tool request parameters + * Create a tool in a project dock + * @param bucketId - The bucket ID + * @param req - Tool creation parameters * @returns The Tool - * @throws {BasecampError} If the request fails + * @throws {BasecampError} If required fields are missing or invalid * * @example * ```ts - * const result = await client.tools.clone({ sourceRecordingId: 1 }); + * const result = await client.tools.create(123, { toolType: "example" }); * ``` */ - async clone(req: CloneToolRequest): Promise { + async create(bucketId: number, req: CreateToolRequest): Promise { + if (!req.toolType) { + throw Errors.validation("Tool type is required"); + } const response = await this.request( { service: "Tools", - operation: "CloneTool", + operation: "CreateTool", resourceType: "tool", isMutation: true, + resourceId: bucketId, }, () => - this.client.POST("/dock/tools.json", { + this.client.POST("/buckets/{bucketId}/dock/tools.json", { + params: { + path: { bucketId }, + }, body: { - source_recording_id: req.sourceRecordingId, + tool_type: req.toolType, title: req.title, }, }) diff --git a/typescript/tests/services/tools.test.ts b/typescript/tests/services/tools.test.ts index 6eed5a15..3f85511f 100644 --- a/typescript/tests/services/tools.test.ts +++ b/typescript/tests/services/tools.test.ts @@ -59,32 +59,33 @@ describe("ToolsService", () => { }); }); - describe("clone", () => { - it("should clone a tool", async () => { - const sourceToolId = 222; + describe("create", () => { + it("should create a tool in a bucket", async () => { + const bucketId = 456; + const toolType = "Message::Board"; const mockTool = { id: 333, - name: "todoset", - title: "To-dos (Copy)", + name: "message_board", + title: "Message Board (Copy)", enabled: true, position: 5, }; server.use( http.post( - `${BASE_URL}/dock/tools.json`, + `${BASE_URL}/buckets/${bucketId}/dock/tools.json`, async ({ request }) => { - const body = await request.json() as { source_recording_id: number; title: string }; - expect(body.source_recording_id).toBe(sourceToolId); - expect(body.title).toBe("To-dos (Copy)"); + const body = await request.json() as { tool_type: string; title: string }; + expect(body.tool_type).toBe(toolType); + expect(body.title).toBe("Message Board (Copy)"); return HttpResponse.json(mockTool, { status: 201 }); } ) ); - const tool = await client.tools.clone({ sourceRecordingId: sourceToolId, title: "To-dos (Copy)" }); + const tool = await client.tools.create(bucketId, { toolType, title: "Message Board (Copy)" }); expect(tool.id).toBe(333); - expect(tool.title).toBe("To-dos (Copy)"); + expect(tool.title).toBe("Message Board (Copy)"); }); });