@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.
- package/README.md +153 -41
- package/dist/client.d.ts +14 -348
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +15 -1300
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +32 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +47 -1
- package/dist/errors.js.map +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +7 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/guards.d.ts +7 -0
- package/dist/internal/guards.d.ts.map +1 -1
- package/dist/internal/guards.js +1 -1
- package/dist/internal/guards.js.map +1 -1
- package/dist/internal/request.d.ts +41 -0
- package/dist/internal/request.d.ts.map +1 -0
- package/dist/internal/request.js +78 -0
- package/dist/internal/request.js.map +1 -0
- package/dist/internal/retry.d.ts +9 -2
- package/dist/internal/retry.d.ts.map +1 -1
- package/dist/internal/retry.js +24 -6
- package/dist/internal/retry.js.map +1 -1
- package/dist/internal/validate.d.ts +13 -0
- package/dist/internal/validate.d.ts.map +1 -0
- package/dist/internal/validate.js +36 -0
- package/dist/internal/validate.js.map +1 -0
- package/dist/liveStatus.d.ts +106 -11
- package/dist/liveStatus.d.ts.map +1 -1
- package/dist/liveStatus.js +20 -3
- package/dist/liveStatus.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +30 -8
- package/dist/mcp.js.map +1 -1
- package/dist/namespaces/actions.d.ts +50 -0
- package/dist/namespaces/actions.d.ts.map +1 -0
- package/dist/namespaces/actions.js +219 -0
- package/dist/namespaces/actions.js.map +1 -0
- package/dist/namespaces/approvals.d.ts +23 -0
- package/dist/namespaces/approvals.d.ts.map +1 -0
- package/dist/namespaces/approvals.js +78 -0
- package/dist/namespaces/approvals.js.map +1 -0
- package/dist/namespaces/attachments.d.ts +37 -0
- package/dist/namespaces/attachments.d.ts.map +1 -0
- package/dist/namespaces/attachments.js +91 -0
- package/dist/namespaces/attachments.js.map +1 -0
- package/dist/namespaces/auth.d.ts +58 -0
- package/dist/namespaces/auth.d.ts.map +1 -0
- package/dist/namespaces/auth.js +242 -0
- package/dist/namespaces/auth.js.map +1 -0
- package/dist/namespaces/directory.d.ts +10 -0
- package/dist/namespaces/directory.d.ts.map +1 -0
- package/dist/namespaces/directory.js +19 -0
- package/dist/namespaces/directory.js.map +1 -0
- package/dist/namespaces/handoffs.d.ts +47 -0
- package/dist/namespaces/handoffs.d.ts.map +1 -0
- package/dist/namespaces/handoffs.js +126 -0
- package/dist/namespaces/handoffs.js.map +1 -0
- package/dist/namespaces/inbox.d.ts +17 -0
- package/dist/namespaces/inbox.d.ts.map +1 -0
- package/dist/namespaces/inbox.js +192 -0
- package/dist/namespaces/inbox.js.map +1 -0
- package/dist/namespaces/live.d.ts +57 -0
- package/dist/namespaces/live.d.ts.map +1 -0
- package/dist/namespaces/live.js +116 -0
- package/dist/namespaces/live.js.map +1 -0
- package/dist/namespaces/notifications.d.ts +25 -0
- package/dist/namespaces/notifications.d.ts.map +1 -0
- package/dist/namespaces/notifications.js +128 -0
- package/dist/namespaces/notifications.js.map +1 -0
- package/dist/namespaces/profile.d.ts +19 -0
- package/dist/namespaces/profile.d.ts.map +1 -0
- package/dist/namespaces/profile.js +22 -0
- package/dist/namespaces/profile.js.map +1 -0
- package/dist/namespaces/questions.d.ts +41 -0
- package/dist/namespaces/questions.d.ts.map +1 -0
- package/dist/namespaces/questions.js +107 -0
- package/dist/namespaces/questions.js.map +1 -0
- package/dist/namespaces/rooms.d.ts +13 -0
- package/dist/namespaces/rooms.d.ts.map +1 -0
- package/dist/namespaces/rooms.js +41 -0
- package/dist/namespaces/rooms.js.map +1 -0
- package/dist/namespaces/webhooks.d.ts +11 -0
- package/dist/namespaces/webhooks.d.ts.map +1 -0
- package/dist/namespaces/webhooks.js +32 -0
- package/dist/namespaces/webhooks.js.map +1 -0
- package/dist/types/approvals.d.ts +62 -0
- package/dist/types/approvals.d.ts.map +1 -0
- package/dist/types/approvals.js +2 -0
- package/dist/types/approvals.js.map +1 -0
- package/dist/types/auth.d.ts +185 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +2 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/common.d.ts +43 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +2 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/handoffs.d.ts +121 -0
- package/dist/types/handoffs.d.ts.map +1 -0
- package/dist/types/handoffs.js +2 -0
- package/dist/types/handoffs.js.map +1 -0
- package/dist/types/inbox.d.ts +51 -0
- package/dist/types/inbox.d.ts.map +1 -0
- package/dist/types/inbox.js +2 -0
- package/dist/types/inbox.js.map +1 -0
- package/dist/types/notifications.d.ts +376 -0
- package/dist/types/notifications.d.ts.map +1 -0
- package/dist/types/notifications.js +2 -0
- package/dist/types/notifications.js.map +1 -0
- package/dist/types/profile.d.ts +30 -0
- package/dist/types/profile.d.ts.map +1 -0
- package/dist/types/profile.js +3 -0
- package/dist/types/profile.js.map +1 -0
- package/dist/types/questions.d.ts +210 -0
- package/dist/types/questions.d.ts.map +1 -0
- package/dist/types/questions.js +2 -0
- package/dist/types/questions.js.map +1 -0
- package/dist/types/rooms.d.ts +163 -0
- package/dist/types/rooms.d.ts.map +1 -0
- package/dist/types/rooms.js +3 -0
- package/dist/types/rooms.js.map +1 -0
- package/dist/types.d.ts +9 -1199
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhooks.d.ts +14 -4
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +30 -5
- package/dist/webhooks.js.map +1 -1
- package/package.json +4 -2
- package/src/client.ts +115 -0
- package/src/errors.ts +291 -0
- package/src/http.ts +225 -0
- package/src/index.ts +97 -0
- package/src/internal/async.ts +54 -0
- package/src/internal/guards.ts +129 -0
- package/src/internal/request.ts +97 -0
- package/src/internal/retry.ts +157 -0
- package/src/internal/url.ts +78 -0
- package/src/internal/validate.ts +42 -0
- package/src/linkPing.ts +56 -0
- package/src/liveStatus.ts +337 -0
- package/src/locationPing.ts +101 -0
- package/src/mcp.ts +161 -0
- package/src/namespaces/actions.ts +234 -0
- package/src/namespaces/approvals.ts +104 -0
- package/src/namespaces/attachments.ts +106 -0
- package/src/namespaces/auth.ts +284 -0
- package/src/namespaces/directory.ts +21 -0
- package/src/namespaces/handoffs.ts +160 -0
- package/src/namespaces/inbox.ts +253 -0
- package/src/namespaces/live.ts +134 -0
- package/src/namespaces/notifications.ts +152 -0
- package/src/namespaces/profile.ts +24 -0
- package/src/namespaces/questions.ts +133 -0
- package/src/namespaces/rooms.ts +53 -0
- package/src/namespaces/webhooks.ts +38 -0
- package/src/types/approvals.ts +67 -0
- package/src/types/auth.ts +202 -0
- package/src/types/common.ts +74 -0
- package/src/types/handoffs.ts +148 -0
- package/src/types/inbox.ts +64 -0
- package/src/types/notifications.ts +393 -0
- package/src/types/profile.ts +37 -0
- package/src/types/questions.ts +226 -0
- package/src/types/rooms.ts +169 -0
- package/src/types.ts +17 -0
- package/src/version.ts +2 -0
- package/src/webhooks.ts +373 -0
- package/dist/liveActivity.d.ts +0 -101
- package/dist/liveActivity.d.ts.map +0 -1
- package/dist/liveActivity.js +0 -83
- 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
|
+
}
|