@pingagent/sdk 0.1.7 → 0.1.9

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/src/client.ts DELETED
@@ -1,582 +0,0 @@
1
- import type { Envelope, ApiResponse, TaskState, InboxBoxType } from '@pingagent/schemas';
2
- import { SCHEMA_TASK, SCHEMA_CONTACT_REQUEST, SCHEMA_RESULT, SCHEMA_RECEIPT, SCHEMA_TEXT } from '@pingagent/schemas';
3
- import {
4
- type Identity,
5
- buildUnsignedEnvelope,
6
- signEnvelope,
7
- generateArtifactRef,
8
- } from '@pingagent/protocol';
9
- import { HttpTransport } from './transport.js';
10
- import type { LocalStore } from './store.js';
11
- import { ContactManager } from './contacts.js';
12
- import { HistoryManager, type StoredMessage } from './history.js';
13
-
14
- export interface ClientOptions {
15
- serverUrl: string;
16
- identity: Identity;
17
- accessToken: string;
18
- mode?: 'long-running' | 'one-shot';
19
- onTokenRefreshed?: (token: string, expiresAt: number) => void;
20
- store?: LocalStore;
21
- }
22
-
23
- export interface SendResponse {
24
- message_id: string;
25
- seq: number;
26
- receipt: { status: string; reason?: string; server_ts_ms: number };
27
- }
28
-
29
- export interface FetchResponse {
30
- messages: any[];
31
- next_since_seq: number;
32
- has_more: boolean;
33
- }
34
-
35
- export interface TaskResult {
36
- status: 'ok' | 'error';
37
- task_id: string;
38
- result?: { summary?: string; artifacts?: any[]; structured_data?: any };
39
- error?: { code: string; message: string };
40
- elapsed_ms: number;
41
- }
42
-
43
- export interface SubscriptionUsage {
44
- relay_today: number;
45
- relay_limit: number;
46
- artifact_bytes: number;
47
- artifact_limit_bytes: number;
48
- alias_count: number;
49
- alias_limit: number;
50
- }
51
-
52
- export interface SubscriptionResponse {
53
- tier: string;
54
- limits: { relay_per_day: number; max_group_size: number; artifact_storage_mb: number; audit_export_allowed: boolean; alias_limit: number };
55
- usage: SubscriptionUsage;
56
- has_stripe_customer: boolean;
57
- }
58
-
59
- export interface ConversationEntry {
60
- conversation_id: string;
61
- type: string;
62
- target_did: string;
63
- trusted: boolean;
64
- created_at: number;
65
- /** Server-side last message write time (ms). Omitted or null when unknown; client should fetch when missing. */
66
- last_activity_at?: number | null;
67
- }
68
-
69
- export interface ConversationListResponse {
70
- conversations: ConversationEntry[];
71
- }
72
-
73
- export interface AgentProfile {
74
- did: string;
75
- alias?: string;
76
- display_name?: string;
77
- bio?: string;
78
- capabilities?: string[];
79
- tags?: string[];
80
- verification_status?: string;
81
- discoverable?: boolean;
82
- }
83
-
84
- export interface DirectoryBrowseResponse {
85
- agents: AgentProfile[];
86
- total?: number;
87
- }
88
-
89
- export interface FeedPost {
90
- post_id: string;
91
- text: string;
92
- artifact_ref?: string | null;
93
- ts_ms: number;
94
- }
95
-
96
- export interface FeedPublicResponse {
97
- posts: Array<FeedPost & { did: string }>;
98
- next_since?: number;
99
- }
100
-
101
- export interface FeedByDidResponse {
102
- did: string;
103
- posts: FeedPost[];
104
- }
105
-
106
- export class PingAgentClient {
107
- private transport: HttpTransport;
108
- private identity: Identity;
109
- private contactManager?: ContactManager;
110
- private historyManager?: HistoryManager;
111
-
112
- constructor(private opts: ClientOptions) {
113
- this.identity = opts.identity;
114
- this.transport = new HttpTransport({
115
- serverUrl: opts.serverUrl,
116
- accessToken: opts.accessToken,
117
- onTokenRefreshed: opts.onTokenRefreshed,
118
- });
119
- if (opts.store) {
120
- this.contactManager = new ContactManager(opts.store);
121
- this.historyManager = new HistoryManager(opts.store);
122
- }
123
- }
124
-
125
- getContactManager(): ContactManager | undefined { return this.contactManager; }
126
- getHistoryManager(): HistoryManager | undefined { return this.historyManager; }
127
-
128
- /** Update the in-memory access token (e.g. after proactive refresh from disk). */
129
- setAccessToken(token: string): void {
130
- this.transport.setToken(token);
131
- }
132
-
133
- async register(developerToken?: string): Promise<ApiResponse> {
134
- let binary = '';
135
- for (const b of this.identity.publicKey) binary += String.fromCharCode(b);
136
- const publicKeyBase64 = btoa(binary);
137
-
138
- return this.transport.request('POST', '/v1/agent/register', {
139
- device_id: this.identity.deviceId,
140
- public_key: publicKeyBase64,
141
- developer_token: developerToken,
142
- }, true);
143
- }
144
-
145
- async openConversation(targetDid: string): Promise<ApiResponse<{
146
- conversation_id: string;
147
- type: string;
148
- trusted: boolean;
149
- relay_ws_url: string;
150
- }>> {
151
- return this.transport.request('POST', '/v1/conversations/open', {
152
- targets: [targetDid],
153
- });
154
- }
155
-
156
- async sendMessage(
157
- conversationId: string,
158
- schema: string,
159
- payload: Record<string, unknown>,
160
- ): Promise<ApiResponse<SendResponse>> {
161
- const ttlMs = schema === SCHEMA_TEXT ? 604_800_000 : undefined; // 7 days for chat, default 24h for others
162
- const unsigned = buildUnsignedEnvelope({
163
- type: 'message',
164
- conversationId,
165
- senderDid: this.identity.did,
166
- senderDeviceId: this.identity.deviceId,
167
- schema,
168
- payload,
169
- ttlMs,
170
- });
171
- const signed = signEnvelope(unsigned, this.identity.privateKey);
172
- const res = await this.transport.request<SendResponse>('POST', '/v1/messages/send', signed);
173
-
174
- if (res.ok && this.historyManager) {
175
- this.historyManager.save([{
176
- conversation_id: conversationId,
177
- message_id: signed.message_id,
178
- seq: res.data?.seq,
179
- sender_did: this.identity.did,
180
- schema,
181
- payload,
182
- ts_ms: signed.ts_ms,
183
- direction: 'sent',
184
- }]);
185
- }
186
-
187
- return res;
188
- }
189
-
190
- async sendTask(
191
- conversationId: string,
192
- task: { task_id: string; title: string; description?: string; input?: any; timeout_ms?: number },
193
- ): Promise<ApiResponse<SendResponse>> {
194
- return this.sendMessage(conversationId, SCHEMA_TASK, {
195
- ...task,
196
- idempotency_key: task.task_id,
197
- expected_output_schema: SCHEMA_RESULT,
198
- });
199
- }
200
-
201
- async sendContactRequest(
202
- conversationId: string,
203
- message?: string,
204
- ): Promise<ApiResponse<SendResponse>> {
205
- const { v7: uuidv7 } = await import('uuid');
206
- return this.sendMessage(conversationId, SCHEMA_CONTACT_REQUEST, {
207
- request_id: `r_${uuidv7()}`,
208
- from_did: this.identity.did,
209
- to_did: '',
210
- capabilities: ['task', 'result', 'files'],
211
- message,
212
- expires_ms: 86400000,
213
- });
214
- }
215
-
216
- async fetchInbox(
217
- conversationId: string,
218
- opts?: { sinceSeq?: number; limit?: number; box?: InboxBoxType },
219
- ): Promise<ApiResponse<FetchResponse>> {
220
- const params = new URLSearchParams({
221
- conversation_id: conversationId,
222
- since_seq: String(opts?.sinceSeq ?? 0),
223
- limit: String(opts?.limit ?? 50),
224
- box: opts?.box ?? 'ready',
225
- });
226
- const res = await this.transport.request<FetchResponse>('GET', `/v1/inbox/fetch?${params}`);
227
-
228
- if (res.ok && res.data && this.historyManager) {
229
- const msgs: StoredMessage[] = res.data.messages.map((msg: any) => ({
230
- conversation_id: conversationId,
231
- message_id: msg.message_id,
232
- seq: msg.seq,
233
- sender_did: msg.sender_did,
234
- schema: msg.schema,
235
- payload: msg.payload,
236
- ts_ms: msg.ts_ms,
237
- direction: msg.sender_did === this.identity.did ? 'sent' as const : 'received' as const,
238
- }));
239
- if (msgs.length > 0) {
240
- this.historyManager.save(msgs);
241
- const maxSeq = Math.max(...msgs.map(m => m.seq ?? 0));
242
- if (maxSeq > 0) this.historyManager.setLastSyncedSeq(conversationId, maxSeq);
243
- }
244
- }
245
-
246
- return res;
247
- }
248
-
249
- async ack(
250
- conversationId: string,
251
- forMessageId: string,
252
- status: string,
253
- opts?: { forTaskId?: string; reason?: string; detail?: Record<string, unknown> },
254
- ): Promise<ApiResponse> {
255
- return this.transport.request('POST', '/v1/receipts/ack', {
256
- conversation_id: conversationId,
257
- for_message_id: forMessageId,
258
- for_task_id: opts?.forTaskId,
259
- status,
260
- reason: opts?.reason,
261
- detail: opts?.detail,
262
- });
263
- }
264
-
265
- async approveContact(conversationId: string): Promise<ApiResponse<{ trusted: boolean; dm_conversation_id?: string }>> {
266
- const res = await this.transport.request<{ trusted: boolean; dm_conversation_id?: string }>('POST', '/v1/control/approve_contact', {
267
- conversation_id: conversationId,
268
- });
269
-
270
- if (res.ok && this.contactManager) {
271
- const pendingMessages = this.historyManager?.list(conversationId, { limit: 1 });
272
- const senderDid = pendingMessages?.[0]?.sender_did;
273
- if (senderDid && senderDid !== this.identity.did) {
274
- this.contactManager.add({
275
- did: senderDid,
276
- conversation_id: res.data?.dm_conversation_id ?? conversationId,
277
- trusted: true,
278
- });
279
- }
280
- }
281
-
282
- return res;
283
- }
284
-
285
- async revokeConversation(conversationId: string): Promise<ApiResponse> {
286
- return this.transport.request('POST', '/v1/control/revoke_conversation', {
287
- conversation_id: conversationId,
288
- });
289
- }
290
-
291
- async cancelTask(conversationId: string, taskId: string): Promise<ApiResponse<{ task_state: TaskState }>> {
292
- return this.transport.request('POST', '/v1/control/cancel_task', {
293
- conversation_id: conversationId,
294
- task_id: taskId,
295
- });
296
- }
297
-
298
- async blockSender(conversationId: string, senderDid: string): Promise<ApiResponse> {
299
- return this.transport.request('POST', '/v1/control/block_sender', {
300
- conversation_id: conversationId,
301
- sender_did: senderDid,
302
- });
303
- }
304
-
305
- async acquireLease(conversationId: string): Promise<ApiResponse> {
306
- return this.transport.request('POST', '/v1/lease/acquire', {
307
- conversation_id: conversationId,
308
- });
309
- }
310
-
311
- async renewLease(conversationId: string): Promise<ApiResponse> {
312
- return this.transport.request('POST', '/v1/lease/renew', {
313
- conversation_id: conversationId,
314
- });
315
- }
316
-
317
- async releaseLease(conversationId: string): Promise<ApiResponse> {
318
- return this.transport.request('POST', '/v1/lease/release', {
319
- conversation_id: conversationId,
320
- });
321
- }
322
-
323
- async uploadArtifact(content: Buffer): Promise<{ artifact_ref: string; sha256: string; size: number }> {
324
- const presignRes = await this.transport.request<any>('POST', '/v1/artifacts/presign_upload', {
325
- size: content.length,
326
- content_type: 'application/octet-stream',
327
- });
328
-
329
- if (!presignRes.ok || !presignRes.data) throw new Error('Failed to presign upload');
330
-
331
- const { upload_url, artifact_ref } = presignRes.data;
332
-
333
- await this.transport.fetchWithAuth(upload_url, {
334
- method: 'PUT',
335
- body: content,
336
- contentType: 'application/octet-stream',
337
- });
338
-
339
- const { createHash } = await import('node:crypto');
340
- const hash = createHash('sha256').update(content).digest('hex');
341
-
342
- return { artifact_ref, sha256: hash, size: content.length };
343
- }
344
-
345
- async downloadArtifact(artifactRef: string, expectedSha256: string, expectedSize: number): Promise<Buffer> {
346
- const presignRes = await this.transport.request<any>('POST', '/v1/artifacts/presign_download', {
347
- artifact_ref: artifactRef,
348
- });
349
-
350
- if (!presignRes.ok || !presignRes.data) throw new Error('Failed to presign download');
351
-
352
- const buffer = Buffer.from(await this.transport.fetchWithAuth(presignRes.data.download_url));
353
-
354
- if (buffer.length !== expectedSize) {
355
- throw new Error(`Size mismatch: expected ${expectedSize}, got ${buffer.length}`);
356
- }
357
-
358
- const { createHash } = await import('node:crypto');
359
- const hash = createHash('sha256').update(buffer).digest('hex');
360
- if (hash !== expectedSha256) {
361
- throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${hash}`);
362
- }
363
-
364
- return buffer;
365
- }
366
-
367
- async getSubscription(): Promise<ApiResponse<SubscriptionResponse>> {
368
- return this.transport.request<SubscriptionResponse>('GET', '/v1/subscription');
369
- }
370
-
371
- async resolveAlias(alias: string): Promise<ApiResponse<{ did: string; alias: string }>> {
372
- return this.transport.request('GET', `/v1/directory/resolve?alias=${encodeURIComponent(alias)}`);
373
- }
374
-
375
- async registerAlias(alias: string): Promise<ApiResponse> {
376
- return this.transport.request('POST', '/v1/directory/alias', { alias });
377
- }
378
-
379
- async listConversations(opts?: { type?: string }): Promise<ApiResponse<ConversationListResponse>> {
380
- const params = new URLSearchParams();
381
- if (opts?.type) params.set('type', opts.type);
382
- const qs = params.toString();
383
- return this.transport.request('GET', `/v1/conversations/list${qs ? '?' + qs : ''}`);
384
- }
385
-
386
- async getProfile(): Promise<ApiResponse<AgentProfile>> {
387
- return this.transport.request('GET', '/v1/directory/profile');
388
- }
389
-
390
- async updateProfile(profile: { display_name?: string; bio?: string; capabilities?: string[]; tags?: string[]; discoverable?: boolean }): Promise<ApiResponse<AgentProfile>> {
391
- return this.transport.request('POST', '/v1/directory/profile', profile);
392
- }
393
-
394
- async enableDiscovery(): Promise<ApiResponse<{ discoverable: boolean }>> {
395
- return this.transport.request('POST', '/v1/directory/enable_discovery');
396
- }
397
-
398
- async disableDiscovery(): Promise<ApiResponse<{ discoverable: boolean }>> {
399
- return this.transport.request('POST', '/v1/directory/disable_discovery');
400
- }
401
-
402
- async browseDirectory(opts?: { tag?: string; query?: string; limit?: number; offset?: number; sort?: 'default' | 'updated' }): Promise<ApiResponse<DirectoryBrowseResponse>> {
403
- const params = new URLSearchParams();
404
- if (opts?.tag) params.set('tag', opts.tag);
405
- if (opts?.query) params.set('q', opts.query);
406
- if (opts?.limit != null) params.set('limit', String(opts.limit));
407
- if (opts?.offset != null) params.set('offset', String(opts.offset));
408
- if (opts?.sort) params.set('sort', opts.sort);
409
- const qs = params.toString();
410
- return this.transport.request('GET', `/v1/directory/browse${qs ? '?' + qs : ''}`);
411
- }
412
-
413
- async publishPost(opts: { text: string; artifact_ref?: string }): Promise<ApiResponse<{ post_id: string; ts_ms: number }>> {
414
- return this.transport.request('POST', '/v1/feed/publish', { text: opts.text, artifact_ref: opts.artifact_ref });
415
- }
416
-
417
- async listFeedPublic(opts?: { limit?: number; since?: number }): Promise<ApiResponse<FeedPublicResponse>> {
418
- const params = new URLSearchParams();
419
- if (opts?.limit != null) params.set('limit', String(opts.limit));
420
- if (opts?.since != null) params.set('since', String(opts.since));
421
- const qs = params.toString();
422
- return this.transport.request('GET', `/v1/feed/public${qs ? '?' + qs : ''}`, undefined, true);
423
- }
424
-
425
- async listFeedByDid(did: string, opts?: { limit?: number }): Promise<ApiResponse<FeedByDidResponse>> {
426
- const params = new URLSearchParams({ did });
427
- if (opts?.limit != null) params.set('limit', String(opts.limit));
428
- return this.transport.request('GET', `/v1/feed/by_did?${params.toString()}`, undefined, true);
429
- }
430
-
431
- async createChannel(opts: { name: string; alias?: string; description?: string; discoverable?: boolean }): Promise<
432
- ApiResponse<{ conversation_id: string; alias: string; name: string; discoverable: boolean; relay_ws_url: string }>
433
- > {
434
- return this.transport.request('POST', '/v1/channels/create', {
435
- name: opts.name,
436
- alias: opts.alias,
437
- description: opts.description,
438
- join_policy: 'open',
439
- discoverable: opts.discoverable,
440
- });
441
- }
442
-
443
- async updateChannel(opts: { alias: string; name?: string; description?: string; discoverable?: boolean }): Promise<
444
- ApiResponse<{ alias: string; name: string; description?: string | null; discoverable: boolean }>
445
- > {
446
- const alias = opts.alias.replace(/^@ch\//, '');
447
- return this.transport.request('PATCH', '/v1/channels/update', {
448
- alias,
449
- name: opts.name,
450
- description: opts.description,
451
- discoverable: opts.discoverable,
452
- });
453
- }
454
-
455
- async deleteChannel(alias: string): Promise<ApiResponse<{ deleted: boolean; alias: string }>> {
456
- const a = alias.replace(/^@ch\//, '');
457
- return this.transport.request('DELETE', '/v1/channels/delete', { alias: a });
458
- }
459
-
460
- async discoverChannels(opts?: { limit?: number; query?: string }): Promise<
461
- ApiResponse<{ channels: Array<{ alias: string; name: string; description?: string | null; owner_did: string }> }>
462
- > {
463
- const params = new URLSearchParams();
464
- if (opts?.limit != null) params.set('limit', String(opts.limit));
465
- if (opts?.query) params.set('q', opts.query);
466
- const qs = params.toString();
467
- return this.transport.request('GET', `/v1/channels/discover${qs ? '?' + qs : ''}`, undefined, true);
468
- }
469
-
470
- async joinChannel(alias: string): Promise<
471
- ApiResponse<{ conversation_id: string; alias: string; owner_did: string; relay_ws_url: string }>
472
- > {
473
- return this.transport.request('POST', '/v1/channels/join', { alias });
474
- }
475
-
476
- getDid(): string {
477
- return this.identity.did;
478
- }
479
-
480
- // === Billing: device linking ===
481
-
482
- async createBillingLinkCode(): Promise<ApiResponse<{ code: string; expires_in_seconds: number }>> {
483
- return this.transport.request('POST', '/v1/billing/link-code');
484
- }
485
-
486
- async redeemBillingLink(code: string): Promise<ApiResponse<{ primary_did: string }>> {
487
- return this.transport.request('POST', '/v1/billing/link', { code });
488
- }
489
-
490
- async unlinkBillingDevice(did: string): Promise<ApiResponse> {
491
- return this.transport.request('POST', '/v1/billing/unlink', { did });
492
- }
493
-
494
- async getLinkedDevices(): Promise<ApiResponse<{ primary_did: string; linked_dids: string[]; is_primary: boolean }>> {
495
- return this.transport.request('GET', '/v1/billing/linked-devices');
496
- }
497
-
498
- // P0: High-level send-task-and-wait
499
- async sendTaskAndWait(
500
- targetDid: string,
501
- task: { title: string; description?: string; input?: any },
502
- opts?: { timeoutMs?: number; pollIntervalMs?: number },
503
- ): Promise<TaskResult> {
504
- const { v7: uuidv7 } = await import('uuid');
505
- const timeout = opts?.timeoutMs ?? 120_000;
506
- const pollInterval = opts?.pollIntervalMs ?? 2_000;
507
- const taskId = `t_${uuidv7()}`;
508
- const startTime = Date.now();
509
-
510
- // Step 1: open conversation
511
- const openRes = await this.openConversation(targetDid);
512
- if (!openRes.ok || !openRes.data) {
513
- return { status: 'error', task_id: taskId, error: { code: 'E_OPEN_FAILED', message: 'Failed to open conversation' }, elapsed_ms: Date.now() - startTime };
514
- }
515
-
516
- let conversationId = openRes.data.conversation_id;
517
-
518
- // Step 2: if not trusted, send contact_request and wait
519
- if (!openRes.data.trusted) {
520
- await this.sendContactRequest(conversationId, task.title);
521
-
522
- // Poll for approval
523
- const approvalDeadline = startTime + timeout;
524
- while (Date.now() < approvalDeadline) {
525
- await sleep(pollInterval);
526
- const reopen = await this.openConversation(targetDid);
527
- if (reopen.ok && reopen.data?.trusted) {
528
- conversationId = reopen.data.conversation_id;
529
- break;
530
- }
531
- }
532
-
533
- if (Date.now() >= approvalDeadline) {
534
- return { status: 'error', task_id: taskId, error: { code: 'E_NOT_APPROVED', message: 'Contact request not approved within timeout' }, elapsed_ms: Date.now() - startTime };
535
- }
536
- }
537
-
538
- // Step 3: send task
539
- const sendRes = await this.sendTask(conversationId, { task_id: taskId, ...task });
540
- if (!sendRes.ok) {
541
- return { status: 'error', task_id: taskId, error: { code: sendRes.error?.code ?? 'E_SEND_FAILED', message: sendRes.error?.message ?? 'Send failed' }, elapsed_ms: Date.now() - startTime };
542
- }
543
-
544
- const sinceSeq = sendRes.data?.seq ?? 0;
545
-
546
- // Step 4: poll for result
547
- const deadline = startTime + timeout;
548
- while (Date.now() < deadline) {
549
- await sleep(pollInterval);
550
-
551
- const fetchRes = await this.fetchInbox(conversationId, { sinceSeq });
552
- if (!fetchRes.ok || !fetchRes.data) continue;
553
-
554
- for (const msg of fetchRes.data.messages) {
555
- if (msg.schema === SCHEMA_RESULT && msg.payload?.task_id === taskId) {
556
- return {
557
- status: msg.payload.status === 'ok' ? 'ok' : 'error',
558
- task_id: taskId,
559
- result: msg.payload.output,
560
- error: msg.payload.error,
561
- elapsed_ms: Date.now() - startTime,
562
- };
563
- }
564
-
565
- if (msg.schema === SCHEMA_RECEIPT && msg.payload?.for_task_id === taskId && msg.payload?.status === 'failed') {
566
- return {
567
- status: 'error',
568
- task_id: taskId,
569
- error: { code: msg.payload.reason ?? 'E_TASK_FAILED', message: msg.payload.reason ?? 'Task failed' },
570
- elapsed_ms: Date.now() - startTime,
571
- };
572
- }
573
- }
574
- }
575
-
576
- return { status: 'error', task_id: taskId, error: { code: 'E_TIMEOUT', message: `No result within ${timeout}ms` }, elapsed_ms: timeout };
577
- }
578
- }
579
-
580
- function sleep(ms: number): Promise<void> {
581
- return new Promise((resolve) => setTimeout(resolve, ms));
582
- }