@pingroom/sdk 0.2.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 +217 -4
- package/dist/client.d.ts +82 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +181 -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 +183 -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
|
@@ -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.
|
|
@@ -141,6 +181,62 @@ await pr.questions.cancel(q.id); // withdraw a pending one
|
|
|
141
181
|
|
|
142
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.
|
|
143
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
|
+
|
|
144
240
|
## Incoming webhooks (no token needed)
|
|
145
241
|
|
|
146
242
|
A room's incoming-webhook URL carries its own secret — ideal for CI/deploy hooks:
|
|
@@ -151,21 +247,138 @@ import { sendIncomingWebhook } from '@pingroom/sdk';
|
|
|
151
247
|
await sendIncomingWebhook(process.env.PINGROOM_WEBHOOK_URL, {
|
|
152
248
|
message: 'Build #42 passed',
|
|
153
249
|
data: { commit: 'abc123' },
|
|
250
|
+
requires_ack: true,
|
|
251
|
+
ack_timeout_seconds: 300,
|
|
154
252
|
}, { idempotencyKey: 'build-42' });
|
|
155
253
|
```
|
|
156
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
|
+
|
|
157
356
|
## Verifying outgoing webhooks
|
|
158
357
|
|
|
159
|
-
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.
|
|
160
364
|
|
|
161
365
|
```ts
|
|
162
|
-
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';
|
|
163
373
|
|
|
164
374
|
// Express example — capture the raw body (e.g. express.raw())
|
|
165
375
|
app.post('/pingroom', async (req, res) => {
|
|
166
376
|
const ok = await verifyWebhookSignature({
|
|
167
377
|
payload: req.body, // a Buffer/string of the RAW body
|
|
168
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),
|
|
169
382
|
secret: process.env.PINGROOM_SIGNING_SECRET,
|
|
170
383
|
});
|
|
171
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, ListQuestionsInput, Paginated, PingInput, PingResult, PingRoomOptions, Question, QuestionInput, QuickAction, RegisterParams, Room, RotateHandleResult, TriggerInput, UpdateQuickActionInput, WaitApprovalInput, WaitInput, WaitQuestionInput, 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 {
|
|
@@ -97,6 +109,49 @@ declare class QuestionsApi {
|
|
|
97
109
|
/** Withdraw a still-pending Question (409 if it already resolved). */
|
|
98
110
|
cancel(questionId: string): Promise<Question>;
|
|
99
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
|
+
}
|
|
100
155
|
declare class ProfileApi {
|
|
101
156
|
private readonly http;
|
|
102
157
|
constructor(http: HttpClient);
|
|
@@ -105,6 +160,28 @@ declare class ProfileApi {
|
|
|
105
160
|
/** Rotate the agent's handle — the kill-switch for a leaked handle. */
|
|
106
161
|
rotateHandle(): Promise<RotateHandleResult>;
|
|
107
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
|
+
}
|
|
108
185
|
/** Public, unauthenticated agent directory. */
|
|
109
186
|
declare class DirectoryApi {
|
|
110
187
|
private readonly http;
|
|
@@ -120,6 +197,9 @@ export declare class PingRoom {
|
|
|
120
197
|
readonly agents: AgentsApi;
|
|
121
198
|
readonly approvals: ApprovalsApi;
|
|
122
199
|
readonly questions: QuestionsApi;
|
|
200
|
+
readonly handoffs: HandoffsApi;
|
|
201
|
+
readonly inbox: InboxApi;
|
|
202
|
+
readonly live: LiveApi;
|
|
123
203
|
readonly profile: ProfileApi;
|
|
124
204
|
readonly directory: DirectoryApi;
|
|
125
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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"}
|
package/dist/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PingRoomError } from './errors.js';
|
|
1
2
|
import { HttpClient } from './http.js';
|
|
2
3
|
import { sleep } from './internal/async.js';
|
|
3
4
|
import { assertActionNumber, assertStructuredData, requireNonEmpty } from './internal/guards.js';
|
|
@@ -119,6 +120,23 @@ class NotificationsApi {
|
|
|
119
120
|
list(input = {}) {
|
|
120
121
|
return this.http.request('GET', '/api/agent/notifications', { query: dropUndefined({ ...input }) });
|
|
121
122
|
}
|
|
123
|
+
/** Fetch one visible notification, including its current acknowledgement state. */
|
|
124
|
+
getNotification(notificationId) {
|
|
125
|
+
requireNonEmpty(notificationId, 'notificationId');
|
|
126
|
+
return this.http.request('GET', `/api/agent/notifications/${enc(notificationId)}`);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* One bounded long-poll for an ack-required ping. The returned state may
|
|
130
|
+
* still be `open` when the hold timeout elapses; call again to keep waiting.
|
|
131
|
+
*/
|
|
132
|
+
waitForAcknowledgement(notificationId, input = {}) {
|
|
133
|
+
requireNonEmpty(notificationId, 'notificationId');
|
|
134
|
+
return this.http.request('GET', `/api/agent/notifications/${enc(notificationId)}/ack/wait`, {
|
|
135
|
+
query: dropUndefined({ timeout: input.timeout }),
|
|
136
|
+
timeoutMs: waitTimeoutMs(input.timeout),
|
|
137
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
122
140
|
/** One long-poll: resolves as soon as new pings land, or with an empty list at timeout. */
|
|
123
141
|
wait(input = {}) {
|
|
124
142
|
return this.http.request('GET', '/api/agent/notifications/wait', {
|
|
@@ -176,7 +194,11 @@ class AgentsApi {
|
|
|
176
194
|
constructor(http) {
|
|
177
195
|
this.http = http;
|
|
178
196
|
}
|
|
179
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* @deprecated Retired server-side — always fails with 410
|
|
199
|
+
* `cross_account_ping_retired`. An agent can only reach the account that
|
|
200
|
+
* connected it; to reach anyone else, post into a room you both belong to.
|
|
201
|
+
*/
|
|
180
202
|
ping(handle, ping) {
|
|
181
203
|
requireNonEmpty(handle, 'handle');
|
|
182
204
|
assertPing(ping);
|
|
@@ -279,6 +301,118 @@ class QuestionsApi {
|
|
|
279
301
|
return this.http.request('POST', `/api/agent/questions/${enc(questionId)}/cancel`, { body: {} });
|
|
280
302
|
}
|
|
281
303
|
}
|
|
304
|
+
/** A bare string option is shorthand for `{ value, label: value }`. */
|
|
305
|
+
function normalizeHandoffOptions(options) {
|
|
306
|
+
return options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
|
307
|
+
}
|
|
308
|
+
/** Terminal states never change again — the wait loop can stop as soon as one lands. */
|
|
309
|
+
function isHandoffTerminal(handoff) {
|
|
310
|
+
const s = handoff.state;
|
|
311
|
+
return s === 'acked' || s === 'answered' || s === 'expired' || s === 'cancelled';
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The Handoff façade: the Agent → one-human loop. Always DIRECT (exactly one
|
|
315
|
+
* human) and PRIVATE (no other room member can read it). Backed by the ack and
|
|
316
|
+
* question primitives, so its {@link Handoff.state | state} space is their union
|
|
317
|
+
* — kept DISTINCT per {@link Handoff.kind | kind}. A negative answer to a
|
|
318
|
+
* question handoff (e.g. `hold`) is a SUCCESSFUL `answered` result, not an error.
|
|
319
|
+
*
|
|
320
|
+
* const task = await pr.handoffs.requestAck({ prompt, idempotencyKey });
|
|
321
|
+
* const result = await pr.handoffs.waitForResult(task.id);
|
|
322
|
+
*
|
|
323
|
+
* const q = await pr.handoffs.ask({ prompt, options: ['deploy', 'hold'], idempotencyKey });
|
|
324
|
+
*/
|
|
325
|
+
class HandoffsApi {
|
|
326
|
+
http;
|
|
327
|
+
constructor(http) {
|
|
328
|
+
this.http = http;
|
|
329
|
+
}
|
|
330
|
+
/** Ask a human to acknowledge (an `ack` handoff). Resolves on the first acknowledgement. */
|
|
331
|
+
requestAck(input) {
|
|
332
|
+
requireNonEmpty(input.prompt, 'prompt');
|
|
333
|
+
assertStructuredData(input.data);
|
|
334
|
+
return this.create('ack', input);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Ask a human a multi-option question (a `question` handoff). Resolves on the
|
|
338
|
+
* first tapped answer. Bare-string options normalize to `{ value, label }`.
|
|
339
|
+
*/
|
|
340
|
+
ask(input) {
|
|
341
|
+
requireNonEmpty(input.prompt, 'prompt');
|
|
342
|
+
assertStructuredData(input.data);
|
|
343
|
+
if (!Array.isArray(input.options) || input.options.length < 2 || input.options.length > 4) {
|
|
344
|
+
throw new PingRoomError('`options` must contain between 2 and 4 entries for a question handoff.', {
|
|
345
|
+
code: 'invalid_request',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return this.create('question', input, normalizeHandoffOptions(input.options));
|
|
349
|
+
}
|
|
350
|
+
/** Fetch a handoff's current state. */
|
|
351
|
+
get(id) {
|
|
352
|
+
requireNonEmpty(id, 'id');
|
|
353
|
+
return this.http.request('GET', `/api/agent/handoffs/${enc(id)}`);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Block until the handoff resolves (or expires/cancels), looping the bounded
|
|
357
|
+
* long-poll under the hood. `expired`/`cancelled` and a negative answer (e.g.
|
|
358
|
+
* `hold`) all surface as normal terminal results — nothing is thrown for them.
|
|
359
|
+
*/
|
|
360
|
+
async waitForResult(id, input = {}) {
|
|
361
|
+
requireNonEmpty(id, 'id');
|
|
362
|
+
for (;;) {
|
|
363
|
+
if (input.signal?.aborted) {
|
|
364
|
+
throw input.signal.reason ?? new Error('aborted');
|
|
365
|
+
}
|
|
366
|
+
const handoff = await this.wait(id, input);
|
|
367
|
+
if (isHandoffTerminal(handoff)) {
|
|
368
|
+
return handoff;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** List the agent's open handoffs or bounded recent history. */
|
|
373
|
+
async list(input = {}) {
|
|
374
|
+
const res = await this.http.request('GET', '/api/agent/handoffs', {
|
|
375
|
+
query: dropUndefined({ state: input.state }),
|
|
376
|
+
});
|
|
377
|
+
return res.handoffs ?? [];
|
|
378
|
+
}
|
|
379
|
+
/** One bounded long-poll; returns the (possibly still-open) handoff. */
|
|
380
|
+
wait(id, input) {
|
|
381
|
+
return this.http.request('GET', `/api/agent/handoffs/${enc(id)}/wait`, {
|
|
382
|
+
query: dropUndefined({ timeout: input.timeout }),
|
|
383
|
+
timeoutMs: waitTimeoutMs(input.timeout),
|
|
384
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
create(kind, input, options) {
|
|
388
|
+
const body = dropUndefined({
|
|
389
|
+
kind,
|
|
390
|
+
prompt: input.prompt,
|
|
391
|
+
audience: { type: 'direct', user_id: input.target ?? 'me' },
|
|
392
|
+
expires_in: input.expiresIn,
|
|
393
|
+
urgency: input.urgency,
|
|
394
|
+
options,
|
|
395
|
+
correlation_id: input.correlationId,
|
|
396
|
+
reply_to: input.replyTo,
|
|
397
|
+
data: input.data,
|
|
398
|
+
});
|
|
399
|
+
return this.http.request('POST', '/api/agent/handoffs', {
|
|
400
|
+
body,
|
|
401
|
+
...(input.idempotencyKey ? { headers: { 'Idempotency-Key': input.idempotencyKey } } : {}),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/** One-call activation of the bound human's private Agent Inbox. */
|
|
406
|
+
class InboxApi {
|
|
407
|
+
http;
|
|
408
|
+
constructor(http) {
|
|
409
|
+
this.http = http;
|
|
410
|
+
}
|
|
411
|
+
/** Idempotently create/resolve the inbox and deliver its one-time test Question. */
|
|
412
|
+
ensure() {
|
|
413
|
+
return this.http.request('POST', '/api/agent/inbox/ensure', { body: {} });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
282
416
|
class ProfileApi {
|
|
283
417
|
http;
|
|
284
418
|
constructor(http) {
|
|
@@ -294,6 +428,46 @@ class ProfileApi {
|
|
|
294
428
|
return this.http.request('POST', '/api/agent/profile/handle/rotate', { body: {} });
|
|
295
429
|
}
|
|
296
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Live-status streams — a self-updating card on the room members' lock screen.
|
|
433
|
+
*
|
|
434
|
+
* Requires the `pingroom:live:write` scope. Free accounts get a small daily
|
|
435
|
+
* budget of NEW streams; updates and the terminal ping are never charged, so a
|
|
436
|
+
* card can never be quota-blocked into hanging open.
|
|
437
|
+
*/
|
|
438
|
+
class LiveApi {
|
|
439
|
+
http;
|
|
440
|
+
constructor(http) {
|
|
441
|
+
this.http = http;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Send one stream ping — start, update, or end, chosen by `live_status.state`
|
|
445
|
+
* and whether the correlation already exists.
|
|
446
|
+
*/
|
|
447
|
+
push(inviteCode, ping) {
|
|
448
|
+
requireNonEmpty(inviteCode, 'inviteCode');
|
|
449
|
+
requireNonEmpty(ping.correlation_id, 'correlation_id');
|
|
450
|
+
assertStructuredData(ping.data);
|
|
451
|
+
return this.http.request('POST', `/api/agent/rooms/${enc(inviteCode)}/live`, { body: ping });
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Read back a stream this credential started, so a restarted producer can
|
|
455
|
+
* reconcile instead of opening a duplicate. Returns null when there is no
|
|
456
|
+
* such stream in the last 24 hours.
|
|
457
|
+
*/
|
|
458
|
+
async get(inviteCode, correlationId) {
|
|
459
|
+
requireNonEmpty(inviteCode, 'inviteCode');
|
|
460
|
+
requireNonEmpty(correlationId, 'correlationId');
|
|
461
|
+
try {
|
|
462
|
+
return await this.http.request('GET', `/api/agent/rooms/${enc(inviteCode)}/live/${enc(correlationId)}`);
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (error instanceof PingRoomError && error.status === 404)
|
|
466
|
+
return null;
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
297
471
|
/** Public, unauthenticated agent directory. */
|
|
298
472
|
class DirectoryApi {
|
|
299
473
|
http;
|
|
@@ -319,6 +493,9 @@ export class PingRoom {
|
|
|
319
493
|
agents;
|
|
320
494
|
approvals;
|
|
321
495
|
questions;
|
|
496
|
+
handoffs;
|
|
497
|
+
inbox;
|
|
498
|
+
live;
|
|
322
499
|
profile;
|
|
323
500
|
directory;
|
|
324
501
|
mcp;
|
|
@@ -340,6 +517,9 @@ export class PingRoom {
|
|
|
340
517
|
this.agents = new AgentsApi(this.http);
|
|
341
518
|
this.approvals = new ApprovalsApi(this.http);
|
|
342
519
|
this.questions = new QuestionsApi(this.http);
|
|
520
|
+
this.handoffs = new HandoffsApi(this.http);
|
|
521
|
+
this.inbox = new InboxApi(this.http);
|
|
522
|
+
this.live = new LiveApi(this.http);
|
|
343
523
|
this.profile = new ProfileApi(this.http);
|
|
344
524
|
this.directory = new DirectoryApi(this.http);
|
|
345
525
|
this.mcp = new McpClient(this.http);
|