@babav/knowledge-core-client 0.23.2 → 0.24.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/dist/index.d.ts CHANGED
@@ -224,6 +224,24 @@ export interface BatchUploadUrls {
224
224
  }>;
225
225
  expires_in: number;
226
226
  }
227
+ /** One RESUMABLE GCS upload session (corpora.createUploadSessions). Hand `upload_url` to the
228
+ * browser: it PUTs bytes straight to GCS there, needing NO KC/tenant credential (the URI is the
229
+ * per-object write capability). Resumable → a dropped connection resumes, not restarts. */
230
+ export interface UploadSession {
231
+ filename: string;
232
+ document_id: UUID;
233
+ upload_url: string;
234
+ gcs_uri: string;
235
+ method: "PUT";
236
+ headers: Record<string, string>;
237
+ expires_at: string;
238
+ }
239
+ /** Per-item result of corpora.finalizeUploads (idempotent registration). */
240
+ export interface FinalizeResult {
241
+ document_id: UUID;
242
+ status: "ingesting" | "failed";
243
+ error?: string;
244
+ }
227
245
  export interface Conversation {
228
246
  id: UUID;
229
247
  title: string | null;
@@ -398,7 +416,9 @@ export declare class KnowledgeCoreClient extends HttpBase {
398
416
  /** @param opts.apiKey a TENANT key, supplied by the caller. */
399
417
  constructor(opts: ClientOptions);
400
418
  query(agentId: UUID, body: QueryRequest): Promise<QueryResponse>;
401
- /** Streaming query (SSE). Resolves when the stream ends. */
419
+ /** Streaming query (SSE). Resolves when the stream ends (the Promise resolving is normal, not an
420
+ * error). Pass `signal` and abort() on unmount / when the user cancels or navigates away so the
421
+ * connection doesn't linger. Transient (ends on `done`) — no reopen logic needed. */
402
422
  queryStream(agentId: UUID, body: QueryRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void>;
403
423
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
404
424
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
@@ -496,6 +516,43 @@ export declare class KnowledgeCoreClient extends HttpBase {
496
516
  custom_metadata?: Record<string, unknown>;
497
517
  concurrency?: number;
498
518
  }) => Promise<Document[]>;
519
+ /** (server-side) Mint one RESUMABLE GCS upload session per file. Return only the sessions to the
520
+ * browser; the browser PUTs bytes to each `upload_url` and needs NO KC/tenant credential (the
521
+ * session URI IS the per-object write capability). `origin` = the browser app origin (scopes the
522
+ * session; the bucket CORS must allow PUT/POST from it). */
523
+ createUploadSessions: (id: UUID, files: Array<{
524
+ filename: string;
525
+ content_type?: string;
526
+ size?: number;
527
+ }>, opts?: {
528
+ origin?: string;
529
+ }) => Promise<{
530
+ items: UploadSession[];
531
+ }>;
532
+ /** (server-side) Register documents whose bytes the browser already PUT to GCS. IDEMPOTENT +
533
+ * PER-ITEM — safe to retry; each result is { document_id, status: 'ingesting' | 'failed' }. Call
534
+ * after the browser reports its PUTs done. folder_id/visibility/custom_metadata apply to all
535
+ * items. `document_id` is stable from createUploadSessions, so documents.events (SSE) tracks it. */
536
+ finalizeUploads: (id: UUID, items: Array<{
537
+ document_id: UUID;
538
+ filename: string;
539
+ content_type?: string;
540
+ }>, opts?: {
541
+ folder_id?: UUID;
542
+ visibility?: Visibility;
543
+ custom_metadata?: Record<string, unknown>;
544
+ }) => Promise<{
545
+ results: FinalizeResult[];
546
+ }>;
547
+ /** (server-side) Cancel upload sessions the browser gave up on (idempotent cleanup). Pass the
548
+ * `upload_url` from createUploadSessions so the resumable session is dropped; any pending row is
549
+ * removed too. Safe to call repeatedly. */
550
+ abortUploadSessions: (id: UUID, items: Array<{
551
+ document_id: UUID;
552
+ upload_url?: string;
553
+ }>) => Promise<{
554
+ aborted: UUID[];
555
+ }>;
499
556
  };
500
557
  parse(a: {
501
558
  file: FileData;
@@ -519,7 +576,8 @@ export declare class KnowledgeCoreClient extends HttpBase {
519
576
  limit?: number;
520
577
  cursor?: string;
521
578
  }) => Promise<Page<Document>>;
522
- /** Live ingestion-status stream for ONE folder (same contract as documents.events). */
579
+ /** Live ingestion-status stream for ONE folder. Same contract AND lifecycle rules as
580
+ * `documents.events` (abort on unmount, one per view, open lazily, don't reopen on complete). */
523
581
  events: (folderId: UUID, handlers: DocumentEventHandlers, signal?: AbortSignal) => Promise<void>;
524
582
  };
525
583
  documents: {
@@ -540,9 +598,25 @@ export declare class KnowledgeCoreClient extends HttpBase {
540
598
  deleting: UUID[];
541
599
  count: number;
542
600
  }>;
543
- /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
544
- * connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
545
- * the signal. 501 (onError) if the environment has no doc-status bus fall back to polling. */
601
+ /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). On connect:
602
+ * `onSnapshot` (current state), then `onDocument` deltas as docs transition (pending → ingesting
603
+ * indexed | failed, and deleting deleted), then `onComplete` when nothing is in-flight.
604
+ * 501 (onError) if the env has no doc-status bus — fall back to polling countDocuments().
605
+ *
606
+ * A watcher is a held connection — manage its lifecycle:
607
+ * 1. ALWAYS abort on unmount/navigation via `signal` (else it stays open until the ~30-min cap):
608
+ * const c = new AbortController();
609
+ * kc.documents.events(id, handlers, c.signal).catch(()=>{});
610
+ * return () => c.abort(); // e.g. React useEffect cleanup
611
+ * 2. ONE watcher per view — never one per document/row.
612
+ * 3. Open lazily — only while the view has in-flight docs, or right after an upload. If nothing
613
+ * is in-flight the server sends `complete` and closes immediately (nothing to hold).
614
+ * 4. Do NOT reopen on `onComplete` — that's a permanent connection. Reopen only on NEW work
615
+ * (another upload) or an unexpected disconnect while work remains.
616
+ * 5. De-dupe — keep the AbortController in a ref and abort the previous before opening a new one
617
+ * (guards against re-renders / StrictMode double-mount).
618
+ * 6. On a network drop, reconnect with backoff and rebuild from the fresh `snapshot` (authoritative).
619
+ * 7. The Promise RESOLVING is normal (stream ended: complete or the cap) — only onError is a failure. */
546
620
  events: (corpusId: UUID, handlers: DocumentEventHandlers, signal?: AbortSignal) => Promise<void>;
547
621
  };
548
622
  conversations: {
package/dist/index.js CHANGED
@@ -151,7 +151,9 @@ export class KnowledgeCoreClient extends HttpBase {
151
151
  query(agentId, body) {
152
152
  return this.request("POST", `/v1/agents/${agentId}/query`, { json: body });
153
153
  }
154
- /** Streaming query (SSE). Resolves when the stream ends. */
154
+ /** Streaming query (SSE). Resolves when the stream ends (the Promise resolving is normal, not an
155
+ * error). Pass `signal` and abort() on unmount / when the user cancels or navigates away so the
156
+ * connection doesn't linger. Transient (ends on `done`) — no reopen logic needed. */
155
157
  async queryStream(agentId, body, handlers, signal) {
156
158
  const res = await this.raw("POST", `/v1/agents/${agentId}/query/stream`, { json: body, signal });
157
159
  if (!res.ok || !res.body) {
@@ -281,6 +283,33 @@ export class KnowledgeCoreClient extends HttpBase {
281
283
  } });
282
284
  return res.documents;
283
285
  },
286
+ // --- browser direct-to-GCS uploads (ANY size; the browser NEVER needs an API key) ---
287
+ // Split flow so the browser uploads bytes ITSELF while the tenant key stays server-side:
288
+ // 1. (server) createUploadSessions -> returns a resumable GCS session URI per file
289
+ // 2. (browser) PUT the bytes straight to each session URI (resumable; no credential)
290
+ // 3. (server) finalizeUploads -> register + enqueue; or abortUploadSessions to cancel
291
+ // Use this instead of uploadMany when bytes must not pass through your worker (browser -> GCS
292
+ // direct), e.g. multi-GB files or a browser that can't hold the tenant key.
293
+ /** (server-side) Mint one RESUMABLE GCS upload session per file. Return only the sessions to the
294
+ * browser; the browser PUTs bytes to each `upload_url` and needs NO KC/tenant credential (the
295
+ * session URI IS the per-object write capability). `origin` = the browser app origin (scopes the
296
+ * session; the bucket CORS must allow PUT/POST from it). */
297
+ createUploadSessions: (id, files, opts) => this.request("POST", `/v1/corpora/${id}/documents/upload-sessions`, { json: {
298
+ items: files.map((f) => ({ filename: f.filename, content_type: f.content_type })),
299
+ origin: opts?.origin,
300
+ } }),
301
+ /** (server-side) Register documents whose bytes the browser already PUT to GCS. IDEMPOTENT +
302
+ * PER-ITEM — safe to retry; each result is { document_id, status: 'ingesting' | 'failed' }. Call
303
+ * after the browser reports its PUTs done. folder_id/visibility/custom_metadata apply to all
304
+ * items. `document_id` is stable from createUploadSessions, so documents.events (SSE) tracks it. */
305
+ finalizeUploads: (id, items, opts) => this.request("POST", `/v1/corpora/${id}/documents/finalize`, { json: {
306
+ items, folder_id: opts?.folder_id, visibility: opts?.visibility,
307
+ custom_metadata: opts?.custom_metadata,
308
+ } }),
309
+ /** (server-side) Cancel upload sessions the browser gave up on (idempotent cleanup). Pass the
310
+ * `upload_url` from createUploadSessions so the resumable session is dropped; any pending row is
311
+ * removed too. Safe to call repeatedly. */
312
+ abortUploadSessions: (id, items) => this.request("POST", `/v1/corpora/${id}/documents/abort-uploads`, { json: { items } }),
284
313
  };
285
314
  // --- parsing (utility: file -> text, stores nothing) ---
286
315
  parse(a) {
@@ -301,7 +330,8 @@ export class KnowledgeCoreClient extends HttpBase {
301
330
  countDocuments: (folderId) => this.request("GET", `/v1/folders/${folderId}/documents/count`),
302
331
  /** Documents in the folder whose filename contains `q` (case-insensitive), paginated. */
303
332
  searchDocuments: (folderId, q, opts) => this.request("GET", `/v1/folders/${folderId}/documents/search`, { query: { q, ...opts } }),
304
- /** Live ingestion-status stream for ONE folder (same contract as documents.events). */
333
+ /** Live ingestion-status stream for ONE folder. Same contract AND lifecycle rules as
334
+ * `documents.events` (abort on unmount, one per view, open lazily, don't reopen on complete). */
305
335
  events: (folderId, handlers, signal) => this._streamDocEvents(`/v1/folders/${folderId}/documents/events`, handlers, signal),
306
336
  };
307
337
  // --- documents ---
@@ -317,9 +347,25 @@ export class KnowledgeCoreClient extends HttpBase {
317
347
  * one request. IDs not in the corpus are skipped. Completion observable via documents.events /
318
348
  * documents.get() → 404. Returns { deleting: ids, count }. */
319
349
  deleteMany: (corpusId, documentIds) => this.request("POST", `/v1/corpora/${corpusId}/documents/batch-delete`, { json: { document_ids: documentIds } }),
320
- /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
321
- * connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
322
- * the signal. 501 (onError) if the environment has no doc-status bus fall back to polling. */
350
+ /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). On connect:
351
+ * `onSnapshot` (current state), then `onDocument` deltas as docs transition (pending → ingesting
352
+ * indexed | failed, and deleting deleted), then `onComplete` when nothing is in-flight.
353
+ * 501 (onError) if the env has no doc-status bus — fall back to polling countDocuments().
354
+ *
355
+ * A watcher is a held connection — manage its lifecycle:
356
+ * 1. ALWAYS abort on unmount/navigation via `signal` (else it stays open until the ~30-min cap):
357
+ * const c = new AbortController();
358
+ * kc.documents.events(id, handlers, c.signal).catch(()=>{});
359
+ * return () => c.abort(); // e.g. React useEffect cleanup
360
+ * 2. ONE watcher per view — never one per document/row.
361
+ * 3. Open lazily — only while the view has in-flight docs, or right after an upload. If nothing
362
+ * is in-flight the server sends `complete` and closes immediately (nothing to hold).
363
+ * 4. Do NOT reopen on `onComplete` — that's a permanent connection. Reopen only on NEW work
364
+ * (another upload) or an unexpected disconnect while work remains.
365
+ * 5. De-dupe — keep the AbortController in a ref and abort the previous before opening a new one
366
+ * (guards against re-renders / StrictMode double-mount).
367
+ * 6. On a network drop, reconnect with backoff and rebuild from the fresh `snapshot` (authoritative).
368
+ * 7. The Promise RESOLVING is normal (stream ended: complete or the cap) — only onError is a failure. */
323
369
  events: (corpusId, handlers, signal) => this._streamDocEvents(`/v1/corpora/${corpusId}/documents/events`, handlers, signal),
324
370
  };
325
371
  // --- conversations ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babav/knowledge-core-client",
3
- "version": "0.23.2",
3
+ "version": "0.24.0",
4
4
  "description": "TypeScript client for the Babav Knowledge Core API (Deno + Node 18+, zero deps). Includes the babav.visual grammar TYPES at the ./visual subpath (types only; all visual rendering is server-side).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -240,6 +240,24 @@ export interface BatchUploadUrls {
240
240
  items: Array<{ filename: string; document_id: UUID; upload_url: string; gcs_uri: string }>;
241
241
  expires_in: number;
242
242
  }
243
+ /** One RESUMABLE GCS upload session (corpora.createUploadSessions). Hand `upload_url` to the
244
+ * browser: it PUTs bytes straight to GCS there, needing NO KC/tenant credential (the URI is the
245
+ * per-object write capability). Resumable → a dropped connection resumes, not restarts. */
246
+ export interface UploadSession {
247
+ filename: string;
248
+ document_id: UUID;
249
+ upload_url: string;
250
+ gcs_uri: string;
251
+ method: "PUT";
252
+ headers: Record<string, string>;
253
+ expires_at: string;
254
+ }
255
+ /** Per-item result of corpora.finalizeUploads (idempotent registration). */
256
+ export interface FinalizeResult {
257
+ document_id: UUID;
258
+ status: "ingesting" | "failed";
259
+ error?: string;
260
+ }
243
261
  export interface Conversation {
244
262
  id: UUID;
245
263
  title: string | null;
@@ -525,7 +543,9 @@ export class KnowledgeCoreClient extends HttpBase {
525
543
  return this.request("POST", `/v1/agents/${agentId}/query`, { json: body });
526
544
  }
527
545
 
528
- /** Streaming query (SSE). Resolves when the stream ends. */
546
+ /** Streaming query (SSE). Resolves when the stream ends (the Promise resolving is normal, not an
547
+ * error). Pass `signal` and abort() on unmount / when the user cancels or navigates away so the
548
+ * connection doesn't linger. Transient (ends on `done`) — no reopen logic needed. */
529
549
  async queryStream(agentId: UUID, body: QueryRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void> {
530
550
  const res = await this.raw("POST", `/v1/agents/${agentId}/query/stream`, { json: body, signal });
531
551
  if (!res.ok || !res.body) {
@@ -667,6 +687,58 @@ export class KnowledgeCoreClient extends HttpBase {
667
687
  );
668
688
  return res.documents;
669
689
  },
690
+
691
+ // --- browser direct-to-GCS uploads (ANY size; the browser NEVER needs an API key) ---
692
+ // Split flow so the browser uploads bytes ITSELF while the tenant key stays server-side:
693
+ // 1. (server) createUploadSessions -> returns a resumable GCS session URI per file
694
+ // 2. (browser) PUT the bytes straight to each session URI (resumable; no credential)
695
+ // 3. (server) finalizeUploads -> register + enqueue; or abortUploadSessions to cancel
696
+ // Use this instead of uploadMany when bytes must not pass through your worker (browser -> GCS
697
+ // direct), e.g. multi-GB files or a browser that can't hold the tenant key.
698
+
699
+ /** (server-side) Mint one RESUMABLE GCS upload session per file. Return only the sessions to the
700
+ * browser; the browser PUTs bytes to each `upload_url` and needs NO KC/tenant credential (the
701
+ * session URI IS the per-object write capability). `origin` = the browser app origin (scopes the
702
+ * session; the bucket CORS must allow PUT/POST from it). */
703
+ createUploadSessions: (
704
+ id: UUID,
705
+ files: Array<{ filename: string; content_type?: string; size?: number }>,
706
+ opts?: { origin?: string },
707
+ ): Promise<{ items: UploadSession[] }> =>
708
+ this.request<{ items: UploadSession[] }>(
709
+ "POST", `/v1/corpora/${id}/documents/upload-sessions`,
710
+ { json: {
711
+ items: files.map((f) => ({ filename: f.filename, content_type: f.content_type })),
712
+ origin: opts?.origin,
713
+ } },
714
+ ),
715
+
716
+ /** (server-side) Register documents whose bytes the browser already PUT to GCS. IDEMPOTENT +
717
+ * PER-ITEM — safe to retry; each result is { document_id, status: 'ingesting' | 'failed' }. Call
718
+ * after the browser reports its PUTs done. folder_id/visibility/custom_metadata apply to all
719
+ * items. `document_id` is stable from createUploadSessions, so documents.events (SSE) tracks it. */
720
+ finalizeUploads: (
721
+ id: UUID,
722
+ items: Array<{ document_id: UUID; filename: string; content_type?: string }>,
723
+ opts?: { folder_id?: UUID; visibility?: Visibility; custom_metadata?: Record<string, unknown> },
724
+ ): Promise<{ results: FinalizeResult[] }> =>
725
+ this.request<{ results: FinalizeResult[] }>(
726
+ "POST", `/v1/corpora/${id}/documents/finalize`,
727
+ { json: {
728
+ items, folder_id: opts?.folder_id, visibility: opts?.visibility,
729
+ custom_metadata: opts?.custom_metadata,
730
+ } },
731
+ ),
732
+
733
+ /** (server-side) Cancel upload sessions the browser gave up on (idempotent cleanup). Pass the
734
+ * `upload_url` from createUploadSessions so the resumable session is dropped; any pending row is
735
+ * removed too. Safe to call repeatedly. */
736
+ abortUploadSessions: (
737
+ id: UUID,
738
+ items: Array<{ document_id: UUID; upload_url?: string }>,
739
+ ): Promise<{ aborted: UUID[] }> =>
740
+ this.request<{ aborted: UUID[] }>(
741
+ "POST", `/v1/corpora/${id}/documents/abort-uploads`, { json: { items } }),
670
742
  };
671
743
 
672
744
  // --- parsing (utility: file -> text, stores nothing) ---
@@ -693,7 +765,8 @@ export class KnowledgeCoreClient extends HttpBase {
693
765
  /** Documents in the folder whose filename contains `q` (case-insensitive), paginated. */
694
766
  searchDocuments: (folderId: UUID, q: string, opts?: { limit?: number; cursor?: string }) =>
695
767
  this.request<Page<Document>>("GET", `/v1/folders/${folderId}/documents/search`, { query: { q, ...opts } }),
696
- /** Live ingestion-status stream for ONE folder (same contract as documents.events). */
768
+ /** Live ingestion-status stream for ONE folder. Same contract AND lifecycle rules as
769
+ * `documents.events` (abort on unmount, one per view, open lazily, don't reopen on complete). */
697
770
  events: (folderId: UUID, handlers: DocumentEventHandlers, signal?: AbortSignal) =>
698
771
  this._streamDocEvents(`/v1/folders/${folderId}/documents/events`, handlers, signal),
699
772
  };
@@ -715,9 +788,25 @@ export class KnowledgeCoreClient extends HttpBase {
715
788
  deleteMany: (corpusId: UUID, documentIds: UUID[]) =>
716
789
  this.request<{ deleting: UUID[]; count: number }>(
717
790
  "POST", `/v1/corpora/${corpusId}/documents/batch-delete`, { json: { document_ids: documentIds } }),
718
- /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). Snapshot on
719
- * connect, then a delta per transition, then `complete` when nothing is in-flight. Abort via
720
- * the signal. 501 (onError) if the environment has no doc-status bus fall back to polling. */
791
+ /** Live ingestion-status stream for a corpus (SSE PUSH — replaces polling count). On connect:
792
+ * `onSnapshot` (current state), then `onDocument` deltas as docs transition (pending → ingesting
793
+ * indexed | failed, and deleting deleted), then `onComplete` when nothing is in-flight.
794
+ * 501 (onError) if the env has no doc-status bus — fall back to polling countDocuments().
795
+ *
796
+ * A watcher is a held connection — manage its lifecycle:
797
+ * 1. ALWAYS abort on unmount/navigation via `signal` (else it stays open until the ~30-min cap):
798
+ * const c = new AbortController();
799
+ * kc.documents.events(id, handlers, c.signal).catch(()=>{});
800
+ * return () => c.abort(); // e.g. React useEffect cleanup
801
+ * 2. ONE watcher per view — never one per document/row.
802
+ * 3. Open lazily — only while the view has in-flight docs, or right after an upload. If nothing
803
+ * is in-flight the server sends `complete` and closes immediately (nothing to hold).
804
+ * 4. Do NOT reopen on `onComplete` — that's a permanent connection. Reopen only on NEW work
805
+ * (another upload) or an unexpected disconnect while work remains.
806
+ * 5. De-dupe — keep the AbortController in a ref and abort the previous before opening a new one
807
+ * (guards against re-renders / StrictMode double-mount).
808
+ * 6. On a network drop, reconnect with backoff and rebuild from the fresh `snapshot` (authoritative).
809
+ * 7. The Promise RESOLVING is normal (stream ended: complete or the cap) — only onError is a failure. */
721
810
  events: (corpusId: UUID, handlers: DocumentEventHandlers, signal?: AbortSignal) =>
722
811
  this._streamDocEvents(`/v1/corpora/${corpusId}/documents/events`, handlers, signal),
723
812
  };