ragora 0.1.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/src/client.ts ADDED
@@ -0,0 +1,1089 @@
1
+ /**
2
+ * Ragora API Client
3
+ *
4
+ * Fetch-based HTTP client for the Ragora API.
5
+ */
6
+
7
+ import { RagoraError } from './errors.js';
8
+ import type {
9
+ APIError,
10
+ ChatChoice,
11
+ ChatRequest,
12
+ ChatResponse,
13
+ ChatStreamChunk,
14
+ Collection,
15
+ CollectionListRequest,
16
+ CollectionListResponse,
17
+ CreateCollectionRequest,
18
+ CreditBalance,
19
+ DeleteResponse,
20
+ Document,
21
+ DocumentListRequest,
22
+ DocumentListResponse,
23
+ DocumentStatus,
24
+ Listing,
25
+ MarketplaceListRequest,
26
+ MarketplaceListResponse,
27
+ MarketplaceProduct,
28
+ ResponseMetadata,
29
+ SearchRequest,
30
+ SearchResponse,
31
+ SearchResult,
32
+ UpdateCollectionRequest,
33
+ UploadDocumentRequest,
34
+ UploadResponse,
35
+ } from './types.js';
36
+
37
+ export interface RagoraClientOptions {
38
+ /** Your Ragora API key */
39
+ apiKey: string;
40
+ /** API base URL (default: https://api.ragora.app) */
41
+ baseUrl?: string;
42
+ /** Request timeout in milliseconds (default: 30000) */
43
+ timeout?: number;
44
+ /** Custom fetch implementation (for testing or environments without global fetch) */
45
+ fetch?: typeof fetch;
46
+ }
47
+
48
+ export class RagoraClient {
49
+ private readonly apiKey: string;
50
+ private readonly baseUrl: string;
51
+ private readonly timeout: number;
52
+ private readonly fetchFn: typeof fetch;
53
+
54
+ constructor(options: RagoraClientOptions) {
55
+ this.apiKey = options.apiKey;
56
+ this.baseUrl = (options.baseUrl ?? 'https://api.ragora.app').replace(
57
+ /\/$/,
58
+ ''
59
+ );
60
+ this.timeout = options.timeout ?? 30000;
61
+ this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
62
+ }
63
+
64
+ /**
65
+ * Extract metadata from response headers.
66
+ */
67
+ private extractMetadata(headers: Headers): ResponseMetadata {
68
+ const safeFloat = (key: string): number | undefined => {
69
+ const val = headers.get(key);
70
+ if (val) {
71
+ const parsed = parseFloat(val);
72
+ if (!isNaN(parsed)) return parsed;
73
+ }
74
+ return undefined;
75
+ };
76
+
77
+ const safeInt = (key: string): number | undefined => {
78
+ const val = headers.get(key);
79
+ if (val) {
80
+ const parsed = parseInt(val, 10);
81
+ if (!isNaN(parsed)) return parsed;
82
+ }
83
+ return undefined;
84
+ };
85
+
86
+ return {
87
+ requestId: headers.get('X-Request-ID') ?? undefined,
88
+ apiVersion: headers.get('X-Ragora-API-Version') ?? undefined,
89
+ costUsd: safeFloat('X-Ragora-Cost-USD'),
90
+ balanceRemainingUsd: safeFloat('X-Ragora-Balance-Remaining-USD'),
91
+ rateLimitLimit: safeInt('X-RateLimit-Limit'),
92
+ rateLimitRemaining: safeInt('X-RateLimit-Remaining'),
93
+ rateLimitReset: safeInt('X-RateLimit-Reset'),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Make an API request.
99
+ */
100
+ private async request<T>(
101
+ method: string,
102
+ path: string,
103
+ options?: {
104
+ body?: unknown;
105
+ params?: Record<string, string | number | undefined>;
106
+ }
107
+ ): Promise<{ data: T; metadata: ResponseMetadata }> {
108
+ const url = new URL(`${this.baseUrl}${path}`);
109
+
110
+ if (options?.params) {
111
+ for (const [key, value] of Object.entries(options.params)) {
112
+ if (value !== undefined) {
113
+ url.searchParams.set(key, String(value));
114
+ }
115
+ }
116
+ }
117
+
118
+ const controller = new AbortController();
119
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
120
+
121
+ try {
122
+ const response = await this.fetchFn(url.toString(), {
123
+ method,
124
+ headers: {
125
+ Authorization: `Bearer ${this.apiKey}`,
126
+ 'Content-Type': 'application/json',
127
+ 'User-Agent': 'ragora-js/0.1.0',
128
+ },
129
+ body: options?.body ? JSON.stringify(options.body) : undefined,
130
+ signal: controller.signal,
131
+ });
132
+
133
+ const metadata = this.extractMetadata(response.headers);
134
+
135
+ if (!response.ok) {
136
+ await this.handleError(response, metadata.requestId);
137
+ }
138
+
139
+ const data = (await response.json()) as T;
140
+ return { data, metadata };
141
+ } finally {
142
+ clearTimeout(timeoutId);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Handle error responses.
148
+ */
149
+ private async handleError(
150
+ response: Response,
151
+ requestId?: string
152
+ ): Promise<never> {
153
+ try {
154
+ const data = (await response.json()) as Record<string, unknown>;
155
+
156
+ if (data.error) {
157
+ const errorData = data.error as Record<string, unknown>;
158
+ if (typeof errorData === 'object') {
159
+ const error: APIError = {
160
+ code: String(errorData.code ?? 'unknown'),
161
+ message: String(errorData.message ?? 'Unknown error'),
162
+ details: Array.isArray(errorData.details) ? errorData.details : [],
163
+ requestId,
164
+ };
165
+ throw new RagoraError(
166
+ error.message,
167
+ response.status,
168
+ error,
169
+ requestId
170
+ );
171
+ }
172
+ throw new RagoraError(
173
+ String(errorData),
174
+ response.status,
175
+ undefined,
176
+ requestId
177
+ );
178
+ }
179
+
180
+ throw new RagoraError(
181
+ String(data.message ?? response.statusText),
182
+ response.status,
183
+ undefined,
184
+ requestId
185
+ );
186
+ } catch (e) {
187
+ if (e instanceof RagoraError) throw e;
188
+
189
+ throw new RagoraError(
190
+ response.statusText || `HTTP ${response.status}`,
191
+ response.status,
192
+ undefined,
193
+ requestId
194
+ );
195
+ }
196
+ }
197
+
198
+ // --- Search ---
199
+
200
+ /**
201
+ * Search for relevant documents in a collection.
202
+ */
203
+ async search(request: SearchRequest): Promise<SearchResponse> {
204
+ const collectionIds = request.collectionId
205
+ ? Array.isArray(request.collectionId)
206
+ ? request.collectionId
207
+ : [request.collectionId]
208
+ : undefined;
209
+
210
+ const { data, metadata } = await this.request<{
211
+ results: Array<{
212
+ id: string;
213
+ text?: string;
214
+ content?: string;
215
+ score: number;
216
+ metadata?: Record<string, unknown>;
217
+ document_id?: string;
218
+ collection_id?: string;
219
+ }>;
220
+ }>('POST', '/v1/retrieve', {
221
+ body: {
222
+ ...(collectionIds && { collection_ids: collectionIds }),
223
+ query: request.query,
224
+ top_k: request.topK ?? 5,
225
+ filters: request.filters,
226
+ },
227
+ });
228
+
229
+ const results: SearchResult[] = data.results.map((r) => ({
230
+ id: r.id,
231
+ content: r.text ?? r.content ?? '',
232
+ score: r.score,
233
+ metadata: r.metadata ?? {},
234
+ documentId: r.document_id,
235
+ collectionId: r.collection_id,
236
+ }));
237
+
238
+ return {
239
+ results,
240
+ query: request.query,
241
+ total: results.length,
242
+ ...metadata,
243
+ };
244
+ }
245
+
246
+ // --- Chat ---
247
+
248
+ /**
249
+ * Generate a chat completion with RAG context.
250
+ */
251
+ async chat(request: ChatRequest): Promise<ChatResponse> {
252
+ const collectionIds = request.collectionId
253
+ ? Array.isArray(request.collectionId)
254
+ ? request.collectionId
255
+ : [request.collectionId]
256
+ : undefined;
257
+
258
+ const { data, metadata } = await this.request<{
259
+ id: string;
260
+ object: string;
261
+ created: number;
262
+ model: string;
263
+ choices: Array<{
264
+ index: number;
265
+ message: { role: string; content: string };
266
+ finish_reason?: string;
267
+ }>;
268
+ usage?: {
269
+ prompt_tokens: number;
270
+ completion_tokens: number;
271
+ total_tokens: number;
272
+ };
273
+ sources?: Array<{
274
+ id: string;
275
+ content: string;
276
+ score: number;
277
+ metadata?: Record<string, unknown>;
278
+ }>;
279
+ }>('POST', '/v1/chat/completions', {
280
+ body: {
281
+ ...(collectionIds && { collection_ids: collectionIds }),
282
+ messages: request.messages,
283
+ model: request.model ?? 'gpt-4o-mini',
284
+ temperature: request.temperature ?? 0.7,
285
+ max_tokens: request.maxTokens,
286
+ top_k: request.topK,
287
+ stream: false,
288
+ },
289
+ });
290
+
291
+ const choices: ChatChoice[] = data.choices.map((c) => ({
292
+ index: c.index,
293
+ message: {
294
+ role: c.message.role as 'user' | 'assistant' | 'system',
295
+ content: c.message.content,
296
+ },
297
+ finishReason: c.finish_reason,
298
+ }));
299
+
300
+ const sources: SearchResult[] = (data.sources ?? []).map((s) => ({
301
+ id: s.id,
302
+ content: s.content,
303
+ score: s.score,
304
+ metadata: s.metadata ?? {},
305
+ }));
306
+
307
+ return {
308
+ id: data.id,
309
+ object: data.object,
310
+ created: data.created,
311
+ model: data.model,
312
+ choices,
313
+ usage: data.usage
314
+ ? {
315
+ promptTokens: data.usage.prompt_tokens,
316
+ completionTokens: data.usage.completion_tokens,
317
+ totalTokens: data.usage.total_tokens,
318
+ }
319
+ : undefined,
320
+ sources,
321
+ ...metadata,
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Stream a chat completion with RAG context.
327
+ */
328
+ async *chatStream(request: ChatRequest): AsyncGenerator<ChatStreamChunk> {
329
+ const url = `${this.baseUrl}/v1/chat/completions`;
330
+ const collectionIds = request.collectionId
331
+ ? Array.isArray(request.collectionId)
332
+ ? request.collectionId
333
+ : [request.collectionId]
334
+ : undefined;
335
+
336
+ const controller = new AbortController();
337
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
338
+
339
+ try {
340
+ const response = await this.fetchFn(url, {
341
+ method: 'POST',
342
+ headers: {
343
+ Authorization: `Bearer ${this.apiKey}`,
344
+ 'Content-Type': 'application/json',
345
+ 'User-Agent': 'ragora-js/0.1.0',
346
+ },
347
+ body: JSON.stringify({
348
+ ...(collectionIds && { collection_ids: collectionIds }),
349
+ messages: request.messages,
350
+ model: request.model ?? 'gpt-4o-mini',
351
+ temperature: request.temperature ?? 0.7,
352
+ max_tokens: request.maxTokens,
353
+ top_k: request.topK,
354
+ stream: true,
355
+ }),
356
+ signal: controller.signal,
357
+ });
358
+
359
+ if (!response.ok) {
360
+ const metadata = this.extractMetadata(response.headers);
361
+ await this.handleError(response, metadata.requestId);
362
+ }
363
+
364
+ const reader = response.body?.getReader();
365
+ if (!reader) {
366
+ throw new Error('Response body is not readable');
367
+ }
368
+
369
+ const decoder = new TextDecoder();
370
+ let buffer = '';
371
+
372
+ while (true) {
373
+ const { done, value } = await reader.read();
374
+ if (done) break;
375
+
376
+ buffer += decoder.decode(value, { stream: true });
377
+ const lines = buffer.split('\n');
378
+ buffer = lines.pop() ?? '';
379
+
380
+ for (const line of lines) {
381
+ if (!line.trim() || !line.startsWith('data: ')) continue;
382
+
383
+ const dataStr = line.slice(6); // Remove "data: " prefix
384
+ if (dataStr === '[DONE]') return;
385
+
386
+ try {
387
+ const data = JSON.parse(dataStr) as {
388
+ choices?: Array<{
389
+ delta?: { content?: string };
390
+ finish_reason?: string;
391
+ }>;
392
+ sources?: Array<{
393
+ id: string;
394
+ content: string;
395
+ score: number;
396
+ metadata?: Record<string, unknown>;
397
+ }>;
398
+ };
399
+
400
+ const delta = data.choices?.[0]?.delta ?? {};
401
+ const finishReason = data.choices?.[0]?.finish_reason;
402
+
403
+ const sources: SearchResult[] = (data.sources ?? []).map((s) => ({
404
+ id: s.id,
405
+ content: s.content,
406
+ score: s.score,
407
+ metadata: s.metadata ?? {},
408
+ }));
409
+
410
+ yield {
411
+ content: delta.content ?? '',
412
+ finishReason,
413
+ sources,
414
+ };
415
+ } catch {
416
+ // Skip invalid JSON lines
417
+ continue;
418
+ }
419
+ }
420
+ }
421
+ } finally {
422
+ clearTimeout(timeoutId);
423
+ }
424
+ }
425
+
426
+ // --- Credits ---
427
+
428
+ /**
429
+ * Get current credit balance.
430
+ */
431
+ async getBalance(): Promise<CreditBalance> {
432
+ const { data, metadata } = await this.request<{
433
+ balance_usd: number;
434
+ currency?: string;
435
+ }>('GET', '/v1/credits/balance');
436
+
437
+ return {
438
+ balanceUsd: data.balance_usd,
439
+ currency: data.currency ?? 'USD',
440
+ ...metadata,
441
+ };
442
+ }
443
+
444
+ // --- Collections ---
445
+
446
+ /**
447
+ * List your collections.
448
+ */
449
+ async listCollections(
450
+ request?: CollectionListRequest
451
+ ): Promise<CollectionListResponse> {
452
+ const { data, metadata } = await this.request<{
453
+ data: Array<{
454
+ id: string;
455
+ owner_id?: string;
456
+ name: string;
457
+ slug?: string;
458
+ description?: string;
459
+ total_documents?: number;
460
+ total_vectors?: number;
461
+ total_chunks?: number;
462
+ total_size_bytes?: number;
463
+ created_at?: string;
464
+ updated_at?: string;
465
+ }>;
466
+ total: number;
467
+ limit: number;
468
+ offset: number;
469
+ hasMore: boolean;
470
+ }>('GET', '/v1/collections', {
471
+ params: {
472
+ limit: request?.limit,
473
+ offset: request?.offset,
474
+ search: request?.search,
475
+ },
476
+ });
477
+
478
+ const collections: Collection[] = data.data.map((c) => ({
479
+ id: c.id,
480
+ ownerId: c.owner_id,
481
+ name: c.name,
482
+ slug: c.slug,
483
+ description: c.description,
484
+ totalDocuments: c.total_documents ?? 0,
485
+ totalVectors: c.total_vectors ?? 0,
486
+ totalChunks: c.total_chunks ?? 0,
487
+ totalSizeBytes: c.total_size_bytes ?? 0,
488
+ createdAt: c.created_at,
489
+ updatedAt: c.updated_at,
490
+ }));
491
+
492
+ return {
493
+ data: collections,
494
+ total: data.total,
495
+ limit: data.limit,
496
+ offset: data.offset,
497
+ hasMore: data.hasMore,
498
+ ...metadata,
499
+ };
500
+ }
501
+
502
+ /**
503
+ * Get a specific collection by ID or slug.
504
+ */
505
+ async getCollection(collectionId: string): Promise<Collection> {
506
+ const { data } = await this.request<{
507
+ data?: {
508
+ id: string;
509
+ owner_id?: string;
510
+ name: string;
511
+ slug?: string;
512
+ description?: string;
513
+ total_documents?: number;
514
+ total_vectors?: number;
515
+ total_chunks?: number;
516
+ total_size_bytes?: number;
517
+ created_at?: string;
518
+ updated_at?: string;
519
+ };
520
+ id: string;
521
+ owner_id?: string;
522
+ name: string;
523
+ slug?: string;
524
+ description?: string;
525
+ total_documents?: number;
526
+ total_vectors?: number;
527
+ total_chunks?: number;
528
+ total_size_bytes?: number;
529
+ created_at?: string;
530
+ updated_at?: string;
531
+ }>('GET', `/v1/collections/${collectionId}`);
532
+
533
+ // Handle nested data structure
534
+ const collData = data.data ?? data;
535
+
536
+ return {
537
+ id: collData.id,
538
+ ownerId: collData.owner_id,
539
+ name: collData.name,
540
+ slug: collData.slug,
541
+ description: collData.description,
542
+ totalDocuments: collData.total_documents ?? 0,
543
+ totalVectors: collData.total_vectors ?? 0,
544
+ totalChunks: collData.total_chunks ?? 0,
545
+ totalSizeBytes: collData.total_size_bytes ?? 0,
546
+ createdAt: collData.created_at,
547
+ updatedAt: collData.updated_at,
548
+ };
549
+ }
550
+
551
+ /**
552
+ * Create a new collection.
553
+ */
554
+ async createCollection(request: CreateCollectionRequest): Promise<Collection> {
555
+ const { data } = await this.request<{
556
+ data?: {
557
+ id: string;
558
+ owner_id?: string;
559
+ name: string;
560
+ slug?: string;
561
+ description?: string;
562
+ total_documents?: number;
563
+ total_vectors?: number;
564
+ total_chunks?: number;
565
+ total_size_bytes?: number;
566
+ created_at?: string;
567
+ updated_at?: string;
568
+ };
569
+ id: string;
570
+ owner_id?: string;
571
+ name: string;
572
+ slug?: string;
573
+ description?: string;
574
+ total_documents?: number;
575
+ total_vectors?: number;
576
+ total_chunks?: number;
577
+ total_size_bytes?: number;
578
+ created_at?: string;
579
+ updated_at?: string;
580
+ }>('POST', '/v1/collections', {
581
+ body: {
582
+ name: request.name,
583
+ description: request.description,
584
+ slug: request.slug,
585
+ },
586
+ });
587
+
588
+ // Handle nested data structure
589
+ const collData = data.data ?? data;
590
+
591
+ return {
592
+ id: collData.id,
593
+ ownerId: collData.owner_id,
594
+ name: collData.name,
595
+ slug: collData.slug,
596
+ description: collData.description,
597
+ totalDocuments: collData.total_documents ?? 0,
598
+ totalVectors: collData.total_vectors ?? 0,
599
+ totalChunks: collData.total_chunks ?? 0,
600
+ totalSizeBytes: collData.total_size_bytes ?? 0,
601
+ createdAt: collData.created_at,
602
+ updatedAt: collData.updated_at,
603
+ };
604
+ }
605
+
606
+ /**
607
+ * Update an existing collection.
608
+ */
609
+ async updateCollection(
610
+ collectionId: string,
611
+ request: UpdateCollectionRequest
612
+ ): Promise<Collection> {
613
+ const { data } = await this.request<{
614
+ data?: {
615
+ id: string;
616
+ owner_id?: string;
617
+ name: string;
618
+ slug?: string;
619
+ description?: string;
620
+ total_documents?: number;
621
+ total_vectors?: number;
622
+ total_chunks?: number;
623
+ total_size_bytes?: number;
624
+ created_at?: string;
625
+ updated_at?: string;
626
+ };
627
+ id: string;
628
+ owner_id?: string;
629
+ name: string;
630
+ slug?: string;
631
+ description?: string;
632
+ total_documents?: number;
633
+ total_vectors?: number;
634
+ total_chunks?: number;
635
+ total_size_bytes?: number;
636
+ created_at?: string;
637
+ updated_at?: string;
638
+ }>('PATCH', `/v1/collections/${collectionId}`, {
639
+ body: {
640
+ name: request.name,
641
+ description: request.description,
642
+ slug: request.slug,
643
+ },
644
+ });
645
+
646
+ // Handle nested data structure
647
+ const collData = data.data ?? data;
648
+
649
+ return {
650
+ id: collData.id,
651
+ ownerId: collData.owner_id,
652
+ name: collData.name,
653
+ slug: collData.slug,
654
+ description: collData.description,
655
+ totalDocuments: collData.total_documents ?? 0,
656
+ totalVectors: collData.total_vectors ?? 0,
657
+ totalChunks: collData.total_chunks ?? 0,
658
+ totalSizeBytes: collData.total_size_bytes ?? 0,
659
+ createdAt: collData.created_at,
660
+ updatedAt: collData.updated_at,
661
+ };
662
+ }
663
+
664
+ /**
665
+ * Delete a collection and all its documents.
666
+ */
667
+ async deleteCollection(collectionId: string): Promise<DeleteResponse> {
668
+ const { data } = await this.request<{
669
+ message: string;
670
+ id: string;
671
+ deleted_at?: string;
672
+ }>('DELETE', `/v1/collections/${collectionId}`);
673
+
674
+ return {
675
+ message: data.message ?? 'Collection deleted',
676
+ id: data.id ?? collectionId,
677
+ deletedAt: data.deleted_at,
678
+ };
679
+ }
680
+
681
+ // --- Documents ---
682
+
683
+ /**
684
+ * Upload a document to a collection.
685
+ */
686
+ async uploadDocument(request: UploadDocumentRequest): Promise<UploadResponse> {
687
+ const formData = new FormData();
688
+ formData.append('file', request.file, request.filename);
689
+ if (request.collectionId) {
690
+ formData.append('collection_id', request.collectionId);
691
+ }
692
+
693
+ const url = `${this.baseUrl}/v1/documents`;
694
+
695
+ const controller = new AbortController();
696
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
697
+
698
+ try {
699
+ const response = await this.fetchFn(url, {
700
+ method: 'POST',
701
+ headers: {
702
+ Authorization: `Bearer ${this.apiKey}`,
703
+ 'User-Agent': 'ragora-js/0.1.0',
704
+ // Note: Don't set Content-Type for FormData - browser sets it with boundary
705
+ },
706
+ body: formData,
707
+ signal: controller.signal,
708
+ });
709
+
710
+ const metadata = this.extractMetadata(response.headers);
711
+
712
+ if (!response.ok) {
713
+ await this.handleError(response, metadata.requestId);
714
+ }
715
+
716
+ const data = (await response.json()) as {
717
+ id: string;
718
+ file_name?: string;
719
+ status: string;
720
+ collection_id: string;
721
+ collection_slug?: string;
722
+ collection_name?: string;
723
+ message?: string;
724
+ };
725
+
726
+ return {
727
+ id: data.id,
728
+ filename: data.file_name ?? request.filename,
729
+ status: data.status ?? 'processing',
730
+ collectionId: data.collection_id ?? request.collectionId ?? '',
731
+ message: data.message,
732
+ ...metadata,
733
+ };
734
+ } finally {
735
+ clearTimeout(timeoutId);
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Get the processing status of a document.
741
+ */
742
+ async getDocumentStatus(documentId: string): Promise<DocumentStatus> {
743
+ const { data } = await this.request<{
744
+ id: string;
745
+ status: string;
746
+ filename: string;
747
+ mime_type?: string;
748
+ vector_count?: number;
749
+ chunk_count?: number;
750
+ progress_percent?: number;
751
+ progress_stage?: string;
752
+ eta_seconds?: number;
753
+ has_transcript?: boolean;
754
+ is_active?: boolean;
755
+ version_number?: number;
756
+ created_at?: string;
757
+ }>('GET', `/v1/documents/${documentId}/status`);
758
+
759
+ return {
760
+ id: data.id ?? documentId,
761
+ status: data.status ?? 'unknown',
762
+ filename: data.filename ?? '',
763
+ mimeType: data.mime_type,
764
+ vectorCount: data.vector_count ?? 0,
765
+ chunkCount: data.chunk_count ?? 0,
766
+ progressPercent: data.progress_percent,
767
+ progressStage: data.progress_stage,
768
+ etaSeconds: data.eta_seconds,
769
+ hasTranscript: data.has_transcript ?? false,
770
+ isActive: data.is_active ?? true,
771
+ versionNumber: data.version_number ?? 1,
772
+ createdAt: data.created_at,
773
+ };
774
+ }
775
+
776
+ /**
777
+ * List documents in a collection.
778
+ */
779
+ async listDocuments(request?: DocumentListRequest): Promise<DocumentListResponse> {
780
+ const { data, metadata } = await this.request<{
781
+ data: Array<{
782
+ id: string;
783
+ filename: string;
784
+ status: string;
785
+ mime_type?: string;
786
+ file_size_bytes?: number;
787
+ vector_count?: number;
788
+ chunk_count?: number;
789
+ collection_id?: string;
790
+ progress_percent?: number;
791
+ progress_stage?: string;
792
+ error_message?: string;
793
+ created_at?: string;
794
+ updated_at?: string;
795
+ }>;
796
+ total: number;
797
+ limit: number;
798
+ offset: number;
799
+ has_more: boolean;
800
+ }>('GET', '/v1/documents', {
801
+ params: {
802
+ collection_id: request?.collectionId,
803
+ limit: request?.limit,
804
+ offset: request?.offset,
805
+ },
806
+ });
807
+
808
+ const documents: Document[] = data.data.map((d) => ({
809
+ id: d.id,
810
+ filename: d.filename,
811
+ status: d.status,
812
+ mimeType: d.mime_type,
813
+ sizeBytes: d.file_size_bytes,
814
+ vectorCount: d.vector_count ?? 0,
815
+ chunkCount: d.chunk_count ?? 0,
816
+ collectionId: d.collection_id,
817
+ progressPercent: d.progress_percent,
818
+ progressStage: d.progress_stage,
819
+ errorMessage: d.error_message,
820
+ createdAt: d.created_at,
821
+ updatedAt: d.updated_at,
822
+ }));
823
+
824
+ return {
825
+ data: documents,
826
+ total: data.total,
827
+ limit: data.limit,
828
+ offset: data.offset,
829
+ hasMore: data.has_more,
830
+ ...metadata,
831
+ };
832
+ }
833
+
834
+ /**
835
+ * Delete a document.
836
+ */
837
+ async deleteDocument(documentId: string): Promise<DeleteResponse> {
838
+ const { data } = await this.request<{
839
+ message: string;
840
+ id: string;
841
+ vectors_removed?: number;
842
+ }>('DELETE', `/v1/documents/${documentId}`);
843
+
844
+ return {
845
+ message: data.message ?? 'Document deleted',
846
+ id: data.id ?? documentId,
847
+ };
848
+ }
849
+
850
+ /**
851
+ * Wait for a document to finish processing.
852
+ */
853
+ async waitForDocument(
854
+ documentId: string,
855
+ options?: {
856
+ /** Maximum time to wait in milliseconds (default: 300000 = 5 minutes) */
857
+ timeout?: number;
858
+ /** Time between status checks in milliseconds (default: 2000) */
859
+ pollInterval?: number;
860
+ }
861
+ ): Promise<DocumentStatus> {
862
+ const timeout = options?.timeout ?? 300000;
863
+ const pollInterval = options?.pollInterval ?? 2000;
864
+ const startTime = Date.now();
865
+
866
+ while (true) {
867
+ const status = await this.getDocumentStatus(documentId);
868
+
869
+ if (status.status === 'completed') {
870
+ return status;
871
+ }
872
+
873
+ if (status.status === 'failed') {
874
+ throw new RagoraError(
875
+ `Document processing failed: ${status.progressStage ?? 'unknown error'}`,
876
+ 500,
877
+ undefined,
878
+ documentId
879
+ );
880
+ }
881
+
882
+ const elapsed = Date.now() - startTime;
883
+ if (elapsed >= timeout) {
884
+ throw new RagoraError(
885
+ `Timeout waiting for document ${documentId} to process`,
886
+ 408,
887
+ undefined,
888
+ documentId
889
+ );
890
+ }
891
+
892
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
893
+ }
894
+ }
895
+
896
+ // --- Marketplace ---
897
+
898
+ /**
899
+ * List public marketplace products.
900
+ */
901
+ async listMarketplace(
902
+ request?: MarketplaceListRequest
903
+ ): Promise<MarketplaceListResponse> {
904
+ const { data, metadata } = await this.request<{
905
+ data: Array<{
906
+ id: string;
907
+ collection_id?: string;
908
+ seller_id: string;
909
+ slug: string;
910
+ title: string;
911
+ description?: string;
912
+ thumbnail_url?: string;
913
+ status: string;
914
+ average_rating: number;
915
+ review_count: number;
916
+ total_vectors: number;
917
+ total_chunks: number;
918
+ access_count: number;
919
+ data_size?: string;
920
+ is_trending?: boolean;
921
+ is_verified?: boolean;
922
+ seller?: { id: string; full_name?: string; name?: string; email?: string };
923
+ categories?: Array<{ id: string; slug: string; name: string }>;
924
+ listings?: Array<{
925
+ id: string;
926
+ product_id: string;
927
+ seller_id: string;
928
+ type: string;
929
+ price_amount_usd: number;
930
+ price_interval?: string;
931
+ price_per_retrieval_usd?: number;
932
+ is_active: boolean;
933
+ buyer_count?: number;
934
+ created_at?: string;
935
+ updated_at?: string;
936
+ }>;
937
+ created_at?: string;
938
+ updated_at?: string;
939
+ }>;
940
+ total: number;
941
+ limit: number;
942
+ offset: number;
943
+ hasMore: boolean;
944
+ }>('GET', '/v1/marketplace', {
945
+ params: {
946
+ limit: request?.limit,
947
+ offset: request?.offset,
948
+ search: request?.search,
949
+ category: request?.category,
950
+ trending: request?.trending ? 'true' : undefined,
951
+ },
952
+ });
953
+
954
+ const products: MarketplaceProduct[] = data.data.map((p) =>
955
+ this.mapMarketplaceProduct(p)
956
+ );
957
+
958
+ return {
959
+ data: products,
960
+ total: data.total,
961
+ limit: data.limit,
962
+ offset: data.offset,
963
+ hasMore: data.hasMore,
964
+ ...metadata,
965
+ };
966
+ }
967
+
968
+ /**
969
+ * Get a marketplace product by ID or slug.
970
+ */
971
+ async getMarketplaceProduct(idOrSlug: string): Promise<MarketplaceProduct> {
972
+ const { data } = await this.request<{
973
+ id: string;
974
+ collection_id?: string;
975
+ seller_id: string;
976
+ slug: string;
977
+ title: string;
978
+ description?: string;
979
+ thumbnail_url?: string;
980
+ status: string;
981
+ average_rating: number;
982
+ review_count: number;
983
+ total_vectors: number;
984
+ total_chunks: number;
985
+ access_count: number;
986
+ data_size?: string;
987
+ is_trending?: boolean;
988
+ is_verified?: boolean;
989
+ seller?: { id: string; full_name?: string; name?: string; email?: string };
990
+ categories?: Array<{ id: string; slug: string; name: string }>;
991
+ listings?: Array<{
992
+ id: string;
993
+ product_id: string;
994
+ seller_id: string;
995
+ type: string;
996
+ price_amount_usd: number;
997
+ price_interval?: string;
998
+ price_per_retrieval_usd?: number;
999
+ is_active: boolean;
1000
+ buyer_count?: number;
1001
+ created_at?: string;
1002
+ updated_at?: string;
1003
+ }>;
1004
+ created_at?: string;
1005
+ updated_at?: string;
1006
+ }>('GET', `/v1/marketplace/${idOrSlug}`);
1007
+
1008
+ return this.mapMarketplaceProduct(data);
1009
+ }
1010
+
1011
+ /**
1012
+ * Map a raw marketplace product response to the SDK type.
1013
+ */
1014
+ private mapMarketplaceProduct(p: {
1015
+ id: string;
1016
+ collection_id?: string;
1017
+ seller_id: string;
1018
+ slug: string;
1019
+ title: string;
1020
+ description?: string;
1021
+ thumbnail_url?: string;
1022
+ status: string;
1023
+ average_rating: number;
1024
+ review_count: number;
1025
+ total_vectors: number;
1026
+ total_chunks: number;
1027
+ access_count: number;
1028
+ data_size?: string;
1029
+ is_trending?: boolean;
1030
+ is_verified?: boolean;
1031
+ seller?: { id: string; full_name?: string; name?: string; email?: string };
1032
+ categories?: Array<{ id: string; slug: string; name: string }>;
1033
+ listings?: Array<{
1034
+ id: string;
1035
+ product_id: string;
1036
+ seller_id: string;
1037
+ type: string;
1038
+ price_amount_usd: number;
1039
+ price_interval?: string;
1040
+ price_per_retrieval_usd?: number;
1041
+ is_active: boolean;
1042
+ buyer_count?: number;
1043
+ created_at?: string;
1044
+ updated_at?: string;
1045
+ }>;
1046
+ created_at?: string;
1047
+ updated_at?: string;
1048
+ }): MarketplaceProduct {
1049
+ const listings: Listing[] | undefined = p.listings?.map((l) => ({
1050
+ id: l.id,
1051
+ productId: l.product_id,
1052
+ sellerId: l.seller_id,
1053
+ type: l.type,
1054
+ priceAmountUsd: l.price_amount_usd,
1055
+ priceInterval: l.price_interval,
1056
+ pricePerRetrievalUsd: l.price_per_retrieval_usd,
1057
+ isActive: l.is_active,
1058
+ buyerCount: l.buyer_count,
1059
+ createdAt: l.created_at,
1060
+ updatedAt: l.updated_at,
1061
+ }));
1062
+
1063
+ return {
1064
+ id: p.id,
1065
+ collectionId: p.collection_id,
1066
+ sellerId: p.seller_id,
1067
+ slug: p.slug,
1068
+ title: p.title,
1069
+ description: p.description,
1070
+ thumbnailUrl: p.thumbnail_url,
1071
+ status: p.status,
1072
+ averageRating: p.average_rating ?? 0,
1073
+ reviewCount: p.review_count ?? 0,
1074
+ totalVectors: p.total_vectors ?? 0,
1075
+ totalChunks: p.total_chunks ?? 0,
1076
+ accessCount: p.access_count ?? 0,
1077
+ dataSize: p.data_size,
1078
+ isTrending: p.is_trending,
1079
+ isVerified: p.is_verified,
1080
+ seller: p.seller
1081
+ ? { id: p.seller.id, name: p.seller.full_name ?? p.seller.name, email: p.seller.email }
1082
+ : undefined,
1083
+ categories: p.categories,
1084
+ listings,
1085
+ createdAt: p.created_at,
1086
+ updatedAt: p.updated_at,
1087
+ };
1088
+ }
1089
+ }