@pingroom/sdk 0.8.0 → 0.9.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.
Files changed (179) hide show
  1. package/README.md +153 -41
  2. package/dist/client.d.ts +14 -348
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +15 -1300
  5. package/dist/client.js.map +1 -1
  6. package/dist/errors.d.ts +32 -3
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +47 -1
  9. package/dist/errors.js.map +1 -1
  10. package/dist/http.d.ts.map +1 -1
  11. package/dist/http.js +7 -1
  12. package/dist/http.js.map +1 -1
  13. package/dist/index.d.ts +3 -5
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/internal/guards.d.ts +7 -0
  18. package/dist/internal/guards.d.ts.map +1 -1
  19. package/dist/internal/guards.js +1 -1
  20. package/dist/internal/guards.js.map +1 -1
  21. package/dist/internal/request.d.ts +41 -0
  22. package/dist/internal/request.d.ts.map +1 -0
  23. package/dist/internal/request.js +78 -0
  24. package/dist/internal/request.js.map +1 -0
  25. package/dist/internal/retry.d.ts +9 -2
  26. package/dist/internal/retry.d.ts.map +1 -1
  27. package/dist/internal/retry.js +24 -6
  28. package/dist/internal/retry.js.map +1 -1
  29. package/dist/internal/validate.d.ts +13 -0
  30. package/dist/internal/validate.d.ts.map +1 -0
  31. package/dist/internal/validate.js +36 -0
  32. package/dist/internal/validate.js.map +1 -0
  33. package/dist/liveStatus.d.ts +106 -11
  34. package/dist/liveStatus.d.ts.map +1 -1
  35. package/dist/liveStatus.js +20 -3
  36. package/dist/liveStatus.js.map +1 -1
  37. package/dist/mcp.d.ts.map +1 -1
  38. package/dist/mcp.js +30 -8
  39. package/dist/mcp.js.map +1 -1
  40. package/dist/namespaces/actions.d.ts +50 -0
  41. package/dist/namespaces/actions.d.ts.map +1 -0
  42. package/dist/namespaces/actions.js +219 -0
  43. package/dist/namespaces/actions.js.map +1 -0
  44. package/dist/namespaces/approvals.d.ts +23 -0
  45. package/dist/namespaces/approvals.d.ts.map +1 -0
  46. package/dist/namespaces/approvals.js +78 -0
  47. package/dist/namespaces/approvals.js.map +1 -0
  48. package/dist/namespaces/attachments.d.ts +37 -0
  49. package/dist/namespaces/attachments.d.ts.map +1 -0
  50. package/dist/namespaces/attachments.js +91 -0
  51. package/dist/namespaces/attachments.js.map +1 -0
  52. package/dist/namespaces/auth.d.ts +58 -0
  53. package/dist/namespaces/auth.d.ts.map +1 -0
  54. package/dist/namespaces/auth.js +242 -0
  55. package/dist/namespaces/auth.js.map +1 -0
  56. package/dist/namespaces/directory.d.ts +10 -0
  57. package/dist/namespaces/directory.d.ts.map +1 -0
  58. package/dist/namespaces/directory.js +19 -0
  59. package/dist/namespaces/directory.js.map +1 -0
  60. package/dist/namespaces/handoffs.d.ts +47 -0
  61. package/dist/namespaces/handoffs.d.ts.map +1 -0
  62. package/dist/namespaces/handoffs.js +126 -0
  63. package/dist/namespaces/handoffs.js.map +1 -0
  64. package/dist/namespaces/inbox.d.ts +17 -0
  65. package/dist/namespaces/inbox.d.ts.map +1 -0
  66. package/dist/namespaces/inbox.js +192 -0
  67. package/dist/namespaces/inbox.js.map +1 -0
  68. package/dist/namespaces/live.d.ts +57 -0
  69. package/dist/namespaces/live.d.ts.map +1 -0
  70. package/dist/namespaces/live.js +116 -0
  71. package/dist/namespaces/live.js.map +1 -0
  72. package/dist/namespaces/notifications.d.ts +25 -0
  73. package/dist/namespaces/notifications.d.ts.map +1 -0
  74. package/dist/namespaces/notifications.js +128 -0
  75. package/dist/namespaces/notifications.js.map +1 -0
  76. package/dist/namespaces/profile.d.ts +19 -0
  77. package/dist/namespaces/profile.d.ts.map +1 -0
  78. package/dist/namespaces/profile.js +22 -0
  79. package/dist/namespaces/profile.js.map +1 -0
  80. package/dist/namespaces/questions.d.ts +41 -0
  81. package/dist/namespaces/questions.d.ts.map +1 -0
  82. package/dist/namespaces/questions.js +107 -0
  83. package/dist/namespaces/questions.js.map +1 -0
  84. package/dist/namespaces/rooms.d.ts +13 -0
  85. package/dist/namespaces/rooms.d.ts.map +1 -0
  86. package/dist/namespaces/rooms.js +41 -0
  87. package/dist/namespaces/rooms.js.map +1 -0
  88. package/dist/namespaces/webhooks.d.ts +11 -0
  89. package/dist/namespaces/webhooks.d.ts.map +1 -0
  90. package/dist/namespaces/webhooks.js +32 -0
  91. package/dist/namespaces/webhooks.js.map +1 -0
  92. package/dist/types/approvals.d.ts +62 -0
  93. package/dist/types/approvals.d.ts.map +1 -0
  94. package/dist/types/approvals.js +2 -0
  95. package/dist/types/approvals.js.map +1 -0
  96. package/dist/types/auth.d.ts +185 -0
  97. package/dist/types/auth.d.ts.map +1 -0
  98. package/dist/types/auth.js +2 -0
  99. package/dist/types/auth.js.map +1 -0
  100. package/dist/types/common.d.ts +43 -0
  101. package/dist/types/common.d.ts.map +1 -0
  102. package/dist/types/common.js +2 -0
  103. package/dist/types/common.js.map +1 -0
  104. package/dist/types/handoffs.d.ts +121 -0
  105. package/dist/types/handoffs.d.ts.map +1 -0
  106. package/dist/types/handoffs.js +2 -0
  107. package/dist/types/handoffs.js.map +1 -0
  108. package/dist/types/inbox.d.ts +51 -0
  109. package/dist/types/inbox.d.ts.map +1 -0
  110. package/dist/types/inbox.js +2 -0
  111. package/dist/types/inbox.js.map +1 -0
  112. package/dist/types/notifications.d.ts +376 -0
  113. package/dist/types/notifications.d.ts.map +1 -0
  114. package/dist/types/notifications.js +2 -0
  115. package/dist/types/notifications.js.map +1 -0
  116. package/dist/types/profile.d.ts +30 -0
  117. package/dist/types/profile.d.ts.map +1 -0
  118. package/dist/types/profile.js +3 -0
  119. package/dist/types/profile.js.map +1 -0
  120. package/dist/types/questions.d.ts +210 -0
  121. package/dist/types/questions.d.ts.map +1 -0
  122. package/dist/types/questions.js +2 -0
  123. package/dist/types/questions.js.map +1 -0
  124. package/dist/types/rooms.d.ts +163 -0
  125. package/dist/types/rooms.d.ts.map +1 -0
  126. package/dist/types/rooms.js +3 -0
  127. package/dist/types/rooms.js.map +1 -0
  128. package/dist/types.d.ts +9 -1199
  129. package/dist/types.d.ts.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/dist/webhooks.d.ts +14 -4
  133. package/dist/webhooks.d.ts.map +1 -1
  134. package/dist/webhooks.js +30 -5
  135. package/dist/webhooks.js.map +1 -1
  136. package/package.json +4 -2
  137. package/src/client.ts +115 -0
  138. package/src/errors.ts +291 -0
  139. package/src/http.ts +225 -0
  140. package/src/index.ts +97 -0
  141. package/src/internal/async.ts +54 -0
  142. package/src/internal/guards.ts +129 -0
  143. package/src/internal/request.ts +97 -0
  144. package/src/internal/retry.ts +157 -0
  145. package/src/internal/url.ts +78 -0
  146. package/src/internal/validate.ts +42 -0
  147. package/src/linkPing.ts +56 -0
  148. package/src/liveStatus.ts +337 -0
  149. package/src/locationPing.ts +101 -0
  150. package/src/mcp.ts +161 -0
  151. package/src/namespaces/actions.ts +234 -0
  152. package/src/namespaces/approvals.ts +104 -0
  153. package/src/namespaces/attachments.ts +106 -0
  154. package/src/namespaces/auth.ts +284 -0
  155. package/src/namespaces/directory.ts +21 -0
  156. package/src/namespaces/handoffs.ts +160 -0
  157. package/src/namespaces/inbox.ts +253 -0
  158. package/src/namespaces/live.ts +134 -0
  159. package/src/namespaces/notifications.ts +152 -0
  160. package/src/namespaces/profile.ts +24 -0
  161. package/src/namespaces/questions.ts +133 -0
  162. package/src/namespaces/rooms.ts +53 -0
  163. package/src/namespaces/webhooks.ts +38 -0
  164. package/src/types/approvals.ts +67 -0
  165. package/src/types/auth.ts +202 -0
  166. package/src/types/common.ts +74 -0
  167. package/src/types/handoffs.ts +148 -0
  168. package/src/types/inbox.ts +64 -0
  169. package/src/types/notifications.ts +393 -0
  170. package/src/types/profile.ts +37 -0
  171. package/src/types/questions.ts +226 -0
  172. package/src/types/rooms.ts +169 -0
  173. package/src/types.ts +17 -0
  174. package/src/version.ts +2 -0
  175. package/src/webhooks.ts +373 -0
  176. package/dist/liveActivity.d.ts +0 -101
  177. package/dist/liveActivity.d.ts.map +0 -1
  178. package/dist/liveActivity.js +0 -83
  179. package/dist/liveActivity.js.map +0 -1
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Live-status streams — the wire contract for driving a Live Activity.
3
+ *
4
+ * A stream is keyed by `correlation_id`: the first ping starts a live card on
5
+ * every room member's lock screen (one alert), further `running` pings move it
6
+ * silently, and the first `done`/`failed` sends one completion alert and ends
7
+ * it. See https://pingroom.io/liveactivities.md.
8
+ *
9
+ * Three producers speak this contract. This SDK reaches the two machine ones:
10
+ * - a room's **incoming webhook** (Pro) — `sendIncomingWebhook()`
11
+ * - an **agent credential** — `pr.live.*` (needs `pingroom:live:write`)
12
+ *
13
+ * The third is a signed-in person driving a stream from the app composer over
14
+ * `POST /api/rooms/{inviteCode}/live` with their own Human JWT. It sends this
15
+ * same body minus `live_status.agent_id` (stripped server-side), so these
16
+ * builders describe it too — but it is not an agent route and has no client
17
+ * here.
18
+ *
19
+ * A **claim request** ("Who's on it?", template `claim`) is a stream one member
20
+ * takes on. It starts like any stream, but it then moves only through
21
+ * `pr.live.claim()` (claim, release, done, cancel), not through further
22
+ * `live_status` legs. See https://pingroom.io/liveactivities.md.
23
+ */
24
+
25
+ export type LiveActivityTemplate =
26
+ | 'status'
27
+ | 'steps'
28
+ | 'progress'
29
+ | 'metrics'
30
+ | 'countdown'
31
+ | 'question'
32
+ | 'matchup'
33
+ | 'claim';
34
+
35
+ export type LiveStatusState = 'running' | 'done' | 'failed';
36
+
37
+ /** Legacy rendering category. Prefer `template`; `alert` selects urgent styling. */
38
+ export type LiveStatusCategory = 'status' | 'steps' | 'alert';
39
+
40
+ export interface LiveStatusMetric {
41
+ label: string;
42
+ value: string;
43
+ }
44
+
45
+ export interface LiveStatusOption {
46
+ value: string;
47
+ label: string;
48
+ }
49
+
50
+ export interface LiveStatusSide {
51
+ label: string;
52
+ value: string;
53
+ iconSvg?: string;
54
+ }
55
+
56
+ /**
57
+ * The `live_status` object. Only `state` is required; every other field is read
58
+ * by the template you selected.
59
+ *
60
+ * `template` and `steps` are fixed at stream creation — a later ping cannot
61
+ * re-template a running card, and a `running` ping whose `steps` list differs
62
+ * from the stored one is rejected with 422 `steps_immutable` (an identical list,
63
+ * or none, is fine; a `done`/`failed` ping ignores it and still ends). Content set at creation is sticky, so a sparse
64
+ * update carrying just `progress` keeps rendering the full template. The
65
+ * terminal leg is sticky too: `{ state: 'done' }` alone keeps the scoreboard,
66
+ * metrics grid, deadline and accent on the final frame.
67
+ */
68
+ export interface LiveStatus {
69
+ state: LiveStatusState;
70
+ template?: LiveActivityTemplate;
71
+ category?: LiveStatusCategory;
72
+ message?: string;
73
+ /** 0..1 — the progress bar / Dynamic Island gauge. */
74
+ progress?: number;
75
+ /**
76
+ * 2–8 labels. Required on the first ping of a `steps` stream; immutable after.
77
+ * Later pings may resend the identical list or omit it; a different list on a
78
+ * `running` ping is rejected with 422 `steps_immutable`, and a `done`/`failed`
79
+ * ping ignores it. Use `current_step` to advance.
80
+ */
81
+ steps?: string[];
82
+ /** Index into `steps`; the only mutable steps field. */
83
+ current_step?: number;
84
+ /** Up to 3 counters, for the `metrics` template. */
85
+ metrics?: LiveStatusMetric[];
86
+ /**
87
+ * Epoch seconds the `countdown` template counts down to. A later ping may move
88
+ * it: send the new absolute value, and the stored `duration_seconds` moves by
89
+ * the same amount (a retried ping moves nothing twice).
90
+ */
91
+ deadline_at?: number;
92
+ /**
93
+ * `countdown` only: the timer's length in seconds. Read from the first ping
94
+ * only (the server derives `deadline_at - now` when omitted); a later ping's
95
+ * own value is ignored. It follows `deadline_at`: moving the deadline on a
96
+ * later ping moves the stored duration by the same amount.
97
+ */
98
+ duration_seconds?: number;
99
+ /** Epoch seconds; renders a live ETA on `status`/`progress`. */
100
+ eta_at?: number;
101
+ /** The ask, for the `question` template. */
102
+ prompt?: string;
103
+ /** Up to 4 choices, for the `question` template. */
104
+ options?: LiveStatusOption[];
105
+ left?: LiveStatusSide;
106
+ right?: LiveStatusSide;
107
+ center?: string;
108
+ /** Hex `#rrggbb` — a semantic accent for one frame (e.g. deadline red). */
109
+ accent_override?: string;
110
+ agent_id?: string;
111
+ }
112
+
113
+ /**
114
+ * Where a "Who's on it?" request stands. `open` and `claimed` are live; the rest
115
+ * are final. A request ends as `expired` when nobody took it by its deadline, or
116
+ * when its claimer let go of it after that deadline.
117
+ */
118
+ export type LiveClaimStatus = 'open' | 'claimed' | 'done' | 'cancelled' | 'expired' | 'unfinished';
119
+
120
+ /** What a member can do to a claim request. */
121
+ export type LiveClaimAction = 'claim' | 'release' | 'done' | 'cancel';
122
+
123
+ /** The stream `state` a claim status maps to: open and claimed are `running`, done is `done`, the rest are `failed`. */
124
+ export type LiveClaimStreamState = 'running' | 'done' | 'failed';
125
+
126
+ /** The person who holds a claim request, or who finished it. */
127
+ export interface LiveClaimParty {
128
+ /** The user id (a UUID). */
129
+ id: string;
130
+ name: string;
131
+ color?: string;
132
+ emoji?: string;
133
+ image_url?: string | null;
134
+ }
135
+
136
+ /** The server-owned `claim` block of a claim request. */
137
+ export interface LiveClaim {
138
+ status: LiveClaimStatus;
139
+ /** Who holds the request (or finished it); `null` while nobody does. */
140
+ by: LiveClaimParty | null;
141
+ /** Epoch seconds the holder took it; `null` while nobody holds it. */
142
+ at: number | null;
143
+ /** A claimed request is past its deadline. It stays live. */
144
+ late: boolean;
145
+ late_nudged_at?: number;
146
+ realerted_at?: number;
147
+ ended_by?: string;
148
+ released_by?: string;
149
+ /** The last claimer the asker was alerted about: a take alerts the asker once per claimer. */
150
+ claimer_alerted_id?: string;
151
+ }
152
+
153
+ /**
154
+ * The 200 body of `pr.live.claim()`. `idempotent` is true when the same person
155
+ * repeated the action that already produced the current status, so nothing
156
+ * changed and nothing was sent.
157
+ *
158
+ * Each action resolves with one `claim.status` / `state` pair: `claim` is
159
+ * `claimed` / `running`, `done` is `done` / `done` and `cancel` is `cancelled` /
160
+ * `failed`. `release` is `open` / `running`, or `expired` / `failed` when the
161
+ * request was already past its deadline, so letting go ended it instead of
162
+ * reopening it.
163
+ */
164
+ export interface LiveClaimResult {
165
+ notification_id: string;
166
+ correlation_id: string;
167
+ claim: LiveClaim;
168
+ state: LiveClaimStreamState;
169
+ idempotent: boolean;
170
+ }
171
+
172
+ /**
173
+ * The body of a refused transition, on `PingRoomError.body`. `claim` and
174
+ * `state` are the request as it now stands (on 409 and 422), so a client can
175
+ * show who got it first. A 403 or 404 carries only `code` and `message`.
176
+ */
177
+ export interface LiveClaimRefusal {
178
+ code: string;
179
+ message: string;
180
+ claim?: LiveClaim;
181
+ state?: LiveClaimStreamState;
182
+ }
183
+
184
+ /** The full body of a live-status ping. */
185
+ export interface LiveStatusPing {
186
+ correlation_id: string;
187
+ live_status: LiveStatus;
188
+ title?: string;
189
+ /** Quick-action slot 1–16 supplying the icon and sound. */
190
+ action?: number;
191
+ data?: Record<string, unknown>;
192
+ requires_ack?: boolean;
193
+ ack_timeout_seconds?: number;
194
+ }
195
+
196
+ export interface LiveStatusResult {
197
+ notification_id: string;
198
+ correlation_id: string;
199
+ /** `started` on creation, then `running` / `done` / `failed`. */
200
+ state: string;
201
+ idempotent: boolean;
202
+ action_state?: unknown;
203
+ }
204
+
205
+ /**
206
+ * The read-back shape from `GET .../live/{correlationId}`.
207
+ *
208
+ * EVERY stored `live_status` field is returned, not a step/progress subset — a
209
+ * `matchup` / `metrics` / `countdown` / `question` stream would otherwise be
210
+ * unresumable after a producer restart. Fields you never set come back as
211
+ * `null` rather than being omitted, so read them defensively.
212
+ *
213
+ * The server-side ownership stamps (`_webhook` / `_agent` / `_user`) are
214
+ * deliberately NOT returned — they are the authorization record, not client
215
+ * state.
216
+ */
217
+ export interface LiveStatusSnapshot {
218
+ notification_id: string;
219
+ correlation_id: string;
220
+ state: string | null;
221
+ progress: number | null;
222
+ message: string | null;
223
+ category: LiveStatusCategory | null;
224
+ template: LiveActivityTemplate | null;
225
+ agent_id: string | null;
226
+ accent_override: string | null;
227
+ eta_at: number | null;
228
+ deadline_at: number | null;
229
+ /** Countdown length in seconds, following any `deadline_at` move; `null` on every other template. */
230
+ duration_seconds: number | null;
231
+ metrics: LiveStatusMetric[] | null;
232
+ prompt: string | null;
233
+ options: LiveStatusOption[] | null;
234
+ left: LiveStatusSide | null;
235
+ right: LiveStatusSide | null;
236
+ center: string | null;
237
+ steps: string[] | null;
238
+ current_step: number | null;
239
+ action_state?: unknown;
240
+ updated_at: string | null;
241
+ /** `claim` template only: the request text. Absent on every other template. */
242
+ title?: string;
243
+ /** `claim` template only: where the request stands. Absent on every other template. */
244
+ claim?: LiveClaim;
245
+ }
246
+
247
+ type Rest = Omit<LiveStatusPing, 'correlation_id' | 'live_status'>;
248
+
249
+ const ping = (correlationId: string, live: LiveStatus, rest: Rest = {}): LiveStatusPing => ({
250
+ correlation_id: correlationId,
251
+ live_status: live,
252
+ ...rest,
253
+ });
254
+
255
+ /**
256
+ * Builders for the common stream shapes. Each returns a body you can hand to
257
+ * `pr.live.push()` or `sendIncomingWebhook()`.
258
+ */
259
+ export const liveStatus = {
260
+ /** Default template: a title line plus a status dot, with optional progress. */
261
+ status(correlationId: string, live: Omit<LiveStatus, 'template'>, rest?: Rest): LiveStatusPing {
262
+ return ping(correlationId, { template: 'status', ...live }, rest);
263
+ },
264
+
265
+ /** A segmented step tracker. `steps` is required on the first ping. */
266
+ steps(correlationId: string, live: Omit<LiveStatus, 'template'>, rest?: Rest): LiveStatusPing {
267
+ return ping(correlationId, { template: 'steps', ...live }, rest);
268
+ },
269
+
270
+ /** A determinate progress bar with an optional live ETA. */
271
+ progress(correlationId: string, live: Omit<LiveStatus, 'template'>, rest?: Rest): LiveStatusPing {
272
+ return ping(correlationId, { template: 'progress', ...live }, rest);
273
+ },
274
+
275
+ /** Up to 3 labelled counters. */
276
+ metrics(correlationId: string, live: Omit<LiveStatus, 'template'>, rest?: Rest): LiveStatusPing {
277
+ return ping(correlationId, { template: 'metrics', ...live }, rest);
278
+ },
279
+
280
+ /** A large live timer counting down to `deadline_at`. */
281
+ countdown(correlationId: string, live: Omit<LiveStatus, 'template'>, rest?: Rest): LiveStatusPing {
282
+ return ping(correlationId, { template: 'countdown', ...live }, rest);
283
+ },
284
+
285
+ /** A prompt with up to 4 options shown on the lock screen. */
286
+ question(correlationId: string, live: Omit<LiveStatus, 'template'>, rest?: Rest): LiveStatusPing {
287
+ return ping(correlationId, { template: 'question', ...live }, rest);
288
+ },
289
+
290
+ /** Two sides plus a center score/clock. */
291
+ matchup(correlationId: string, live: Omit<LiveStatus, 'template'>, rest?: Rest): LiveStatusPing {
292
+ return ping(correlationId, { template: 'matchup', ...live }, rest);
293
+ },
294
+
295
+ /**
296
+ * "Who's on it?": a request one room member takes on. Private standard rooms
297
+ * only (`422 claim_private_rooms_only` elsewhere).
298
+ *
299
+ * Pass the request text as `rest.title` (1 to 120 characters; `live.message`
300
+ * is used when there is no title). Send `state: 'running'` on the first ping.
301
+ * `live.deadline_at` is optional, in the future and at most 8 hours away.
302
+ * Anything else in `live` is ignored, including a `claim` block.
303
+ *
304
+ * After creation the request moves with `pr.live.claim()`. A producer
305
+ * without a member identity (an incoming webhook) ends its own request with
306
+ * `liveStatus.done()` (done) or `liveStatus.failed()` (cancelled).
307
+ */
308
+ claim(correlationId: string, live: Omit<LiveStatus, 'template'>, rest?: Rest): LiveStatusPing {
309
+ return ping(correlationId, { template: 'claim', ...live }, rest);
310
+ },
311
+
312
+ /**
313
+ * An urgent stream, delivered time-sensitive from the start so it breaks
314
+ * through Focus.
315
+ *
316
+ * This is a `category`, not a `template`: `alert` has no template equivalent,
317
+ * and it is the only way to start a STREAM time-sensitive. Fixed at creation,
318
+ * like `template`.
319
+ *
320
+ * (`requires_ack` is unrelated: it demands somebody tap, and since urgency and
321
+ * acknowledgement were split it no longer raises the interruption level at
322
+ * all. For a one-shot ping the delivery-priority flag is `is_urgent`.)
323
+ */
324
+ alert(correlationId: string, live: Omit<LiveStatus, 'template' | 'category'>, rest?: Rest): LiveStatusPing {
325
+ return ping(correlationId, { category: 'alert', ...live }, rest);
326
+ },
327
+
328
+ /** Finish a stream. The one alert your stream earns. */
329
+ done(correlationId: string, message?: string, rest?: Rest): LiveStatusPing {
330
+ return ping(correlationId, { state: 'done', ...(message ? { message } : {}) }, rest);
331
+ },
332
+
333
+ /** Finish a stream unsuccessfully. */
334
+ failed(correlationId: string, message?: string, rest?: Rest): LiveStatusPing {
335
+ return ping(correlationId, { state: 'failed', ...(message ? { message } : {}) }, rest);
336
+ },
337
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Location pings — attach a map-ready point to a ping.
3
+ *
4
+ * The reserved wire shape lives at `data.location`:
5
+ *
6
+ * await pr.broadcast('ab12cd', {
7
+ * message: 'Meet me here',
8
+ * data: { event: 'lunch', ...locationPing({
9
+ * latitude: 25.2048,
10
+ * longitude: 55.2708,
11
+ * label: 'Dubai Mall',
12
+ * }) },
13
+ * });
14
+ */
15
+
16
+ export interface LocationPingInput {
17
+ /** Latitude in decimal degrees, inclusive -90..90. */
18
+ latitude: number;
19
+ /** Longitude in decimal degrees, inclusive -180..180. */
20
+ longitude: number;
21
+ /** Optional display label. Max 100 Unicode characters. */
22
+ label?: string;
23
+ /** Optional human-readable address. Max 255 Unicode characters. */
24
+ address?: string;
25
+ }
26
+
27
+ /** The validated value stored under the reserved `data.location` key. */
28
+ export interface LocationPingLocation {
29
+ latitude: number;
30
+ longitude: number;
31
+ label?: string;
32
+ address?: string;
33
+ }
34
+
35
+ /** A fragment that can be spread into a ping's structured `data` object. */
36
+ export interface LocationPingData {
37
+ location: LocationPingLocation;
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ const LABEL_MAX = 100;
42
+ const ADDRESS_MAX = 255;
43
+
44
+ function requireCoordinate(value: unknown, name: 'latitude' | 'longitude', min: number, max: number): number {
45
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
46
+ throw new TypeError(`locationPing: ${name} must be a finite JSON number`);
47
+ }
48
+ if (value < min || value > max) {
49
+ throw new TypeError(`locationPing: ${name} must be between ${min} and ${max}`);
50
+ }
51
+ return value;
52
+ }
53
+
54
+ function optionalString(value: unknown, name: 'label' | 'address', max: number): string | undefined {
55
+ if (value === undefined) return undefined;
56
+ if (typeof value !== 'string') {
57
+ throw new TypeError(`locationPing: ${name} must be a string`);
58
+ }
59
+ const length = Array.from(value).length;
60
+ if (length > max) {
61
+ throw new TypeError(`locationPing: ${name} must be at most ${max} characters`);
62
+ }
63
+ return value;
64
+ }
65
+
66
+ function validateLocation(input: unknown): LocationPingLocation {
67
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
68
+ throw new TypeError('locationPing: input must be an object');
69
+ }
70
+
71
+ const candidate = input as Record<string, unknown>;
72
+ const location: LocationPingLocation = {
73
+ latitude: requireCoordinate(candidate.latitude, 'latitude', -90, 90),
74
+ longitude: requireCoordinate(candidate.longitude, 'longitude', -180, 180),
75
+ };
76
+ const label = optionalString(candidate.label, 'label', LABEL_MAX);
77
+ const address = optionalString(candidate.address, 'address', ADDRESS_MAX);
78
+ if (label !== undefined) location.label = label;
79
+ if (address !== undefined) location.address = address;
80
+ return location;
81
+ }
82
+
83
+ /** Build and validate the reserved `data.location` fragment for a location ping. */
84
+ export function locationPing(input: LocationPingInput): LocationPingData {
85
+ return { location: validateLocation(input) };
86
+ }
87
+
88
+ /**
89
+ * Read a valid reserved location from an arbitrary received `data` value.
90
+ * Invalid or absent location metadata is ignored instead of throwing while a
91
+ * notification feed is being processed.
92
+ */
93
+ export function extractLocationPing(data: unknown): LocationPingLocation | null {
94
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) return null;
95
+ if (!Object.hasOwn(data, 'location')) return null;
96
+ try {
97
+ return validateLocation((data as Record<string, unknown>).location);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
package/src/mcp.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { PingRoomError } from './errors.js';
2
+ import type { HttpClient } from './http.js';
3
+ import { VERSION } from './version.js';
4
+
5
+ export interface JsonRpcError {
6
+ code: number;
7
+ message: string;
8
+ data?: unknown;
9
+ }
10
+
11
+ export interface McpTool {
12
+ name: string;
13
+ description?: string;
14
+ inputSchema?: Record<string, unknown>;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ export interface McpToolResult {
19
+ content: Array<{ type: string; text?: string; [key: string]: unknown }>;
20
+ isError?: boolean;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ export interface McpInitializeResult {
25
+ protocolVersion: string;
26
+ capabilities: Record<string, unknown>;
27
+ serverInfo: { name: string; version: string; [key: string]: unknown };
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ /**
32
+ * Thin client for the PingRoom MCP endpoint (`POST /api/agent/mcp`,
33
+ * JSON-RPC 2.0). Tools are scope-filtered server-side, so `listTools()` returns
34
+ * only what the credential can call.
35
+ */
36
+ export class McpClient {
37
+ private nextId = 1;
38
+ private initialization: Promise<McpInitializeResult> | null = null;
39
+ private initializeResult: McpInitializeResult | null = null;
40
+ private protocolVersion: string | null = null;
41
+
42
+ constructor(
43
+ private readonly http: HttpClient,
44
+ private readonly path = '/api/agent/mcp',
45
+ ) {}
46
+
47
+ /** Raw JSON-RPC call. Resolves the `result`; throws PingRoomError on a JSON-RPC `error`. */
48
+ async call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
49
+ const id = this.nextId++;
50
+ const envelope = { jsonrpc: '2.0', id, method, ...(params ? { params } : {}) };
51
+ let res: { result?: T; error?: JsonRpcError };
52
+ try {
53
+ res = await this.http.request<{ result?: T; error?: JsonRpcError }>('POST', this.path, {
54
+ body: envelope,
55
+ headers: this.headers(),
56
+ });
57
+ } catch (error) {
58
+ // The HTTP layer refuses a non-JSON 2xx as `invalid_response`; MCP callers
59
+ // branch on its own code for a reply that is not a JSON-RPC response.
60
+ if (error instanceof PingRoomError && error.status === 0 && error.code === 'invalid_response') {
61
+ throw new PingRoomError('MCP endpoint returned a response with neither `result` nor `error`.', {
62
+ code: 'mcp_invalid_response',
63
+ body: error.body,
64
+ });
65
+ }
66
+ throw error;
67
+ }
68
+ const isObject = res !== null && typeof res === 'object' && !Array.isArray(res);
69
+ if (isObject && res.error) {
70
+ throw new PingRoomError(`MCP error ${res.error.code}: ${res.error.message}`, {
71
+ code: `mcp_${res.error.code}`,
72
+ body: res.error,
73
+ });
74
+ }
75
+ // A 2xx that is not a JSON-RPC response (no body, an HTML page, an object
76
+ // with neither member) must fail here, not as a TypeError in the caller.
77
+ if (!isObject || !Object.prototype.hasOwnProperty.call(res, 'result')) {
78
+ throw new PingRoomError('MCP endpoint returned a response with neither `result` nor `error`.', {
79
+ code: 'mcp_invalid_response',
80
+ });
81
+ }
82
+ return res.result as T;
83
+ }
84
+
85
+ /**
86
+ * Perform the MCP initialize handshake once, including the required
87
+ * `notifications/initialized` notification. Safe to call more than once.
88
+ */
89
+ initialize(params?: Record<string, unknown>): Promise<McpInitializeResult> {
90
+ if (this.initializeResult) {
91
+ return Promise.resolve(this.initializeResult);
92
+ }
93
+ if (this.initialization) {
94
+ return this.initialization;
95
+ }
96
+
97
+ this.initialization = this.performInitialize(params).finally(() => {
98
+ this.initialization = null;
99
+ });
100
+ return this.initialization;
101
+ }
102
+
103
+ /** Alias for initialize(), useful when treating this helper as a connection. */
104
+ connect(params?: Record<string, unknown>): Promise<McpInitializeResult> {
105
+ return this.initialize(params);
106
+ }
107
+
108
+ async listTools(): Promise<{ tools: McpTool[] }> {
109
+ await this.initialize();
110
+ return this.call<{ tools: McpTool[] }>('tools/list');
111
+ }
112
+
113
+ async callTool(name: string, args: Record<string, unknown> = {}): Promise<McpToolResult> {
114
+ await this.initialize();
115
+ return this.call<McpToolResult>('tools/call', { name, arguments: args });
116
+ }
117
+
118
+ private async performInitialize(params?: Record<string, unknown>): Promise<McpInitializeResult> {
119
+ const result = await this.call<McpInitializeResult>(
120
+ 'initialize',
121
+ params ?? {
122
+ protocolVersion: '2025-06-18',
123
+ capabilities: {},
124
+ clientInfo: { name: 'pingroom-sdk', version: VERSION },
125
+ },
126
+ );
127
+ if (!result || typeof result.protocolVersion !== 'string') {
128
+ throw new PingRoomError('MCP initialize response did not include a protocol version.', {
129
+ code: 'mcp_invalid_initialize',
130
+ body: result,
131
+ });
132
+ }
133
+
134
+ this.protocolVersion = result.protocolVersion;
135
+ await this.notify('notifications/initialized');
136
+ this.initializeResult = result;
137
+ return result;
138
+ }
139
+
140
+ private async notify(method: string, params?: Record<string, unknown>): Promise<void> {
141
+ const envelope = { jsonrpc: '2.0', method, ...(params ? { params } : {}) };
142
+ try {
143
+ await this.http.request('POST', this.path, {
144
+ body: envelope,
145
+ headers: this.headers(),
146
+ });
147
+ } catch (error) {
148
+ // A notification has no reply: any 2xx acknowledges it, whatever its body.
149
+ if (!(error instanceof PingRoomError && error.status === 0 && error.code === 'invalid_response')) {
150
+ throw error;
151
+ }
152
+ }
153
+ }
154
+
155
+ private headers(): Record<string, string> {
156
+ return {
157
+ Accept: 'application/json, text/event-stream',
158
+ ...(this.protocolVersion ? { 'MCP-Protocol-Version': this.protocolVersion } : {}),
159
+ };
160
+ }
161
+ }