@sanity/client 8.2.0 → 8.4.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.
Files changed (43) hide show
  1. package/README.md +2 -2
  2. package/dist/{browserUpload-2tz6Sdqp.js → browserUpload-C7PwCs-C.js} +6 -9
  3. package/dist/browserUpload-C7PwCs-C.js.map +1 -0
  4. package/dist/{browserUpload-CwpNx7Vl.js → browserUpload-D-2Rmfjo.js} +6 -9
  5. package/dist/browserUpload-D-2Rmfjo.js.map +1 -0
  6. package/dist/{config-3wiPP-sZ.js → config-CgJ16jET.js} +4 -2
  7. package/dist/config-CgJ16jET.js.map +1 -0
  8. package/dist/csm.js +1 -1
  9. package/dist/{dist-C9ExSk2R.js → dist-C5K_YcEU.js} +3 -2
  10. package/dist/{dist-C9ExSk2R.js.map → dist-C5K_YcEU.js.map} +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +942 -115
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.node.d.ts +3345 -16
  15. package/dist/index.node.js +865 -59
  16. package/dist/index.node.js.map +1 -1
  17. package/dist/media-library.d.ts +1 -1
  18. package/dist/rolldown-runtime-4YWMqDIC.js +9 -0
  19. package/dist/{stegaEncodeSourceMap-DbM2fTN4.js → stegaEncodeSourceMap-CO1HKnm2.js} +2 -2
  20. package/dist/{stegaEncodeSourceMap-DbM2fTN4.js.map → stegaEncodeSourceMap-CO1HKnm2.js.map} +1 -1
  21. package/dist/{types-nJhm5Nyq.d.ts → types-DiPF0ENT.d.ts} +3346 -17
  22. package/package.json +3 -3
  23. package/src/SanityClient.ts +7 -0
  24. package/src/assets/AssetsClient.ts +5 -0
  25. package/src/collaboration/types.ts +59 -8
  26. package/src/config.ts +1 -0
  27. package/src/context/ContextClient.ts +1006 -0
  28. package/src/context/openapi.json +5345 -0
  29. package/src/context/reads.ts +206 -0
  30. package/src/context/store.ts +100 -0
  31. package/src/context/types.gen.ts +2428 -0
  32. package/src/context/types.ts +228 -0
  33. package/src/data/dataMethods.ts +4 -1
  34. package/src/defineCreateClient.ts +1 -0
  35. package/src/functions/FunctionsClient.ts +52 -9
  36. package/src/functions/invoke.ts +47 -6
  37. package/src/http/browserUpload.ts +0 -12
  38. package/src/types.ts +29 -3
  39. package/src/util/createVersionId.ts +15 -5
  40. package/src/validators.ts +1 -0
  41. package/dist/browserUpload-2tz6Sdqp.js.map +0 -1
  42. package/dist/browserUpload-CwpNx7Vl.js.map +0 -1
  43. package/dist/config-3wiPP-sZ.js.map +0 -1
@@ -0,0 +1,1006 @@
1
+ import {type FetchFunction} from 'get-it'
2
+ import {lastValueFrom, type Observable} from 'rxjs'
3
+
4
+ import {_observe, _request} from '../data/dataMethods'
5
+ import type {ListenEventFromOptions} from '../data/listen'
6
+ import type {ObservableSanityClient, SanityClient} from '../SanityClient'
7
+ import type {HttpRequest, QueryParams, SanityDocument} from '../types'
8
+ import {
9
+ _listEntries,
10
+ _listInstructions,
11
+ _listIssues,
12
+ _listMcpEndpoints,
13
+ _readConversation,
14
+ _readEntry,
15
+ _readIssue,
16
+ _readMcpEndpoint,
17
+ } from './reads'
18
+ import {_fetch as _fetchStore, _listen as _listenStore} from './store'
19
+ import type {
20
+ ApplyIssuesParams,
21
+ ApplyIssuesResponse,
22
+ ClassifyConversationParams,
23
+ ContextListenOptions,
24
+ ContextRequestOptions,
25
+ Conversation,
26
+ ConversationDoc,
27
+ CreateFileImportParams,
28
+ CreateImportParams,
29
+ CreateInstructionParams,
30
+ CreateInstructionResponse,
31
+ CreateKnowledgeBaseParams,
32
+ DismissIssueResponse,
33
+ EditInstructionParams,
34
+ EditKnowledgeBaseParams,
35
+ Entry,
36
+ EntryDoc,
37
+ ImportDetail,
38
+ ImportDownloadResponse,
39
+ ImportsResponse,
40
+ Instruction,
41
+ InstructionDoc,
42
+ IssueDoc,
43
+ Job,
44
+ JobAccepted,
45
+ KnowledgeBase,
46
+ KnowledgeBasesResponse,
47
+ McpDoc,
48
+ RebuildEntryResponse,
49
+ ReopenIssueResponse,
50
+ RequestOptions,
51
+ ResolveIssueParams,
52
+ ResolveIssueResponse,
53
+ SaveConversationParams,
54
+ Source,
55
+ SourceContentResponse,
56
+ SourceDetail,
57
+ SourcesResponse,
58
+ StagedUpload,
59
+ } from './types'
60
+
61
+ type ListOptions = RequestOptions & {cursor?: string; limit?: number}
62
+
63
+ type Client = SanityClient | ObservableSanityClient
64
+
65
+ const COLLECTION_URL = '/context/knowledge-bases'
66
+
67
+ /**
68
+ * The knowledge base every scoped method operates on comes from the client's
69
+ * `resource` configuration, matching how media libraries and canvases are
70
+ * addressed. Resolved per call so `withConfig` clones behave.
71
+ */
72
+ function _resolveKnowledgeBaseId(client: Client): string {
73
+ const resource = client.config().resource
74
+
75
+ if (resource?.type !== 'knowledge-base') {
76
+ throw new Error(
77
+ '`resource` of type `knowledge-base` must be configured to use knowledge-base methods',
78
+ )
79
+ }
80
+
81
+ return resource.id
82
+ }
83
+
84
+ function _knowledgeBaseUrl(knowledgeBaseId: string, suffix = ''): string {
85
+ return `${COLLECTION_URL}/${encodeURIComponent(knowledgeBaseId)}${suffix}`
86
+ }
87
+
88
+ /** Serialize defined values into query params, dropping the undefined ones. */
89
+ function _conversationUrl(client: Client, threadId: string): string {
90
+ const organizationId = client.config().context?.organizationId
91
+
92
+ if (!organizationId) {
93
+ throw new Error('`context.organizationId` must be configured to record conversations')
94
+ }
95
+
96
+ if (!threadId) {
97
+ throw new Error('`threadId` must be provided')
98
+ }
99
+
100
+ return `/context/organizations/${encodeURIComponent(organizationId)}/conversations/${encodeURIComponent(threadId)}`
101
+ }
102
+
103
+ function _query(entries: Record<string, string | number | undefined>): Record<string, string> {
104
+ return Object.fromEntries(
105
+ Object.entries(entries).flatMap(([key, value]) =>
106
+ value === undefined ? [] : [[key, `${value}`]],
107
+ ),
108
+ )
109
+ }
110
+
111
+ function _fetchBody(file: CreateFileImportParams['file']) {
112
+ if (!ArrayBuffer.isView(file)) return file
113
+ return file.buffer instanceof ArrayBuffer
114
+ ? new Uint8Array(file.buffer, file.byteOffset, file.byteLength)
115
+ : Uint8Array.from(file)
116
+ }
117
+
118
+ /**
119
+ * `client.context` — knowledge bases and everything scoped to them.
120
+ *
121
+ * Collection-level management (create, list, get, edit, delete) addresses
122
+ * knowledge bases per call, like `client.projects`. Everything scoped to one
123
+ * knowledge base (imports, builds, issues, entries, ...) operates on the
124
+ * client's configured `resource`, like media libraries:
125
+ *
126
+ * @example Full lifecycle
127
+ * ```ts
128
+ * const created = await client.context.knowledgeBases.create({
129
+ * organizationId: 'org123',
130
+ * title: 'Support docs',
131
+ * description: 'Product docs and troubleshooting guides',
132
+ * })
133
+ *
134
+ * const kb = createClient({
135
+ * apiVersion: '2026-08-25',
136
+ * token,
137
+ * resource: {type: 'knowledge-base', id: created.publicId},
138
+ * })
139
+ *
140
+ * await kb.context.imports.create({type: 'text', title: 'Refund policy', content: refundMd})
141
+ * const {jobId} = await kb.context.build()
142
+ * ```
143
+ *
144
+ * @beta
145
+ */
146
+ export class ContextClient {
147
+ #client: SanityClient
148
+ #httpRequest: HttpRequest
149
+
150
+ constructor(client: SanityClient, httpRequest: HttpRequest) {
151
+ this.#client = client
152
+ this.#httpRequest = httpRequest
153
+ }
154
+
155
+ /** Request against the configured knowledge base. */
156
+ #request<R>(
157
+ suffix: string,
158
+ reqOptions: {
159
+ method?: string
160
+ body?: unknown
161
+ query?: Record<string, string>
162
+ } & RequestOptions = {},
163
+ ): Promise<R> {
164
+ return _request<R>(this.#client, this.#httpRequest, {
165
+ url: _knowledgeBaseUrl(_resolveKnowledgeBaseId(this.#client), suffix),
166
+ ...reqOptions,
167
+ })
168
+ }
169
+
170
+ /** Shared shape of every paginated list endpoint scoped to the knowledge base. */
171
+ #list<R>(
172
+ suffix: string,
173
+ params?: ListOptions,
174
+ extraQuery?: Record<string, string | undefined>,
175
+ ): Promise<R> {
176
+ return this.#request<R>(suffix, {
177
+ query: _query({
178
+ cursor: params?.cursor,
179
+ limit: params?.limit,
180
+ ...extraQuery,
181
+ }),
182
+ signal: params?.signal,
183
+ tag: params?.tag,
184
+ })
185
+ }
186
+
187
+ async #uploadFile(
188
+ params: CreateFileImportParams,
189
+ options?: RequestOptions,
190
+ ): Promise<JobAccepted> {
191
+ const staged = await this.#request<StagedUpload>('/imports/uploads', {
192
+ method: 'POST',
193
+ body: {
194
+ filename: params.filename,
195
+ ...(params.contentType && {contentType: params.contentType}),
196
+ },
197
+ ...options,
198
+ })
199
+ // The signed URL PUT goes straight to storage, outside the API pipeline:
200
+ // no auth header, and the body is raw bytes rather than JSON. Still uses
201
+ // the client's fetch resolution so proxy config applies in Node.
202
+ const config = this.#client.config()
203
+ const doFetch: FetchFunction = config.resolveFetch?.(config.proxy) ?? globalThis.fetch
204
+ const putResponse = await doFetch(staged.uploadUrl, {
205
+ method: 'PUT',
206
+ body: _fetchBody(params.file),
207
+ ...(params.contentType && {
208
+ headers: {'content-type': params.contentType},
209
+ }),
210
+ signal: options?.signal,
211
+ })
212
+ if (!putResponse.ok) {
213
+ throw new Error(`File upload failed: ${putResponse.status} ${putResponse.statusText}`)
214
+ }
215
+ return this.#request<JobAccepted>(
216
+ `/imports/uploads/${encodeURIComponent(staged.importId)}/complete`,
217
+ {
218
+ method: 'POST',
219
+ body: {},
220
+ ...options,
221
+ },
222
+ )
223
+ }
224
+
225
+ /** The knowledge base collection: management addressed per call. */
226
+ knowledgeBases = {
227
+ /** Create a knowledge base. Requires the org-level knowledge-base create grant. */
228
+ create: (params: CreateKnowledgeBaseParams, options?: RequestOptions): Promise<KnowledgeBase> =>
229
+ _request<KnowledgeBase>(this.#client, this.#httpRequest, {
230
+ url: COLLECTION_URL,
231
+ method: 'POST',
232
+ body: params,
233
+ ...options,
234
+ }),
235
+ /** List the organization's knowledge bases. */
236
+ list: (params: {organizationId: string} & ListOptions): Promise<KnowledgeBasesResponse> =>
237
+ _request<KnowledgeBasesResponse>(this.#client, this.#httpRequest, {
238
+ url: COLLECTION_URL,
239
+ query: _query({
240
+ organizationId: params.organizationId,
241
+ cursor: params.cursor,
242
+ limit: params.limit,
243
+ }),
244
+ signal: params.signal,
245
+ tag: params.tag,
246
+ }),
247
+ /** Fetch a knowledge base by its id. */
248
+ get: (knowledgeBaseId: string, options?: RequestOptions): Promise<KnowledgeBase> =>
249
+ _request<KnowledgeBase>(this.#client, this.#httpRequest, {
250
+ url: _knowledgeBaseUrl(knowledgeBaseId),
251
+ ...options,
252
+ }),
253
+ /** Edit a knowledge base's configuration. */
254
+ edit: (
255
+ knowledgeBaseId: string,
256
+ params: EditKnowledgeBaseParams,
257
+ options?: RequestOptions,
258
+ ): Promise<KnowledgeBase> =>
259
+ _request<KnowledgeBase>(this.#client, this.#httpRequest, {
260
+ url: _knowledgeBaseUrl(knowledgeBaseId),
261
+ method: 'PATCH',
262
+ body: params,
263
+ ...options,
264
+ }),
265
+ /** Delete a knowledge base and its generated content. */
266
+ delete: async (knowledgeBaseId: string, options?: RequestOptions): Promise<void> => {
267
+ await _request<void>(this.#client, this.#httpRequest, {
268
+ url: _knowledgeBaseUrl(knowledgeBaseId),
269
+ method: 'DELETE',
270
+ ...options,
271
+ })
272
+ },
273
+ }
274
+
275
+ /**
276
+ * GROQ over the organization's Context documents (conversation telemetry
277
+ * today; the store holds every Context family and the caller's access
278
+ * decides what a query returns, so filter on `_type`).
279
+ *
280
+ * Requires `context.organizationId` in the client configuration.
281
+ */
282
+ fetch<R = unknown>(
283
+ query: string,
284
+ params?: QueryParams,
285
+ options?: ContextRequestOptions,
286
+ ): Promise<R> {
287
+ return lastValueFrom(_fetchStore<R>(this.#client, this.#httpRequest, query, params, options))
288
+ }
289
+
290
+ /**
291
+ * Listen for changes to the organization's Context documents. Mirrors
292
+ * `client.listen(query, params, options)` and emits mutation events by
293
+ * default.
294
+ */
295
+ listen<Opts extends ContextListenOptions | undefined = undefined>(
296
+ query: string,
297
+ params?: QueryParams,
298
+ options?: Opts,
299
+ ): Observable<ListenEventFromOptions<SanityDocument, Opts>> {
300
+ return _listenStore(this.#client, query, params, options)
301
+ }
302
+
303
+ /**
304
+ * Conversation telemetry. `threadId` identifies the conversation within
305
+ * the organization — reuse means the same conversation. Beyond the canned
306
+ * `get`, reads go through {@link fetch} and {@link listen} with GROQ
307
+ * (`_type == "sanity.context.conversation"`).
308
+ *
309
+ * Requires `context.organizationId` in the client configuration.
310
+ */
311
+ conversations = {
312
+ /**
313
+ * Record a conversation. Messages replace the stored transcript
314
+ * wholesale; `metadata` and model fields only overwrite when present.
315
+ * Last write per thread wins — retries are safe.
316
+ */
317
+ save: (
318
+ params: {threadId: string} & SaveConversationParams,
319
+ options?: RequestOptions,
320
+ ): Promise<Conversation> => {
321
+ const {threadId, ...body} = params
322
+ return _request<Conversation>(this.#client, this.#httpRequest, {
323
+ url: _conversationUrl(this.#client, threadId),
324
+ method: 'PUT',
325
+ body,
326
+ ...options,
327
+ })
328
+ },
329
+ /**
330
+ * Record the classification your own model produced for one thread:
331
+ * exactly one of `coreMetrics` (a verdict) or `classificationError`
332
+ * (why classification failed).
333
+ */
334
+ classify: (
335
+ params: {threadId: string} & ClassifyConversationParams,
336
+ options?: RequestOptions,
337
+ ): Promise<Conversation> => {
338
+ const {threadId, ...body} = params
339
+ return _request<Conversation>(this.#client, this.#httpRequest, {
340
+ url: _conversationUrl(this.#client, threadId),
341
+ method: 'PATCH',
342
+ body,
343
+ ...options,
344
+ })
345
+ },
346
+ /**
347
+ * One recorded conversation by its thread id, or `null` when the thread
348
+ * was never recorded. Runs:
349
+ *
350
+ * `*[_type == "sanity.context.conversation" && organizationId == $org && threadId == $threadId][0]`
351
+ *
352
+ * For anything more, use {@link fetch}.
353
+ */
354
+ get: (
355
+ params: {threadId: string},
356
+ options?: ContextRequestOptions,
357
+ ): Promise<ConversationDoc | null> =>
358
+ _readConversation(this.#client, this.#httpRequest, params.threadId, options),
359
+ }
360
+
361
+ /**
362
+ * Build the configured knowledge base. The server waits for pending import
363
+ * processing before assembling, so importing and building back to back is
364
+ * safe. Track the returned job with {@link jobs}.
365
+ */
366
+ build(options?: RequestOptions): Promise<JobAccepted> {
367
+ return this.#request('/build', {method: 'POST', ...options})
368
+ }
369
+
370
+ /** Cancel the running build, if any. */
371
+ cancelBuild(options?: RequestOptions): Promise<{cancelled: boolean}> {
372
+ return this.#request('/build/cancel', {method: 'POST', ...options})
373
+ }
374
+
375
+ /** Run an incremental refresh: re-check sources and apply what changed. */
376
+ refresh(options?: RequestOptions): Promise<{jobId: string; started: boolean}> {
377
+ return this.#request('/refresh', {method: 'POST', ...options})
378
+ }
379
+
380
+ /** Imports: feed content into the configured knowledge base. */
381
+ imports = {
382
+ /**
383
+ * Import content. One entry point, discriminated on `type`: inline
384
+ * `text`, a website `crawl`, a Sanity `dataset` bind, or a `file`
385
+ * upload. Processing queues automatically. The file variant stages the
386
+ * upload, PUTs the bytes to a signed storage URL, and confirms; the
387
+ * bytes never pass through the Context API.
388
+ */
389
+ create: (
390
+ params: CreateImportParams | CreateFileImportParams,
391
+ options?: RequestOptions,
392
+ ): Promise<JobAccepted> =>
393
+ params.type === 'file'
394
+ ? this.#uploadFile(params, options)
395
+ : this.#request('/imports', {
396
+ method: 'POST',
397
+ body: params,
398
+ ...options,
399
+ }),
400
+ list: (params?: ListOptions) => this.#list<ImportsResponse>('/imports', params),
401
+ get: (params: {importId: string}, options?: RequestOptions) =>
402
+ this.#request<ImportDetail>(`/imports/${encodeURIComponent(params.importId)}`, options),
403
+ /** A short-lived signed URL for the original uploaded bytes. */
404
+ download: (params: {importId: string}, options?: RequestOptions) =>
405
+ this.#request<ImportDownloadResponse>(
406
+ `/imports/${encodeURIComponent(params.importId)}/download`,
407
+ options,
408
+ ),
409
+ delete: async (params: {importId: string}, options?: RequestOptions) => {
410
+ await this.#request<void>(`/imports/${encodeURIComponent(params.importId)}`, {
411
+ method: 'DELETE',
412
+ ...options,
413
+ })
414
+ },
415
+ }
416
+
417
+ /** Jobs: poll async work (builds, imports) to a terminal state. */
418
+ jobs = {
419
+ get: (params: {jobId: string}, options?: RequestOptions) =>
420
+ this.#request<Job>(`/jobs/${encodeURIComponent(params.jobId)}`, options),
421
+ }
422
+
423
+ /**
424
+ * Issues: findings from builds awaiting triage. Reads are canned GROQ
425
+ * queries against the organization's document store; for anything more,
426
+ * use {@link fetch}. Reads require `context.organizationId` alongside the
427
+ * knowledge-base `resource` in the client configuration.
428
+ */
429
+ issues = {
430
+ /**
431
+ * Every issue on the knowledge base, oldest first, optionally narrowed
432
+ * to one status. Drains keyset pages internally and resolves with the
433
+ * complete set. Runs:
434
+ *
435
+ * `*[_type == "sanity.context.issue" && knowledgeBaseId == $kb && status == $status] | order(_createdAt asc, _id asc)`
436
+ *
437
+ * (the status clause only when given). For anything more, use {@link fetch}.
438
+ */
439
+ list: (
440
+ params?: {status?: 'open' | 'accepted' | 'rejected'},
441
+ options?: ContextRequestOptions,
442
+ ): Promise<IssueDoc[]> =>
443
+ _listIssues(
444
+ this.#client,
445
+ this.#httpRequest,
446
+ _resolveKnowledgeBaseId(this.#client),
447
+ params?.status,
448
+ options,
449
+ ),
450
+ /**
451
+ * One issue by its document id, or `null` when it does not exist. Runs:
452
+ *
453
+ * `*[_type == "sanity.context.issue" && knowledgeBaseId == $kb && _id == $id][0]`
454
+ *
455
+ * For anything more, use {@link fetch}.
456
+ */
457
+ get: (params: {issueId: string}, options?: ContextRequestOptions): Promise<IssueDoc | null> =>
458
+ _readIssue(
459
+ this.#client,
460
+ this.#httpRequest,
461
+ _resolveKnowledgeBaseId(this.#client),
462
+ params.issueId,
463
+ options,
464
+ ),
465
+ /** Resolve a conflict issue. Mints the standing instruction, same as the dashboard. */
466
+ resolve: (params: {issueId: string} & ResolveIssueParams, options?: RequestOptions) => {
467
+ const {issueId, ...body} = params
468
+ return this.#request<ResolveIssueResponse>(`/issues/${encodeURIComponent(issueId)}/resolve`, {
469
+ method: 'POST',
470
+ body,
471
+ ...options,
472
+ })
473
+ },
474
+ dismiss: (params: {issueId: string}, options?: RequestOptions) =>
475
+ this.#request<DismissIssueResponse>(`/issues/${encodeURIComponent(params.issueId)}/dismiss`, {
476
+ method: 'POST',
477
+ ...options,
478
+ }),
479
+ reopen: (params: {issueId: string}, options?: RequestOptions) =>
480
+ this.#request<ReopenIssueResponse>(`/issues/${encodeURIComponent(params.issueId)}/reopen`, {
481
+ method: 'POST',
482
+ ...options,
483
+ }),
484
+ /** Apply already-accepted issues to the knowledge base in one batch. */
485
+ apply: (params: ApplyIssuesParams, options?: RequestOptions) =>
486
+ this.#request<ApplyIssuesResponse>('/issues/apply', {
487
+ method: 'POST',
488
+ body: params,
489
+ ...options,
490
+ }),
491
+ }
492
+
493
+ /** Instructions: standing decisions that steer every build. */
494
+ instructions = {
495
+ create: (
496
+ params: CreateInstructionParams,
497
+ options?: RequestOptions,
498
+ ): Promise<CreateInstructionResponse> =>
499
+ this.#request<CreateInstructionResponse>('/instructions', {
500
+ method: 'POST',
501
+ body: params,
502
+ ...options,
503
+ }),
504
+ /**
505
+ * Every current-schema instruction on the knowledge base, oldest first.
506
+ * Drains keyset pages internally and resolves with the complete set.
507
+ * Runs:
508
+ *
509
+ * `*[_type == "sanity.context.instruction" && knowledgeBaseId == $kb && schemaVersion == 1] | order(_createdAt asc, _id asc)`
510
+ *
511
+ * For anything more, use {@link fetch}. Requires `context.organizationId`
512
+ * alongside the knowledge-base `resource` in the client configuration.
513
+ */
514
+ list: (options?: ContextRequestOptions): Promise<InstructionDoc[]> =>
515
+ _listInstructions(
516
+ this.#client,
517
+ this.#httpRequest,
518
+ _resolveKnowledgeBaseId(this.#client),
519
+ options,
520
+ ),
521
+ edit: (params: {instructionId: string} & EditInstructionParams, options?: RequestOptions) => {
522
+ const {instructionId, ...body} = params
523
+ return this.#request<Instruction>(`/instructions/${encodeURIComponent(instructionId)}`, {
524
+ method: 'PATCH',
525
+ body,
526
+ ...options,
527
+ })
528
+ },
529
+ delete: async (params: {instructionId: string}, options?: RequestOptions) => {
530
+ await this.#request<void>(`/instructions/${encodeURIComponent(params.instructionId)}`, {
531
+ method: 'DELETE',
532
+ ...options,
533
+ })
534
+ },
535
+ }
536
+
537
+ /**
538
+ * Entries: the built outline, one entry per node. Reads are canned GROQ
539
+ * queries against the organization's document store; for anything more,
540
+ * use {@link fetch}. Requires `context.organizationId` alongside the
541
+ * knowledge-base `resource` in the client configuration.
542
+ */
543
+ entries = {
544
+ /**
545
+ * Every entry, path-ordered, as a metadata view (`_id`, `path`,
546
+ * `title`, `tldr`, `status`) with bodies excluded. Drains keyset pages
547
+ * internally and resolves with the complete set. Runs:
548
+ *
549
+ * `*[_type == "sanity.context.entry" && knowledgeBaseId == $kb && path > $after] | order(path asc) [0...200] {_id, path, title, tldr, status}`
550
+ *
551
+ * For bodies, use `entries.get` or {@link fetch}.
552
+ */
553
+ list: (options?: ContextRequestOptions): Promise<Entry[]> =>
554
+ _listEntries(this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), options),
555
+ /**
556
+ * One entry with its full body and citations, by outline path (e.g.
557
+ * `billing/refunds`), or `null` when no entry sits at that path. Runs:
558
+ *
559
+ * `*[_type == "sanity.context.entry" && knowledgeBaseId == $kb && path == $path][0]`
560
+ *
561
+ * For anything more, use {@link fetch}.
562
+ */
563
+ get: (params: {path: string}, options?: ContextRequestOptions): Promise<EntryDoc | null> =>
564
+ _readEntry(
565
+ this.#client,
566
+ this.#httpRequest,
567
+ _resolveKnowledgeBaseId(this.#client),
568
+ params.path,
569
+ options,
570
+ ),
571
+ /**
572
+ * Rebuild one entry from its already-placed sources, by outline path.
573
+ * Poll the returned job with {@link jobs}; `affectedEntries` lists every
574
+ * entry the rebuild touches.
575
+ */
576
+ rebuild: (params: {path: string}, options?: RequestOptions): Promise<RebuildEntryResponse> =>
577
+ this.#request<RebuildEntryResponse>(`/entries/${encodeURIComponent(params.path)}/rebuild`, {
578
+ method: 'POST',
579
+ ...options,
580
+ }),
581
+ }
582
+
583
+ /**
584
+ * MCP endpoint configurations, org-owned documents read with canned GROQ
585
+ * queries. Requires `context.organizationId` in the client configuration.
586
+ */
587
+ mcpEndpoints = {
588
+ /**
589
+ * The organization's MCP endpoint configurations, oldest first. Runs:
590
+ *
591
+ * `*[_type == "sanity.context.mcp" && organizationId == $org] | order(_createdAt asc, _id asc) [0...500]`
592
+ *
593
+ * For anything more, use {@link fetch}.
594
+ */
595
+ list: (options?: ContextRequestOptions): Promise<McpDoc[]> =>
596
+ _listMcpEndpoints(this.#client, this.#httpRequest, options),
597
+ /**
598
+ * One MCP endpoint configuration by its URL name, or `null` when none
599
+ * carries that name. Runs:
600
+ *
601
+ * `*[_type == "sanity.context.mcp" && organizationId == $org && name == $name][0]`
602
+ *
603
+ * For anything more, use {@link fetch}.
604
+ */
605
+ get: (params: {name: string}, options?: ContextRequestOptions): Promise<McpDoc | null> =>
606
+ _readMcpEndpoint(this.#client, this.#httpRequest, params.name, options),
607
+ }
608
+
609
+ /** Sources: the distilled units builds cite. */
610
+ sources = {
611
+ /**
612
+ * List sources, optionally filtered by `status` or the `importId` they
613
+ * came from. `ids` is a lookup mode: it resolves those exact sources
614
+ * (e.g. from an entry's citations) and overrides `status` and `cursor`.
615
+ */
616
+ list: (
617
+ params?: {
618
+ status?: Source['status']
619
+ importId?: string
620
+ ids?: string[]
621
+ } & ListOptions,
622
+ ) =>
623
+ this.#list<SourcesResponse>('/sources', params, {
624
+ status: params?.status,
625
+ importId: params?.importId,
626
+ ids: params?.ids?.join(','),
627
+ }),
628
+ get: (params: {sourceId: string}, options?: RequestOptions) =>
629
+ this.#request<SourceDetail>(`/sources/${encodeURIComponent(params.sourceId)}`, options),
630
+ /**
631
+ * Distilled source content, optionally a line range: the evidence behind
632
+ * a citation or an issue.
633
+ */
634
+ content: (
635
+ params: {sourceId: string; startLine?: number; endLine?: number},
636
+ options?: RequestOptions,
637
+ ) =>
638
+ this.#request<SourceContentResponse>(
639
+ `/sources/${encodeURIComponent(params.sourceId)}/content`,
640
+ {
641
+ query: _query({
642
+ startLine: params.startLine,
643
+ endLine: params.endLine,
644
+ }),
645
+ ...options,
646
+ },
647
+ ),
648
+ delete: async (params: {sourceId: string}, options?: RequestOptions) => {
649
+ await this.#request<void>(`/sources/${encodeURIComponent(params.sourceId)}`, {
650
+ method: 'DELETE',
651
+ ...options,
652
+ })
653
+ },
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Observable counterpart of {@link ContextClient}. Collection-level
659
+ * methods and the GROQ-backed reads; knowledge-base scoped write
660
+ * operations are promise-based, so use the promise client
661
+ * (`client.context`) for those.
662
+ *
663
+ * @beta
664
+ */
665
+ export class ObservableContextClient {
666
+ #client: ObservableSanityClient
667
+ #httpRequest: HttpRequest
668
+
669
+ constructor(client: ObservableSanityClient, httpRequest: HttpRequest) {
670
+ this.#client = client
671
+ this.#httpRequest = httpRequest
672
+ }
673
+
674
+ /** The knowledge base collection: management addressed per call. */
675
+ knowledgeBases = {
676
+ /** Create a knowledge base. Requires the org-level knowledge-base create grant. */
677
+ create: (
678
+ params: CreateKnowledgeBaseParams,
679
+ options?: RequestOptions,
680
+ ): Observable<KnowledgeBase> =>
681
+ _observe(options?.signal, (signal) =>
682
+ _request<KnowledgeBase>(this.#client, this.#httpRequest, {
683
+ url: COLLECTION_URL,
684
+ method: 'POST',
685
+ body: params,
686
+ tag: options?.tag,
687
+ signal,
688
+ }),
689
+ ),
690
+ /** List the organization's knowledge bases. */
691
+ list: (params: {organizationId: string} & ListOptions): Observable<KnowledgeBasesResponse> =>
692
+ _observe(params.signal, (signal) =>
693
+ _request<KnowledgeBasesResponse>(this.#client, this.#httpRequest, {
694
+ url: COLLECTION_URL,
695
+ query: _query({
696
+ organizationId: params.organizationId,
697
+ cursor: params.cursor,
698
+ limit: params.limit,
699
+ }),
700
+ tag: params.tag,
701
+ signal,
702
+ }),
703
+ ),
704
+ /** Fetch a knowledge base by its id. */
705
+ get: (knowledgeBaseId: string, options?: RequestOptions): Observable<KnowledgeBase> =>
706
+ _observe(options?.signal, (signal) =>
707
+ _request<KnowledgeBase>(this.#client, this.#httpRequest, {
708
+ url: _knowledgeBaseUrl(knowledgeBaseId),
709
+ tag: options?.tag,
710
+ signal,
711
+ }),
712
+ ),
713
+ /** Edit a knowledge base's configuration. */
714
+ edit: (
715
+ knowledgeBaseId: string,
716
+ params: EditKnowledgeBaseParams,
717
+ options?: RequestOptions,
718
+ ): Observable<KnowledgeBase> =>
719
+ _observe(options?.signal, (signal) =>
720
+ _request<KnowledgeBase>(this.#client, this.#httpRequest, {
721
+ url: _knowledgeBaseUrl(knowledgeBaseId),
722
+ method: 'PATCH',
723
+ body: params,
724
+ tag: options?.tag,
725
+ signal,
726
+ }),
727
+ ),
728
+ /** Delete a knowledge base and its generated content. */
729
+ delete: (knowledgeBaseId: string, options?: RequestOptions): Observable<void> =>
730
+ _observe(options?.signal, (signal) =>
731
+ _request<void>(this.#client, this.#httpRequest, {
732
+ url: _knowledgeBaseUrl(knowledgeBaseId),
733
+ method: 'DELETE',
734
+ tag: options?.tag,
735
+ signal,
736
+ }),
737
+ ),
738
+ }
739
+
740
+ /**
741
+ * GROQ over the organization's Context documents (conversation telemetry
742
+ * today; the store holds every Context family and the caller's access
743
+ * decides what a query returns, so filter on `_type`).
744
+ *
745
+ * Requires `context.organizationId` in the client configuration.
746
+ */
747
+ fetch<R = unknown>(
748
+ query: string,
749
+ params?: QueryParams,
750
+ options?: ContextRequestOptions,
751
+ ): Observable<R> {
752
+ return _fetchStore<R>(this.#client, this.#httpRequest, query, params, options)
753
+ }
754
+
755
+ /**
756
+ * Listen for changes to the organization's Context documents. Mirrors
757
+ * `client.listen(query, params, options)` and emits mutation events by
758
+ * default.
759
+ */
760
+ listen<Opts extends ContextListenOptions | undefined = undefined>(
761
+ query: string,
762
+ params?: QueryParams,
763
+ options?: Opts,
764
+ ): Observable<ListenEventFromOptions<SanityDocument, Opts>> {
765
+ return _listenStore(this.#client, query, params, options)
766
+ }
767
+
768
+ /**
769
+ * Conversation telemetry. `threadId` identifies the conversation within
770
+ * the organization — reuse means the same conversation. Beyond the canned
771
+ * `get`, reads go through {@link fetch} and {@link listen} with GROQ
772
+ * (`_type == "sanity.context.conversation"`).
773
+ *
774
+ * Requires `context.organizationId` in the client configuration.
775
+ */
776
+ conversations = {
777
+ /**
778
+ * Record a conversation. Messages replace the stored transcript
779
+ * wholesale; `metadata` and model fields only overwrite when present.
780
+ * Last write per thread wins — retries are safe.
781
+ */
782
+ save: (
783
+ params: {threadId: string} & SaveConversationParams,
784
+ options?: RequestOptions,
785
+ ): Observable<Conversation> => {
786
+ const {threadId, ...body} = params
787
+ return _observe(options?.signal, (signal) =>
788
+ _request<Conversation>(this.#client, this.#httpRequest, {
789
+ url: _conversationUrl(this.#client, threadId),
790
+ method: 'PUT',
791
+ body,
792
+ tag: options?.tag,
793
+ signal,
794
+ }),
795
+ )
796
+ },
797
+ /**
798
+ * Record the classification your own model produced for one thread:
799
+ * exactly one of `coreMetrics` (a verdict) or `classificationError`
800
+ * (why classification failed).
801
+ */
802
+ classify: (
803
+ params: {threadId: string} & ClassifyConversationParams,
804
+ options?: RequestOptions,
805
+ ): Observable<Conversation> => {
806
+ const {threadId, ...body} = params
807
+ return _observe(options?.signal, (signal) =>
808
+ _request<Conversation>(this.#client, this.#httpRequest, {
809
+ url: _conversationUrl(this.#client, threadId),
810
+ method: 'PATCH',
811
+ body,
812
+ tag: options?.tag,
813
+ signal,
814
+ }),
815
+ )
816
+ },
817
+ /**
818
+ * One recorded conversation by its thread id, or `null` when the thread
819
+ * was never recorded. Runs:
820
+ *
821
+ * `*[_type == "sanity.context.conversation" && organizationId == $org && threadId == $threadId][0]`
822
+ *
823
+ * For anything more, use {@link fetch}.
824
+ */
825
+ get: (
826
+ params: {threadId: string},
827
+ options?: ContextRequestOptions,
828
+ ): Observable<ConversationDoc | null> =>
829
+ _observe(options?.signal, (signal) =>
830
+ _readConversation(this.#client, this.#httpRequest, params.threadId, {
831
+ ...options,
832
+ signal,
833
+ }),
834
+ ),
835
+ }
836
+
837
+ /**
838
+ * Entries: the built outline, one entry per node. Reads are canned GROQ
839
+ * queries against the organization's document store; for anything more,
840
+ * use {@link fetch}. Requires `context.organizationId` alongside the
841
+ * knowledge-base `resource` in the client configuration.
842
+ */
843
+ entries = {
844
+ /**
845
+ * Every entry, path-ordered, as a metadata view (`_id`, `path`,
846
+ * `title`, `tldr`, `status`) with bodies excluded. Drains keyset pages
847
+ * internally and emits the complete set. Runs:
848
+ *
849
+ * `*[_type == "sanity.context.entry" && knowledgeBaseId == $kb && path > $after] | order(path asc) [0...200] {_id, path, title, tldr, status}`
850
+ *
851
+ * For bodies, use `entries.get` or {@link fetch}.
852
+ */
853
+ list: (options?: ContextRequestOptions): Observable<Entry[]> =>
854
+ _observe(options?.signal, (signal) =>
855
+ _listEntries(this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), {
856
+ ...options,
857
+ signal,
858
+ }),
859
+ ),
860
+ /**
861
+ * One entry with its full body and citations, by outline path (e.g.
862
+ * `billing/refunds`), or `null` when no entry sits at that path. Runs:
863
+ *
864
+ * `*[_type == "sanity.context.entry" && knowledgeBaseId == $kb && path == $path][0]`
865
+ *
866
+ * For anything more, use {@link fetch}.
867
+ */
868
+ get: (params: {path: string}, options?: ContextRequestOptions): Observable<EntryDoc | null> =>
869
+ _observe(options?.signal, (signal) =>
870
+ _readEntry(
871
+ this.#client,
872
+ this.#httpRequest,
873
+ _resolveKnowledgeBaseId(this.#client),
874
+ params.path,
875
+ {...options, signal},
876
+ ),
877
+ ),
878
+ /**
879
+ * Rebuild one entry from its already-placed sources, by outline path.
880
+ * Poll the returned job with the promise client's `jobs`;
881
+ * `affectedEntries` lists every entry the rebuild touches.
882
+ */
883
+ rebuild: (params: {path: string}, options?: RequestOptions): Observable<RebuildEntryResponse> =>
884
+ _observe(options?.signal, (signal) =>
885
+ _request<RebuildEntryResponse>(this.#client, this.#httpRequest, {
886
+ url: _knowledgeBaseUrl(
887
+ _resolveKnowledgeBaseId(this.#client),
888
+ `/entries/${encodeURIComponent(params.path)}/rebuild`,
889
+ ),
890
+ method: 'POST',
891
+ tag: options?.tag,
892
+ signal,
893
+ }),
894
+ ),
895
+ }
896
+
897
+ /**
898
+ * Issues: findings from builds awaiting triage. Reads are canned GROQ
899
+ * queries against the organization's document store; for anything more,
900
+ * use {@link fetch}. Requires `context.organizationId` alongside the
901
+ * knowledge-base `resource` in the client configuration.
902
+ */
903
+ issues = {
904
+ /**
905
+ * Every issue on the knowledge base, oldest first, optionally narrowed
906
+ * to one status. Drains keyset pages internally and emits the complete
907
+ * set. Runs:
908
+ *
909
+ * `*[_type == "sanity.context.issue" && knowledgeBaseId == $kb && status == $status] | order(_createdAt asc, _id asc)`
910
+ *
911
+ * (the status clause only when given). For anything more, use {@link fetch}.
912
+ */
913
+ list: (
914
+ params?: {status?: 'open' | 'accepted' | 'rejected'},
915
+ options?: ContextRequestOptions,
916
+ ): Observable<IssueDoc[]> =>
917
+ _observe(options?.signal, (signal) =>
918
+ _listIssues(
919
+ this.#client,
920
+ this.#httpRequest,
921
+ _resolveKnowledgeBaseId(this.#client),
922
+ params?.status,
923
+ {...options, signal},
924
+ ),
925
+ ),
926
+ /**
927
+ * One issue by its document id, or `null` when it does not exist. Runs:
928
+ *
929
+ * `*[_type == "sanity.context.issue" && knowledgeBaseId == $kb && _id == $id][0]`
930
+ *
931
+ * For anything more, use {@link fetch}.
932
+ */
933
+ get: (
934
+ params: {issueId: string},
935
+ options?: ContextRequestOptions,
936
+ ): Observable<IssueDoc | null> =>
937
+ _observe(options?.signal, (signal) =>
938
+ _readIssue(
939
+ this.#client,
940
+ this.#httpRequest,
941
+ _resolveKnowledgeBaseId(this.#client),
942
+ params.issueId,
943
+ {...options, signal},
944
+ ),
945
+ ),
946
+ }
947
+
948
+ /**
949
+ * Instructions: standing decisions that steer every build. The canned
950
+ * GROQ read; writes are promise-based on `client.context`.
951
+ */
952
+ instructions = {
953
+ /**
954
+ * Every current-schema instruction on the knowledge base, oldest first.
955
+ * Drains keyset pages internally and emits the complete set. Runs:
956
+ *
957
+ * `*[_type == "sanity.context.instruction" && knowledgeBaseId == $kb && schemaVersion == 1] | order(_createdAt asc, _id asc)`
958
+ *
959
+ * For anything more, use {@link fetch}. Requires `context.organizationId`
960
+ * alongside the knowledge-base `resource` in the client configuration.
961
+ */
962
+ list: (options?: ContextRequestOptions): Observable<InstructionDoc[]> =>
963
+ _observe(options?.signal, (signal) =>
964
+ _listInstructions(this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), {
965
+ ...options,
966
+ signal,
967
+ }),
968
+ ),
969
+ }
970
+
971
+ /**
972
+ * MCP endpoint configurations, org-owned documents read with canned GROQ
973
+ * queries. Requires `context.organizationId` in the client configuration.
974
+ */
975
+ mcpEndpoints = {
976
+ /**
977
+ * The organization's MCP endpoint configurations, oldest first. Runs:
978
+ *
979
+ * `*[_type == "sanity.context.mcp" && organizationId == $org] | order(_createdAt asc, _id asc) [0...500]`
980
+ *
981
+ * For anything more, use {@link fetch}.
982
+ */
983
+ list: (options?: ContextRequestOptions): Observable<McpDoc[]> =>
984
+ _observe(options?.signal, (signal) =>
985
+ _listMcpEndpoints(this.#client, this.#httpRequest, {
986
+ ...options,
987
+ signal,
988
+ }),
989
+ ),
990
+ /**
991
+ * One MCP endpoint configuration by its URL name, or `null` when none
992
+ * carries that name. Runs:
993
+ *
994
+ * `*[_type == "sanity.context.mcp" && organizationId == $org && name == $name][0]`
995
+ *
996
+ * For anything more, use {@link fetch}.
997
+ */
998
+ get: (params: {name: string}, options?: ContextRequestOptions): Observable<McpDoc | null> =>
999
+ _observe(options?.signal, (signal) =>
1000
+ _readMcpEndpoint(this.#client, this.#httpRequest, params.name, {
1001
+ ...options,
1002
+ signal,
1003
+ }),
1004
+ ),
1005
+ }
1006
+ }