@pingroom/sdk 0.1.0 → 0.3.1
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 +259 -5
- package/dist/client.d.ts +111 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +242 -1
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/linkPing.d.ts +27 -0
- package/dist/linkPing.d.ts.map +1 -0
- package/dist/linkPing.js +39 -0
- package/dist/linkPing.js.map +1 -0
- package/dist/liveActivity.d.ts +101 -0
- package/dist/liveActivity.d.ts.map +1 -0
- package/dist/liveActivity.js +83 -0
- package/dist/liveActivity.js.map +1 -0
- package/dist/liveStatus.d.ts +168 -0
- package/dist/liveStatus.d.ts.map +1 -0
- package/dist/liveStatus.js +82 -0
- package/dist/liveStatus.js.map +1 -0
- package/dist/types.d.ts +296 -1
- 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 +32 -6
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +69 -13
- package/dist/webhooks.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official TypeScript/JavaScript SDK for the [PingRoom](https://pingroom.io) agent API — the push-native notification fabric for agents.
|
|
4
4
|
|
|
5
|
-
One typed client for everything an agent does: authenticate, create and join rooms, send pings, message other agents by handle, ask a human to approve something and
|
|
5
|
+
One typed client for everything an agent does: authenticate, create and join rooms, send pings, message other agents by handle, ask a human to approve something — or answer a multi-option question — and block until they tap, listen for inbound pings in real time, drive the MCP endpoint, and verify outgoing-webhook signatures.
|
|
6
6
|
|
|
7
7
|
- **Zero runtime dependencies** — built on the platform `fetch`. Works in Node ≥ 20, browsers, Cloudflare Workers, Deno, and Bun.
|
|
8
8
|
- **Fully typed** — ships `.d.ts`; request fields mirror the HTTP API verbatim.
|
|
@@ -20,12 +20,32 @@ import { PingRoom } from '@pingroom/sdk';
|
|
|
20
20
|
const pr = new PingRoom({ token: process.env.PINGROOM_TOKEN });
|
|
21
21
|
|
|
22
22
|
// Broadcast to a room you own
|
|
23
|
-
await pr.broadcast('ab12cd', {
|
|
23
|
+
await pr.broadcast('ab12cd', {
|
|
24
|
+
message: 'Deploy shipped ✅',
|
|
25
|
+
data: { version: '1.4.0' },
|
|
26
|
+
requires_ack: true,
|
|
27
|
+
ack_timeout_seconds: 300,
|
|
28
|
+
});
|
|
24
29
|
|
|
25
30
|
// Ping another agent by handle
|
|
26
31
|
await pr.agents.ping('agt_reviewer', { message: 'PR #42 ready', correlation_id: 'pr-42' });
|
|
27
32
|
```
|
|
28
33
|
|
|
34
|
+
### Link pings
|
|
35
|
+
|
|
36
|
+
A ping becomes a tappable link button when its `data` carries the reserved
|
|
37
|
+
keys `url` (absolute http(s), ≤ 2048 chars) and optional `button_label`
|
|
38
|
+
(≤ 26 chars). The `linkPing()` helper builds and validates that fragment:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { PingRoom, linkPing } from '@pingroom/sdk';
|
|
42
|
+
|
|
43
|
+
await pr.broadcast('ab12cd', {
|
|
44
|
+
message: 'Build 512 ready',
|
|
45
|
+
data: { commit: 'abc123', ...linkPing({ url: 'https://ci.example.com/b/512', buttonLabel: 'Open build' }) },
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
29
49
|
> Security note: an agent credential is a bearer token. Keep it server-side. Do not embed a long-lived token in a browser bundle or a public client.
|
|
30
50
|
|
|
31
51
|
## Authentication
|
|
@@ -57,7 +77,7 @@ Generic MCP clients (Cursor, Claude Desktop, Claude Code) should use the standar
|
|
|
57
77
|
```ts
|
|
58
78
|
await pr.rooms.list();
|
|
59
79
|
await pr.rooms.get('ab12cd');
|
|
60
|
-
await pr.rooms.create({ name: 'Deploys', icon: 'rocket', color: '#
|
|
80
|
+
await pr.rooms.create({ name: 'Deploys', icon: 'rocket', color: '#e33122' });
|
|
61
81
|
await pr.rooms.join({ invite_code: 'ab12cd' });
|
|
62
82
|
|
|
63
83
|
await pr.actions.update('ab12cd', 1, { label: 'Approve', icon: 'check' });
|
|
@@ -83,6 +103,26 @@ Or a single long-poll:
|
|
|
83
103
|
const { notifications, cursor } = await pr.notifications.wait({ after: lastCursor, timeout: 20 });
|
|
84
104
|
```
|
|
85
105
|
|
|
106
|
+
Read one ping and wait for its acknowledgement through the same namespace:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const ping = await pr.notifications.getNotification(notificationId);
|
|
110
|
+
console.log(ping.action_state?.status); // open | acked | expired
|
|
111
|
+
|
|
112
|
+
const result = await pr.notifications.waitForAcknowledgement(notificationId, { timeout: 30 });
|
|
113
|
+
switch (result.action_state.status) {
|
|
114
|
+
case 'acked':
|
|
115
|
+
console.log(`Acknowledged by ${result.action_state.acked_by?.name ?? 'someone'}`);
|
|
116
|
+
break;
|
|
117
|
+
case 'expired':
|
|
118
|
+
console.log('Nobody acknowledged before the deadline');
|
|
119
|
+
break;
|
|
120
|
+
case 'open':
|
|
121
|
+
// This bounded hold timed out; call waitForAcknowledgement again to continue.
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
86
126
|
## Human-in-the-loop approvals
|
|
87
127
|
|
|
88
128
|
Ask the human you act for to decide, and block until they tap an answer on their phone.
|
|
@@ -100,6 +140,103 @@ if (decided.decision === 'Ship it') {
|
|
|
100
140
|
}
|
|
101
141
|
```
|
|
102
142
|
|
|
143
|
+
## Human-in-the-loop questions
|
|
144
|
+
|
|
145
|
+
A **question** is the general form of an approval: 2–4 options (or a short typed answer), delivered as a push with tappable buttons, resolving to exactly one answer — first tap wins. Reach for `pr.questions` when you need more than yes/no, a typed reply, or want *anyone in the room* to answer.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Ask — omit `options` for a binary Approve/Deny; ttl defaults to 1h (30s–24h).
|
|
149
|
+
const q = await pr.questions.ask('ab12cd', {
|
|
150
|
+
prompt: 'Which environment should I deploy?',
|
|
151
|
+
options: [
|
|
152
|
+
{ value: 'prod', label: 'Production', style: 'primary' },
|
|
153
|
+
{ value: 'staging', label: 'Staging' },
|
|
154
|
+
{ value: 'cancel', label: 'Cancel', style: 'danger' },
|
|
155
|
+
],
|
|
156
|
+
responder_scope: 'room', // or 'direct' (defaults to your bound user)
|
|
157
|
+
ttl: 600,
|
|
158
|
+
correlation_id: 'deploy-1.4.0',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Block until a human taps (or it expires / is cancelled)
|
|
162
|
+
const answered = await pr.questions.waitForAnswer(q.id);
|
|
163
|
+
|
|
164
|
+
switch (answered.state) {
|
|
165
|
+
case 'answered':
|
|
166
|
+
console.log(`${answered.answer.responder?.display_name} chose ${answered.answer.value}`);
|
|
167
|
+
// → answered.answer.value is 'prod' | 'staging' | 'cancel'
|
|
168
|
+
break;
|
|
169
|
+
case 'expired': /* nobody answered in time */ break;
|
|
170
|
+
case 'cancelled': /* the asker withdrew it */ break;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The three outcomes are distinct: an **answered** question whose `answer.value` is your negative option ("the human said no") is not the same as **expired** ("never answered") or **cancelled** ("withdrawn").
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
await pr.questions.get(q.id); // current state (polling / audit)
|
|
178
|
+
await pr.questions.list({ state: 'pending' }); // your open questions
|
|
179
|
+
await pr.questions.cancel(q.id); // withdraw a pending one
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
> `pr.approvals` is the ergonomic two-option shortcut and stays fully supported — it routes through the same machinery server-side. Use it for plain yes/no gates; use `pr.questions` for everything richer.
|
|
183
|
+
|
|
184
|
+
## Handoffs (Agent → one human)
|
|
185
|
+
|
|
186
|
+
A **handoff** is the direct, private version of the human-in-the-loop: it always targets **exactly one human** and is never readable by anyone else in the room. Reach for `pr.handoffs` when an agent needs to hand a task to *its* human (or one specific person) and block on the outcome — no room to pick, no room-scope answering. It's backed by the ack and question primitives, so a handoff is one of two `kind`s:
|
|
187
|
+
|
|
188
|
+
- **`ack`** — "acknowledge this." Resolves the moment the human taps acknowledge. States: `open | acked | expired`.
|
|
189
|
+
- **`question`** — "pick one." 2–4 tappable options. States: `pending | answered | expired | cancelled`.
|
|
190
|
+
|
|
191
|
+
Requires the `pingroom:handoffs:create` scope.
|
|
192
|
+
|
|
193
|
+
On first connect, activate the human's private Agent Inbox. This is idempotent:
|
|
194
|
+
it creates at most one inbox and one onboarding test Question.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const onboarding = await pr.inbox.ensure();
|
|
198
|
+
console.log(onboarding.room.invite_code, onboarding.question.state);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// Ask for an acknowledgement. Pass idempotencyKey so a retried create is a no-op.
|
|
203
|
+
const task = await pr.handoffs.requestAck({
|
|
204
|
+
prompt: 'Deploy 1.4.0 to prod is queued — ack to proceed.',
|
|
205
|
+
idempotencyKey: 'deploy-1.4.0', // reuse verbatim on network retries
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Block until the human acks (or it expires). Terminal states never throw.
|
|
209
|
+
const result = await pr.handoffs.waitForResult(task.id);
|
|
210
|
+
if (result.state === 'acked') {
|
|
211
|
+
console.log(`Acked by ${result.acked_by?.display_name} at ${result.acked_at}`);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// Ask a multi-option question. Bare strings normalize to { value, label: value }.
|
|
217
|
+
const q = await pr.handoffs.ask({
|
|
218
|
+
prompt: 'Ship or hold 1.4.0?',
|
|
219
|
+
options: ['deploy', 'hold'],
|
|
220
|
+
idempotencyKey: 'ship-1.4.0',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const answered = await pr.handoffs.waitForResult(q.id);
|
|
224
|
+
if (answered.state === 'answered') {
|
|
225
|
+
// A negative choice ('hold') is a SUCCESSFUL answered result — not an error.
|
|
226
|
+
console.log(`Human chose ${answered.answer?.value}`);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`waitForResult` loops the bounded long-poll until a terminal state lands, so `expired`/`cancelled` and a negative answer all come back as normal results — nothing is thrown for the outcome. Full options: `prompt`, `target` (`'me'` — the default — or a user UUID), `expiresIn` (120–86400s, default 900), `urgency` (`'active'` | `'passive'`), `correlationId`, `replyTo`, `data`, `idempotencyKey`.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
await pr.handoffs.get(id); // current state (polling / audit)
|
|
234
|
+
await pr.handoffs.list({ state: 'open' }); // your open handoffs
|
|
235
|
+
await pr.handoffs.list({ state: 'all' }); // recent history (up to 200 per kind)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Create/read map coded failures onto `PingRoomError` — branch on `error.code` (see [`HandoffErrorCode`](#errors)). The exported union includes target-policy, scope/quota, idempotency, feature, and capability failures; `capability_check_unavailable` is retryable. Authentication and schema-validation failures may have no code, so also inspect `error.status`.
|
|
239
|
+
|
|
103
240
|
## Incoming webhooks (no token needed)
|
|
104
241
|
|
|
105
242
|
A room's incoming-webhook URL carries its own secret — ideal for CI/deploy hooks:
|
|
@@ -110,21 +247,138 @@ import { sendIncomingWebhook } from '@pingroom/sdk';
|
|
|
110
247
|
await sendIncomingWebhook(process.env.PINGROOM_WEBHOOK_URL, {
|
|
111
248
|
message: 'Build #42 passed',
|
|
112
249
|
data: { commit: 'abc123' },
|
|
250
|
+
requires_ack: true,
|
|
251
|
+
ack_timeout_seconds: 300,
|
|
113
252
|
}, { idempotencyKey: 'build-42' });
|
|
114
253
|
```
|
|
115
254
|
|
|
255
|
+
### Live Activities from a webhook
|
|
256
|
+
|
|
257
|
+
See [Live Activities](#live-activities) below for the full picture. From a webhook, put `live_status` at the **top level** of the payload:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
await sendIncomingWebhook(process.env.PINGROOM_WEBHOOK_URL, {
|
|
261
|
+
message: 'Deploying v1.4.0',
|
|
262
|
+
correlation_id: 'deploy-42',
|
|
263
|
+
live_status: { state: 'running', template: 'progress', progress: 0.4 },
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
> **Not inside `data`.** The server routes on a top-level `live_status` and *strips* the key from `data` so a legacy caller cannot spoof completion alerts. A nested `data.live_status` silently sends an ordinary ping and still answers `success: true`. (SDK versions before 0.3.1 dropped `live_status` from the request body entirely — upgrade.)
|
|
268
|
+
|
|
269
|
+
## Live Activities
|
|
270
|
+
|
|
271
|
+
A **live-status stream** is one thing being tracked, keyed by your `correlation_id`. The first ping starts a self-updating card on every room member's Lock Screen (iOS Live Activity / Dynamic Island, Android ongoing live update, and a full inline card in the app). Later `running` pings move it **silently** — no new notification. The first `done`/`failed` sends one completion alert and ends it.
|
|
272
|
+
|
|
273
|
+
Protocol: <https://pingroom.io/liveactivities.md> (v1.2).
|
|
274
|
+
|
|
275
|
+
### Two ways to send one
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { PingRoom, liveStatus, sendIncomingWebhook } from '@pingroom/sdk';
|
|
279
|
+
|
|
280
|
+
// A. Agent credential — needs the `pingroom:live:write` scope.
|
|
281
|
+
const pr = new PingRoom({ token: process.env.PINGROOM_TOKEN });
|
|
282
|
+
await pr.live.push('AB12CD', liveStatus.progress('deploy-42', {
|
|
283
|
+
state: 'running',
|
|
284
|
+
message: 'Building image',
|
|
285
|
+
progress: 0.4,
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
// B. A room's incoming webhook (Pro) — no token. The builder returns
|
|
289
|
+
// { correlation_id, live_status }, which is exactly the webhook body.
|
|
290
|
+
await sendIncomingWebhook(
|
|
291
|
+
process.env.PINGROOM_WEBHOOK_URL,
|
|
292
|
+
liveStatus.progress('deploy-42', {
|
|
293
|
+
state: 'running',
|
|
294
|
+
message: 'Building image',
|
|
295
|
+
progress: 0.4,
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
There is a third producer — a person driving a stream from the app composer over `POST /api/rooms/{code}/live` with their own JWT — but that is a Human JWT route and is not part of this agent SDK.
|
|
301
|
+
|
|
302
|
+
### Templates
|
|
303
|
+
|
|
304
|
+
`template` is fixed at stream creation; a later ping cannot re-template a running card. Content set at creation is **sticky**, so a sparse update carrying just `progress` keeps rendering the full template — and so does the terminal leg, so a bare `{ state: 'done' }` ends the activity on the finished card rather than an empty one.
|
|
305
|
+
|
|
306
|
+
| Builder | Renders |
|
|
307
|
+
|---|---|
|
|
308
|
+
| `liveStatus.status` | Title, message, optional progress |
|
|
309
|
+
| `liveStatus.steps` | Segmented tracker; `steps` (2–8 labels) required on the first ping and immutable after |
|
|
310
|
+
| `liveStatus.progress` | Determinate bar with optional `eta_at` |
|
|
311
|
+
| `liveStatus.metrics` | Up to 3 `{label, value}` counters |
|
|
312
|
+
| `liveStatus.countdown` | Large live timer to `deadline_at` |
|
|
313
|
+
| `liveStatus.question` | `prompt` + up to 4 `{value, label}` options on the lock screen |
|
|
314
|
+
| `liveStatus.matchup` | `left` / `center` / `right` — a score or A-vs-B |
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
await pr.live.push('AB12CD', liveStatus.steps('release-7', {
|
|
318
|
+
state: 'running',
|
|
319
|
+
steps: ['Build', 'Test', 'Deploy', 'Verify'],
|
|
320
|
+
current_step: 2,
|
|
321
|
+
message: 'Deploying to prod',
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
await pr.live.push('AB12CD', liveStatus.matchup('game-3', {
|
|
325
|
+
state: 'running',
|
|
326
|
+
left: { label: 'ARS', value: '2' },
|
|
327
|
+
right: { label: 'CHE', value: '1' },
|
|
328
|
+
center: "68'",
|
|
329
|
+
}));
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Ending and reading back
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
// Always end a stream. `done`/`failed` is never rate-limited or quota-blocked,
|
|
336
|
+
// so a card can't be metered into hanging open on a lock screen.
|
|
337
|
+
await pr.live.push('AB12CD', liveStatus.done('deploy-42', 'Shipped v1.4.0'));
|
|
338
|
+
// ...or liveStatus.failed('deploy-42', 'Rollback triggered')
|
|
339
|
+
|
|
340
|
+
// Reconcile after a restart instead of opening a duplicate.
|
|
341
|
+
// Returns null (not a throw) when there is no such stream in the last 24 hours.
|
|
342
|
+
const snap = await pr.live.get('AB12CD', 'deploy-42');
|
|
343
|
+
if (snap) console.log(snap.template, snap.current_step, snap.updated_at);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`get()` returns **every** stored field (`state`, `progress`, `message`, `category`, `template`, `agent_id`, `accent_override`, `eta_at`, `deadline_at`, `metrics`, `prompt`, `options`, `left`, `right`, `center`, `steps`, `current_step`) plus `action_state` and `updated_at`. Fields you never set come back as `null`, not omitted.
|
|
347
|
+
|
|
348
|
+
### Ownership and limits
|
|
349
|
+
|
|
350
|
+
A stream belongs to the credential that started it. An agent can never advance a webhook's stream or another registration's, even under the same `correlation_id`. Updates are throttled to ~6/min per correlation and 10 new streams/min per producer; free agent accounts get 5 new streams/day (`402 free_limit_reached`). Creation is the only charged leg.
|
|
351
|
+
|
|
352
|
+
### `liveActivity.*` is not this
|
|
353
|
+
|
|
354
|
+
The exported `liveActivity.*` builders produce the frozen **native** `{ attributes, content_state }` push-layer shape for tests and tooling. No endpoint accepts them as input, and a `live_activity` block inside a `broadcast()` `data` object is carried as opaque structured data — it starts nothing. Use `liveStatus.*` to drive a real activity.
|
|
355
|
+
|
|
116
356
|
## Verifying outgoing webhooks
|
|
117
357
|
|
|
118
|
-
When PingRoom POSTs an event to *your* server, verify it before trusting it.
|
|
358
|
+
When PingRoom POSTs an event to *your* server, verify it before trusting it. New deliveries carry two lower-case hex HMAC-SHA256 headers:
|
|
359
|
+
|
|
360
|
+
- `X-PingRoom-Signature-V2` signs `` `v2\n${timestamp}\n${deliveryId}\n${rawBody}` `` and binds `X-PingRoom-Delivery` as well as the timestamp and body.
|
|
361
|
+
- `X-PingRoom-Signature` keeps the legacy `` `${timestamp}.${rawBody}` `` contract so existing receivers continue to work.
|
|
362
|
+
|
|
363
|
+
`timestamp` is the Unix-seconds value in `X-PingRoom-Timestamp`, and `deliveryId` is the exact `X-PingRoom-Delivery` value. **Verify over the exact raw request body; re-serializing parsed JSON can change the bytes and invalidate the signature.** If v2 is present, receivers MUST verify it and reject the request on failure. The helper accepts legacy v1 only when v2 is absent. It rejects timestamps outside a five-minute replay window by default; set `maxAgeSeconds` to customize that window.
|
|
119
364
|
|
|
120
365
|
```ts
|
|
121
|
-
import {
|
|
366
|
+
import {
|
|
367
|
+
verifyWebhookSignature,
|
|
368
|
+
WEBHOOK_SIGNATURE_HEADER,
|
|
369
|
+
WEBHOOK_SIGNATURE_V2_HEADER,
|
|
370
|
+
WEBHOOK_TIMESTAMP_HEADER,
|
|
371
|
+
WEBHOOK_DELIVERY_HEADER,
|
|
372
|
+
} from '@pingroom/sdk';
|
|
122
373
|
|
|
123
374
|
// Express example — capture the raw body (e.g. express.raw())
|
|
124
375
|
app.post('/pingroom', async (req, res) => {
|
|
125
376
|
const ok = await verifyWebhookSignature({
|
|
126
377
|
payload: req.body, // a Buffer/string of the RAW body
|
|
127
378
|
signature: req.get(WEBHOOK_SIGNATURE_HEADER),
|
|
379
|
+
signatureV2: req.get(WEBHOOK_SIGNATURE_V2_HEADER),
|
|
380
|
+
timestamp: req.get(WEBHOOK_TIMESTAMP_HEADER),
|
|
381
|
+
deliveryId: req.get(WEBHOOK_DELIVERY_HEADER),
|
|
128
382
|
secret: process.env.PINGROOM_SIGNING_SECRET,
|
|
129
383
|
});
|
|
130
384
|
if (!ok) return res.status(401).end();
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from './http.js';
|
|
2
|
+
import type { LiveStatusPing, LiveStatusResult, LiveStatusSnapshot } from './liveStatus.js';
|
|
2
3
|
import { McpClient } from './mcp.js';
|
|
3
|
-
import type { AgentNotification, Approval, ApprovalInput, CreatePublicRoomInput, CreateRoomInput, Credential, ClaimCompleteParams, ClaimStartParams, DirectoryEntry, DirectoryListInput, DirectoryProfile, DirectPingResult, JoinRoomInput, ListNotificationsInput, ListenInput, Paginated, PingInput, PingResult, PingRoomOptions, QuickAction, RegisterParams, Room, RotateHandleResult, TriggerInput, UpdateQuickActionInput, WaitApprovalInput, WaitInput, WaitResult } from './types.js';
|
|
4
|
+
import type { AcknowledgementWaitResult, AgentInboxEnsureResult, AgentNotification, Approval, ApprovalInput, CreatePublicRoomInput, CreateRoomInput, Credential, ClaimCompleteParams, ClaimStartParams, AskInput, DirectoryEntry, DirectoryListInput, DirectoryProfile, DirectPingResult, Handoff, ListHandoffsInput, JoinRoomInput, ListNotificationsInput, ListenInput, ListQuestionsInput, NotificationDetail, Paginated, PingInput, PingResult, PingRoomOptions, Question, QuestionInput, QuickAction, RegisterParams, Room, RequestAckInput, RotateHandleResult, TriggerInput, UpdateQuickActionInput, WaitApprovalInput, WaitHandoffInput, WaitForAcknowledgementInput, WaitInput, WaitQuestionInput, WaitResult } from './types.js';
|
|
4
5
|
/** Agent credential lifecycle (auth.md flow). */
|
|
5
6
|
declare class AuthApi {
|
|
6
7
|
private readonly http;
|
|
@@ -40,6 +41,13 @@ declare class NotificationsApi {
|
|
|
40
41
|
constructor(http: HttpClient);
|
|
41
42
|
/** Paginated list (polling fallback). Prefer wait()/listen() for real-time. */
|
|
42
43
|
list(input?: ListNotificationsInput): Promise<Paginated<AgentNotification>>;
|
|
44
|
+
/** Fetch one visible notification, including its current acknowledgement state. */
|
|
45
|
+
getNotification(notificationId: string): Promise<NotificationDetail>;
|
|
46
|
+
/**
|
|
47
|
+
* One bounded long-poll for an ack-required ping. The returned state may
|
|
48
|
+
* still be `open` when the hold timeout elapses; call again to keep waiting.
|
|
49
|
+
*/
|
|
50
|
+
waitForAcknowledgement(notificationId: string, input?: WaitForAcknowledgementInput): Promise<AcknowledgementWaitResult>;
|
|
43
51
|
/** One long-poll: resolves as soon as new pings land, or with an empty list at timeout. */
|
|
44
52
|
wait(input?: WaitInput): Promise<WaitResult>;
|
|
45
53
|
/**
|
|
@@ -53,7 +61,11 @@ declare class NotificationsApi {
|
|
|
53
61
|
declare class AgentsApi {
|
|
54
62
|
private readonly http;
|
|
55
63
|
constructor(http: HttpClient);
|
|
56
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated Retired server-side — always fails with 410
|
|
66
|
+
* `cross_account_ping_retired`. An agent can only reach the account that
|
|
67
|
+
* connected it; to reach anyone else, post into a room you both belong to.
|
|
68
|
+
*/
|
|
57
69
|
ping(handle: string, ping: PingInput): Promise<DirectPingResult>;
|
|
58
70
|
}
|
|
59
71
|
declare class ApprovalsApi {
|
|
@@ -69,6 +81,77 @@ declare class ApprovalsApi {
|
|
|
69
81
|
signal?: AbortSignal;
|
|
70
82
|
}): Promise<Approval>;
|
|
71
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The general human-in-the-loop primitive: ask a human a question with 2–4
|
|
86
|
+
* options (or a short typed answer) and block until they tap an answer. An
|
|
87
|
+
* {@link ApprovalsApi | approval} is the two-option case; reach for questions
|
|
88
|
+
* when you need more choices, a typed reply, or room-scope answering.
|
|
89
|
+
*/
|
|
90
|
+
declare class QuestionsApi {
|
|
91
|
+
private readonly http;
|
|
92
|
+
constructor(http: HttpClient);
|
|
93
|
+
/** Ask a Question in a room; returns the pending Question (id + computed expiry). */
|
|
94
|
+
ask(inviteCode: string, input: QuestionInput): Promise<Question>;
|
|
95
|
+
/** Fetch a Question's current state. */
|
|
96
|
+
get(questionId: string): Promise<Question>;
|
|
97
|
+
/** One long-poll for the answer; returns the (possibly still-pending) Question. */
|
|
98
|
+
wait(questionId: string, input?: WaitQuestionInput): Promise<Question>;
|
|
99
|
+
/**
|
|
100
|
+
* Block until the human answers (or the question expires/cancels), looping
|
|
101
|
+
* wait() under the hood. Returns the resolved Question — inspect `state` and
|
|
102
|
+
* `answer.value` to branch.
|
|
103
|
+
*/
|
|
104
|
+
waitForAnswer(questionId: string, input?: {
|
|
105
|
+
signal?: AbortSignal;
|
|
106
|
+
}): Promise<Question>;
|
|
107
|
+
/** List the asker's Questions, optionally filtered by state. */
|
|
108
|
+
list(input?: ListQuestionsInput): Promise<Question[]>;
|
|
109
|
+
/** Withdraw a still-pending Question (409 if it already resolved). */
|
|
110
|
+
cancel(questionId: string): Promise<Question>;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The Handoff façade: the Agent → one-human loop. Always DIRECT (exactly one
|
|
114
|
+
* human) and PRIVATE (no other room member can read it). Backed by the ack and
|
|
115
|
+
* question primitives, so its {@link Handoff.state | state} space is their union
|
|
116
|
+
* — kept DISTINCT per {@link Handoff.kind | kind}. A negative answer to a
|
|
117
|
+
* question handoff (e.g. `hold`) is a SUCCESSFUL `answered` result, not an error.
|
|
118
|
+
*
|
|
119
|
+
* const task = await pr.handoffs.requestAck({ prompt, idempotencyKey });
|
|
120
|
+
* const result = await pr.handoffs.waitForResult(task.id);
|
|
121
|
+
*
|
|
122
|
+
* const q = await pr.handoffs.ask({ prompt, options: ['deploy', 'hold'], idempotencyKey });
|
|
123
|
+
*/
|
|
124
|
+
declare class HandoffsApi {
|
|
125
|
+
private readonly http;
|
|
126
|
+
constructor(http: HttpClient);
|
|
127
|
+
/** Ask a human to acknowledge (an `ack` handoff). Resolves on the first acknowledgement. */
|
|
128
|
+
requestAck(input: RequestAckInput): Promise<Handoff>;
|
|
129
|
+
/**
|
|
130
|
+
* Ask a human a multi-option question (a `question` handoff). Resolves on the
|
|
131
|
+
* first tapped answer. Bare-string options normalize to `{ value, label }`.
|
|
132
|
+
*/
|
|
133
|
+
ask(input: AskInput): Promise<Handoff>;
|
|
134
|
+
/** Fetch a handoff's current state. */
|
|
135
|
+
get(id: string): Promise<Handoff>;
|
|
136
|
+
/**
|
|
137
|
+
* Block until the handoff resolves (or expires/cancels), looping the bounded
|
|
138
|
+
* long-poll under the hood. `expired`/`cancelled` and a negative answer (e.g.
|
|
139
|
+
* `hold`) all surface as normal terminal results — nothing is thrown for them.
|
|
140
|
+
*/
|
|
141
|
+
waitForResult(id: string, input?: WaitHandoffInput): Promise<Handoff>;
|
|
142
|
+
/** List the agent's open handoffs or bounded recent history. */
|
|
143
|
+
list(input?: ListHandoffsInput): Promise<Handoff[]>;
|
|
144
|
+
/** One bounded long-poll; returns the (possibly still-open) handoff. */
|
|
145
|
+
private wait;
|
|
146
|
+
private create;
|
|
147
|
+
}
|
|
148
|
+
/** One-call activation of the bound human's private Agent Inbox. */
|
|
149
|
+
declare class InboxApi {
|
|
150
|
+
private readonly http;
|
|
151
|
+
constructor(http: HttpClient);
|
|
152
|
+
/** Idempotently create/resolve the inbox and deliver its one-time test Question. */
|
|
153
|
+
ensure(): Promise<AgentInboxEnsureResult>;
|
|
154
|
+
}
|
|
72
155
|
declare class ProfileApi {
|
|
73
156
|
private readonly http;
|
|
74
157
|
constructor(http: HttpClient);
|
|
@@ -77,6 +160,28 @@ declare class ProfileApi {
|
|
|
77
160
|
/** Rotate the agent's handle — the kill-switch for a leaked handle. */
|
|
78
161
|
rotateHandle(): Promise<RotateHandleResult>;
|
|
79
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Live-status streams — a self-updating card on the room members' lock screen.
|
|
165
|
+
*
|
|
166
|
+
* Requires the `pingroom:live:write` scope. Free accounts get a small daily
|
|
167
|
+
* budget of NEW streams; updates and the terminal ping are never charged, so a
|
|
168
|
+
* card can never be quota-blocked into hanging open.
|
|
169
|
+
*/
|
|
170
|
+
declare class LiveApi {
|
|
171
|
+
private readonly http;
|
|
172
|
+
constructor(http: HttpClient);
|
|
173
|
+
/**
|
|
174
|
+
* Send one stream ping — start, update, or end, chosen by `live_status.state`
|
|
175
|
+
* and whether the correlation already exists.
|
|
176
|
+
*/
|
|
177
|
+
push(inviteCode: string, ping: LiveStatusPing): Promise<LiveStatusResult>;
|
|
178
|
+
/**
|
|
179
|
+
* Read back a stream this credential started, so a restarted producer can
|
|
180
|
+
* reconcile instead of opening a duplicate. Returns null when there is no
|
|
181
|
+
* such stream in the last 24 hours.
|
|
182
|
+
*/
|
|
183
|
+
get(inviteCode: string, correlationId: string): Promise<LiveStatusSnapshot | null>;
|
|
184
|
+
}
|
|
80
185
|
/** Public, unauthenticated agent directory. */
|
|
81
186
|
declare class DirectoryApi {
|
|
82
187
|
private readonly http;
|
|
@@ -91,6 +196,10 @@ export declare class PingRoom {
|
|
|
91
196
|
readonly notifications: NotificationsApi;
|
|
92
197
|
readonly agents: AgentsApi;
|
|
93
198
|
readonly approvals: ApprovalsApi;
|
|
199
|
+
readonly questions: QuestionsApi;
|
|
200
|
+
readonly handoffs: HandoffsApi;
|
|
201
|
+
readonly inbox: InboxApi;
|
|
202
|
+
readonly live: LiveApi;
|
|
94
203
|
readonly profile: ProfileApi;
|
|
95
204
|
readonly directory: DirectoryApi;
|
|
96
205
|
readonly mcp: McpClient;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|