Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.1.0"
".": "0.2.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-0763b61997721da6f4514241bf0f7bb5f7a88c7298baf0f1b2d58036aaf7e2f1.yml
openapi_spec_hash: 5158475919c04bb52fb03c6a4582188d
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-bea2e5f3b01053a66261a824c75c2640856d0ba00ad795ab71734c4ab9ae33b0.yml
openapi_spec_hash: d766f6e344c12ca6d23e6ef6713b38c4
config_hash: 5fa7ded4bfdffe4cc1944a819da87f9f
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## 0.2.0 (2026-01-06)

Full Changelog: [v0.1.0...v0.2.0](https://github.com/beeper/desktop-api-go/compare/v0.1.0...v0.2.0)

### Features

* **api:** add `description` field to chats, make `title` optional ([fbf0470](https://github.com/beeper/desktop-api-go/commit/fbf047029f11c57542963ed3446ab191ae738643))
* **encoder:** support bracket encoding form-data object members ([a338742](https://github.com/beeper/desktop-api-go/commit/a33874294b78dc962f1acf74668ab59697d61711))


### Bug Fixes

* **client:** correctly specify Accept header with */* instead of empty ([2bcd6c1](https://github.com/beeper/desktop-api-go/commit/2bcd6c1aaaf2b7e6e554d8a87743d2593856f678))
* **mcp:** correct code tool API endpoint ([568939e](https://github.com/beeper/desktop-api-go/commit/568939eff6ad5bcdcd42d7bae3439af3389e4b8a))
* rename param to avoid collision ([d304cbe](https://github.com/beeper/desktop-api-go/commit/d304cbe9e74f9486567156cb28a60cf7cee5b053))


### Chores

* add float64 to valid types for RegisterFieldValidator ([4658d5c](https://github.com/beeper/desktop-api-go/commit/4658d5c7d11ff703a1b7e5ccfb594b22f2ee5144))
* bump gjson version ([1194594](https://github.com/beeper/desktop-api-go/commit/1194594b4e0744f59f66e0cda1929e75eb8973a2))
* elide duplicate aliases ([fa3d456](https://github.com/beeper/desktop-api-go/commit/fa3d4564c46606bcb63fd98906eb0687758ac059))
* **internal:** codegen related update ([f2d3a30](https://github.com/beeper/desktop-api-go/commit/f2d3a30447ffaae00b456f610aba7afee08d32f0))
* **internal:** codegen related update ([77a6d60](https://github.com/beeper/desktop-api-go/commit/77a6d605f55019d31fba8ed59ddc5232f527f9ff))
* **internal:** grammar fix (it's -> its) ([60171f3](https://github.com/beeper/desktop-api-go/commit/60171f3c6eed029a5689bc8ac58656baf63d88f3))


### Documentation

* prominently feature MCP server setup in root SDK readmes ([fcad6f3](https://github.com/beeper/desktop-api-go/commit/fcad6f33198927cfd1e144648a38fb43b1d43f06))

## 0.1.0 (2025-10-16)

Full Changelog: [v0.0.1...v0.1.0](https://github.com/beeper/desktop-api-go/compare/v0.0.1...v0.1.0)
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2025 beeperdesktop
Copyright 2026 beeperdesktop

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
The Beeper Desktop Go library provides convenient access to the [Beeper Desktop REST API](https://developers.beeper.com/desktop-api/)
from applications written in Go.

## MCP Server

Use the Beeper Desktop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.

[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1tY3AiXX0)
[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-mcp%22%5D%7D)

> Note: You may need to set environment variables in your MCP client.

## Installation

<!-- x-release-please-start-version -->
Expand All @@ -26,7 +35,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/beeper/desktop-api-go@v0.1.0'
go get -u 'github.com/beeper/desktop-api-go@v0.2.0'
```

<!-- x-release-please-end -->
Expand Down Expand Up @@ -127,7 +136,7 @@ custom := param.Override[beeperdesktopapi.FooParams](12)

### Request unions

Unions are represented as a struct with fields prefixed by "Of" for each of it's variants,
Unions are represented as a struct with fields prefixed by "Of" for each of its variants,
only one field can be non-zero. The non-zero field will be serialized.

Sub-properties of the union can be accessed via methods on the union struct.
Expand Down
15 changes: 9 additions & 6 deletions chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func (r *ChatService) ListAutoPaging(ctx context.Context, query ChatListParams,
// archived=false to move back to inbox
func (r *ChatService) Archive(ctx context.Context, chatID string, body ChatArchiveParams, opts ...option.RequestOption) (err error) {
opts = slices.Concat(r.Options, opts)
opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...)
opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...)
if chatID == "" {
err = errors.New("missing required chatID parameter")
return
Expand Down Expand Up @@ -141,41 +141,44 @@ type Chat struct {
Network string `json:"network,required"`
// Chat participants information.
Participants ChatParticipants `json:"participants,required"`
// Display title of the chat as computed by the client/server.
Title string `json:"title,required"`
// Chat type: 'single' for direct messages, 'group' for group chats.
//
// Any of "single", "group".
Type ChatType `json:"type,required"`
// Number of unread messages.
UnreadCount int64 `json:"unreadCount,required"`
// Description of the chat.
Description string `json:"description,nullable"`
// True if chat is archived.
IsArchived bool `json:"isArchived"`
// True if chat notifications are muted.
// True if the chat is muted.
IsMuted bool `json:"isMuted"`
// True if chat is pinned.
// True if the chat is pinned.
IsPinned bool `json:"isPinned"`
// Timestamp of last activity.
LastActivity time.Time `json:"lastActivity" format:"date-time"`
// Last read message sortKey.
LastReadMessageSortKey string `json:"lastReadMessageSortKey"`
// Local chat ID specific to this Beeper Desktop installation.
LocalChatID string `json:"localChatID,nullable"`
// Display title of the chat.
Title string `json:"title,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
ID respjson.Field
AccountID respjson.Field
Network respjson.Field
Participants respjson.Field
Title respjson.Field
Type respjson.Field
UnreadCount respjson.Field
Description respjson.Field
IsArchived respjson.Field
IsMuted respjson.Field
IsPinned respjson.Field
LastActivity respjson.Field
LastReadMessageSortKey respjson.Field
LocalChatID respjson.Field
Title respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
Expand Down
4 changes: 2 additions & 2 deletions chatreminder.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func NewChatReminderService(opts ...option.RequestOption) (r ChatReminderService
// Set a reminder for a chat at a specific time
func (r *ChatReminderService) New(ctx context.Context, chatID string, body ChatReminderNewParams, opts ...option.RequestOption) (err error) {
opts = slices.Concat(r.Options, opts)
opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...)
opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...)
if chatID == "" {
err = errors.New("missing required chatID parameter")
return
Expand All @@ -52,7 +52,7 @@ func (r *ChatReminderService) New(ctx context.Context, chatID string, body ChatR
// Clear an existing reminder from a chat
func (r *ChatReminderService) Delete(ctx context.Context, chatID string, opts ...option.RequestOption) (err error) {
opts = slices.Concat(r.Options, opts)
opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...)
opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...)
if chatID == "" {
err = errors.New("missing required chatID parameter")
return
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/beeper/desktop-api-go
go 1.22

require (
github.com/tidwall/gjson v1.14.4
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
Expand Down
80 changes: 44 additions & 36 deletions internal/apiform/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type encoderField struct {
type encoderEntry struct {
reflect.Type
dateFormat string
arrayFmt string
root bool
}

Expand All @@ -77,6 +78,7 @@ func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
entry := encoderEntry{
Type: t,
dateFormat: e.dateFormat,
arrayFmt: e.arrayFmt,
root: e.root,
}

Expand Down Expand Up @@ -178,34 +180,9 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
}
}

func arrayKeyEncoder(arrayFmt string) func(string, int) string {
var keyFn func(string, int) string
switch arrayFmt {
case "comma", "repeat":
keyFn = func(k string, _ int) string { return k }
case "brackets":
keyFn = func(key string, _ int) string { return key + "[]" }
case "indices:dots":
keyFn = func(k string, i int) string {
if k == "" {
return strconv.Itoa(i)
}
return k + "." + strconv.Itoa(i)
}
case "indices:brackets":
keyFn = func(k string, i int) string {
if k == "" {
return strconv.Itoa(i)
}
return k + "[" + strconv.Itoa(i) + "]"
}
}
return keyFn
}

func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
itemEncoder := e.typeEncoder(t.Elem())
keyFn := arrayKeyEncoder(e.arrayFmt)
keyFn := e.arrayKeyEncoder()
return func(key string, v reflect.Value, writer *multipart.Writer) error {
if keyFn == nil {
return fmt.Errorf("apiform: unsupported array format")
Expand Down Expand Up @@ -303,13 +280,10 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
})

return func(key string, value reflect.Value, writer *multipart.Writer) error {
if key != "" {
key = key + "."
}

keyFn := e.objKeyEncoder(key)
for _, ef := range encoderFields {
field := value.FieldByIndex(ef.idx)
err := ef.fn(key+ef.tag.name, field, writer)
err := ef.fn(keyFn(ef.tag.name), field, writer)
if err != nil {
return err
}
Expand Down Expand Up @@ -405,6 +379,43 @@ func (e *encoder) newReaderTypeEncoder() encoderFunc {
}
}

func (e encoder) arrayKeyEncoder() func(string, int) string {
var keyFn func(string, int) string
switch e.arrayFmt {
case "comma", "repeat":
keyFn = func(k string, _ int) string { return k }
case "brackets":
keyFn = func(key string, _ int) string { return key + "[]" }
case "indices:dots":
keyFn = func(k string, i int) string {
if k == "" {
return strconv.Itoa(i)
}
return k + "." + strconv.Itoa(i)
}
case "indices:brackets":
keyFn = func(k string, i int) string {
if k == "" {
return strconv.Itoa(i)
}
return k + "[" + strconv.Itoa(i) + "]"
}
}
return keyFn
}

func (e encoder) objKeyEncoder(parent string) func(string) string {
if parent == "" {
return func(child string) string { return child }
}
switch e.arrayFmt {
case "brackets":
return func(child string) string { return parent + "[" + child + "]" }
default:
return func(child string) string { return parent + "." + child }
}
}

// Given a []byte of json (may either be an empty object or an object that already contains entries)
// encode all of the entries in the map to the json byte array.
func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error {
Expand All @@ -413,10 +424,6 @@ func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipar
value reflect.Value
}

if key != "" {
key = key + "."
}

pairs := []mapPair{}

iter := v.MapRange()
Expand All @@ -434,8 +441,9 @@ func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipar
})

elementEncoder := e.typeEncoder(v.Type().Elem())
keyFn := e.objKeyEncoder(key)
for _, p := range pairs {
err := elementEncoder(key+string(p.key), p.value, writer)
err := elementEncoder(keyFn(p.key), p.value, writer)
if err != nil {
return err
}
Expand Down
51 changes: 50 additions & 1 deletion internal/apiform/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ type StructUnion struct {
param.APIUnion
}

type MultipartMarshalerParent struct {
Middle MultipartMarshalerMiddleNext `form:"middle"`
}

type MultipartMarshalerMiddleNext struct {
MiddleNext MultipartMarshalerMiddle `form:"middleNext"`
}

type MultipartMarshalerMiddle struct {
Child int `form:"child"`
}

var tests = map[string]struct {
buf string
val any
Expand Down Expand Up @@ -366,6 +378,19 @@ true
},
},
},
"recursive_struct,brackets": {
`--xxx
Content-Disposition: form-data; name="child[name]"

Alex
--xxx
Content-Disposition: form-data; name="name"

Robert
--xxx--
`,
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
},

"recursive_struct": {
`--xxx
Expand Down Expand Up @@ -529,6 +554,30 @@ Content-Disposition: form-data; name="union"
Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
},
},
"deeply-nested-struct,brackets": {
`--xxx
Content-Disposition: form-data; name="middle[middleNext][child]"

10
--xxx--
`,
MultipartMarshalerParent{
Middle: MultipartMarshalerMiddleNext{
MiddleNext: MultipartMarshalerMiddle{
Child: 10,
},
},
},
},
"deeply-nested-map,brackets": {
`--xxx
Content-Disposition: form-data; name="middle[middleNext][child]"

10
--xxx--
`,
map[string]any{"middle": map[string]any{"middleNext": map[string]any{"child": 10}}},
},
}

func TestEncode(t *testing.T) {
Expand All @@ -553,7 +602,7 @@ func TestEncode(t *testing.T) {
}
raw := buf.Bytes()
if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") {
t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw))
t.Errorf("expected %+#v to serialize to '%s' but got '%s' (with format %s)", test.val, test.buf, string(raw), arrayFmt)
}
})
}
Expand Down
Loading