@pidgeon-ai/sdk 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,14 +1,67 @@
1
1
  # Changelog
2
2
 
3
3
  Versioned independently of the application. The API is `/v1`; this package
4
- is `0.x`. They are different contracts with different audiences, and tying
4
+ is `1.x`. They are different contracts with different audiences, and tying
5
5
  the two would publish a release for every unrelated product deploy.
6
6
 
7
- Semver applies to **this package's own surface** — the exported types, the
8
- method signatures, the error shape — not to the API behind it.
7
+ Semver applies to **this package's own surface** — the exported names, the
8
+ call shapes, the result shape — not to the API behind it.
9
9
 
10
10
  ## Unreleased
11
11
 
12
+ ## 1.2.0 — the document says what its fields mean
13
+
14
+ **Additive, and a correction.**
15
+
16
+ **Added.** Every request field that means one thing everywhere now carries a
17
+ description, so the types say what `from`, `to`, `attachment_ids` and twenty
18
+ others are for — 0 of 136 had one before.
19
+
20
+ **Corrected.** `messagesSend` is typed `Sent` at `202`, which is what the API
21
+ has always answered. It was declared `Message` at `201`, a response that never
22
+ arrives — so a caller reading `data.subject` off a send got `undefined`, and
23
+ `send` and `reply` appeared to disagree about their shape when they never did.
24
+ Minor rather than major because nothing could have depended on a status code
25
+ the server does not send.
26
+
27
+ ## 1.1.0 — the domain an address is on
28
+
29
+ Additive. Nothing here changes a call that already works.
30
+
31
+ **Added**
32
+
33
+ - `Identity.domain_id`. Every address now says which domain it is on, not
34
+ only its name. This exists because the id was otherwise unlearnable: an
35
+ account whose only domain is the shared one gets `[]` from `GET /domains`,
36
+ correctly — that endpoint lists what a workspace _owns_ — and `domain_id`
37
+ is what `identitiesCreate` requires. A customer building against production
38
+ read the uuid out of the database to get past it.
39
+ - `identitiesCreate` takes `domain` (a name) or `address` (a whole one) in
40
+ place of `domain_id`. Pass exactly one. `address` also supplies the local
41
+ part, which is the form `skills/pidgeon/SKILL.md` has documented since it
42
+ was written.
43
+ - `threadsList` takes `identity_id`, `status`, `owner_type` and
44
+ `assignee_id`. The API has honoured all four since it was written and the
45
+ contract declared none of them, so no generated client could offer them.
46
+ `assignee_id` takes a person's id or the literal `unassigned`.
47
+
48
+ ## 1.0.0 — one name per operation, generated
49
+
50
+ **Published without an entry here**, which is how a changelog stops being
51
+ read. Recorded after the fact, on 13 October, when a customer found the
52
+ package README describing a class the package does not export.
53
+
54
+ **Breaking.** The hand-written client is gone. There is no `Pidgeon` class
55
+ and no `PidgeonError`: the package is generated from `openapi.json`, one
56
+ function per operation named for its tag — `messagesSend`, `messagesGet`,
57
+ `threadsUpdate` — sharing one client you configure at startup with
58
+ `client.setConfig`. A call resolves to `{ data, error }` rather than
59
+ throwing, or pass `throwOnError: true`.
60
+
61
+ Both clients are generated from the same document, so an operation has the
62
+ same name in TypeScript and in Python. `decisions.md` §343 is the argument,
63
+ including why the stutter in `messagesSend` is accepted.
64
+
12
65
  ## 0.2.0 — the mailbox, and the credentials to reach it
13
66
 
14
67
  A minor rather than a patch, because one signature changed. In `0.x` semver
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @pidgeon-ai/sdk
2
2
 
3
- A thin TypeScript client for the [Pidgeon](https://pidgeon.ai) email API.
4
- Typed `fetch` wrappers, one error class, and a webhook verifier.
3
+ A TypeScript client for the [Pidgeon](https://pidgeon.ai) email API,
4
+ generated from `openapi.json`: one typed function per operation, plus a
5
+ webhook verifier.
5
6
 
6
7
  **Zero dependencies.** Needs `fetch` and `crypto.subtle`, so it runs on
7
8
  Node 18+, Bun, Deno, Cloudflare Workers and Vercel functions. ESM and
@@ -11,15 +12,35 @@ CommonJS.
11
12
  pnpm add @pidgeon-ai/sdk
12
13
  ```
13
14
 
15
+ ## The shape
16
+
17
+ **There is no `Pidgeon` class.** Every operation is a function named for its
18
+ tag — `messagesSend`, `messagesGet`, `threadsUpdate` — and they share one
19
+ client you configure at startup. The Python package is generated from the same
20
+ document, so an operation has the same name in both.
21
+
22
+ ```ts
23
+ import { client, messagesGet } from '@pidgeon-ai/sdk';
24
+
25
+ client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });
26
+
27
+ const { data, error } = await messagesGet({ path: { id: messageId } });
28
+ ```
29
+
30
+ A call resolves to `{ data, error }` rather than throwing: `error` carries the
31
+ API's own `code`, a `message` written for a person, and a `field` when one
32
+ input was at fault. Pass `throwOnError: true` on a call to get the data
33
+ directly and an exception instead.
34
+
14
35
  ## Receiving mail
15
36
 
16
37
  Mail arriving at an address your code owns becomes an event, and your
17
38
  endpoint is called with a signed payload.
18
39
 
19
40
  ```ts
20
- import { Pidgeon, verifyWebhook } from '@pidgeon-ai/sdk';
41
+ import { client, messagesGet, messagesSend, verifyWebhook } from '@pidgeon-ai/sdk';
21
42
 
22
- const pidgeon = new Pidgeon({ apiKey: process.env.PIDGEON_API_KEY! });
43
+ client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });
23
44
 
24
45
  export async function POST(request: Request) {
25
46
  // Verify first. An unverified payload is an unauthenticated POST that
@@ -33,15 +54,21 @@ export async function POST(request: Request) {
33
54
  if (event.type !== 'message.received') return new Response(null, { status: 204 });
34
55
 
35
56
  // The event carries ids, never a body. Exchange one for the content.
36
- const mail = await pidgeon.messages.get(event.data.message_id as string);
37
-
38
- await pidgeon.messages.send({
39
- from: mail.to[0].email,
40
- to: mail.from.email,
41
- subject: `Re: ${mail.subject}`,
42
- text: 'Got it — someone will be in touch.',
43
- // Keeps the conversation together in the recipient's client too.
44
- in_reply_to: mail.id,
57
+ const { data: mail } = await messagesGet({
58
+ path: { id: event.data.message_id as string },
59
+ throwOnError: true,
60
+ });
61
+
62
+ await messagesSend({
63
+ body: {
64
+ from: mail.data.to[0]!.email,
65
+ to: mail.data.from.email,
66
+ subject: `Re: ${mail.data.subject}`,
67
+ text: 'Got it — someone will be in touch.',
68
+ // Keeps the conversation together in the recipient's client too.
69
+ in_reply_to: mail.data.id,
70
+ },
71
+ throwOnError: true,
45
72
  });
46
73
 
47
74
  return new Response(null, { status: 204 });
@@ -60,45 +87,60 @@ the signature, and compares in constant time.
60
87
  ## Handing a conversation to a person
61
88
 
62
89
  ```ts
63
- await pidgeon.threads.handOff(threadId);
90
+ import { threadsUpdate } from '@pidgeon-ai/sdk';
91
+
92
+ await threadsUpdate({
93
+ path: { id: threadId },
94
+ body: { owner_type: 'human' },
95
+ throwOnError: true,
96
+ });
64
97
  ```
65
98
 
99
+ Hand it back by setting `owner_type` to `agent`.
100
+
66
101
  It appears in the web inbox, and `thread.updated` tells whatever was
67
102
  driving the conversation that it no longer is.
68
103
 
69
104
  ## Errors
70
105
 
71
- Every failure is a `PidgeonError` carrying the API's own `code`, the HTTP
72
- `status`, the offending `field` on a validation failure, and the request id
73
- to quote in a support conversation.
106
+ A failure comes back in `error`, carrying the API's own `code`, a `message`
107
+ written for a person and safe to show one, and the offending `field` on a
108
+ validation failure.
74
109
 
75
110
  ```ts
76
- import { PidgeonError } from '@pidgeon-ai/sdk';
111
+ const { data, error } = await messagesSend({ body: { from, to, text } });
77
112
 
78
- try {
79
- await pidgeon.messages.send({ from, to, text });
80
- } catch (error) {
81
- if (error instanceof PidgeonError && error.code === 'rate_limited') {
113
+ if (error) {
114
+ if (error.error.code === 'rate_limited') {
82
115
  // Back off. Deliberately not done for you — see below.
83
116
  }
84
- throw error;
117
+ throw new Error(error.error.message);
85
118
  }
86
119
  ```
87
120
 
88
121
  `code` is one of `validation`, `unauthenticated`, `entitlement`,
89
122
  `forbidden`, `not_found`, `conflict`, `rate_limited`, `internal`.
90
123
 
124
+ With `throwOnError: true` the same failure is thrown instead and `data` is no
125
+ longer `undefined`, which is the shape most call sites want.
126
+
91
127
  ## Pagination
92
128
 
93
129
  List endpoints return a page, not an iterator.
94
130
 
95
131
  ```ts
96
- let cursor: string | null = null;
132
+ import { eventsList } from '@pidgeon-ai/sdk';
133
+
134
+ let cursor: string | undefined;
97
135
 
98
136
  do {
99
- const page = await pidgeon.events.list({ limit: 50, cursor: cursor ?? undefined });
137
+ const { data: page } = await eventsList({
138
+ query: { limit: 50, cursor },
139
+ throwOnError: true,
140
+ });
141
+
100
142
  for (const event of page.data) handle(event);
101
- cursor = page.next_cursor;
143
+ cursor = page.next_cursor ?? undefined;
102
144
  } while (cursor);
103
145
  ```
104
146
 
@@ -112,8 +154,10 @@ pagination also hides the rate limit pagination is subject to.
112
154
  ## Options
113
155
 
114
156
  ```ts
115
- new Pidgeon({
116
- apiKey: process.env.PIDGEON_API_KEY!,
157
+ import { client } from '@pidgeon-ai/sdk';
158
+
159
+ client.setConfig({
160
+ auth: () => process.env.PIDGEON_API_KEY!,
117
161
  // Point at a local dev server, or a preview deployment.
118
162
  baseUrl: 'http://localhost:3000/api/v1',
119
163
  // Inject a transport. Useful in tests.
@@ -121,10 +165,13 @@ new Pidgeon({
121
165
  });
122
166
  ```
123
167
 
168
+ `createClient(createConfig({ … }))` makes a second, independent client, for
169
+ code that talks to two accounts; pass it as `client` on a call.
170
+
124
171
  ## Versioning
125
172
 
126
173
  Versioned independently of the API. The API is `/v1`; this package is
127
- `0.x`, and semver applies to its own surface — the exported types, the
174
+ `1.x`, and semver applies to its own surface — the exported types, the
128
175
  method signatures, the error shape. See `CHANGELOG.md`.
129
176
 
130
177
  MIT.