UNPKG

@sanity/client

Version:

Client for retrieving, creating and patching data from Sanity.io

1,007 lines (963 loc) 35.2 kB
import {type FetchFunction} from 'get-it' import {lastValueFrom, type Observable} from 'rxjs' import {_observe, _request} from '../data/dataMethods' import type {ListenEventFromOptions} from '../data/listen' import type {ObservableSanityClient, SanityClient} from '../SanityClient' import type {HttpRequest, QueryParams, SanityDocument} from '../types' import { _listEntries, _listInstructions, _listIssues, _listMcpEndpoints, _readConversation, _readEntry, _readIssue, _readMcpEndpoint, } from './reads' import {_fetch as _fetchStore, _listen as _listenStore} from './store' import type { ApplyIssuesParams, ApplyIssuesResponse, ClassifyConversationParams, ContextListenOptions, ContextRequestOptions, Conversation, ConversationDoc, CreateFileImportParams, CreateImportParams, CreateInstructionParams, CreateInstructionResponse, CreateKnowledgeBaseParams, DismissIssueResponse, EditInstructionParams, EditKnowledgeBaseParams, Entry, EntryDoc, ImportDetail, ImportDownloadResponse, ImportsResponse, Instruction, InstructionDoc, IssueDoc, Job, JobAccepted, KnowledgeBase, KnowledgeBasesResponse, McpDoc, RebuildEntryResponse, ReopenIssueResponse, RequestOptions, ResolveIssueParams, ResolveIssueResponse, SaveConversationParams, Source, SourceContentResponse, SourceDetail, SourcesResponse, StagedUpload, } from './types' type ListOptions = RequestOptions & {cursor?: string; limit?: number} type Client = SanityClient | ObservableSanityClient const COLLECTION_URL = '/context/knowledge-bases' /** * The knowledge base every scoped method operates on comes from the client's * `resource` configuration, matching how media libraries and canvases are * addressed. Resolved per call so `withConfig` clones behave. */ function _resolveKnowledgeBaseId(client: Client): string { const resource = client.config().resource if (resource?.type !== 'knowledge-base') { throw new Error( '`resource` of type `knowledge-base` must be configured to use knowledge-base methods', ) } return resource.id } function _knowledgeBaseUrl(knowledgeBaseId: string, suffix = ''): string { return `${COLLECTION_URL}/${encodeURIComponent(knowledgeBaseId)}${suffix}` } /** Serialize defined values into query params, dropping the undefined ones. */ function _conversationUrl(client: Client, threadId: string): string { const organizationId = client.config().context?.organizationId if (!organizationId) { throw new Error('`context.organizationId` must be configured to record conversations') } if (!threadId) { throw new Error('`threadId` must be provided') } return `/context/organizations/${encodeURIComponent(organizationId)}/conversations/${encodeURIComponent(threadId)}` } function _query(entries: Record<string, string | number | undefined>): Record<string, string> { return Object.fromEntries( Object.entries(entries).flatMap(([key, value]) => value === undefined ? [] : [[key, `${value}`]], ), ) } function _fetchBody(file: CreateFileImportParams['file']) { if (!ArrayBuffer.isView(file)) return file return file.buffer instanceof ArrayBuffer ? new Uint8Array(file.buffer, file.byteOffset, file.byteLength) : Uint8Array.from(file) } /** * `client.context` — knowledge bases and everything scoped to them. * * Collection-level management (create, list, get, edit, delete) addresses * knowledge bases per call, like `client.projects`. Everything scoped to one * knowledge base (imports, builds, issues, entries, ...) operates on the * client's configured `resource`, like media libraries: * * @example Full lifecycle * ```ts * const created = await client.context.knowledgeBases.create({ * organizationId: 'org123', * title: 'Support docs', * description: 'Product docs and troubleshooting guides', * }) * * const kb = createClient({ * apiVersion: '2026-08-25', * token, * resource: {type: 'knowledge-base', id: created.publicId}, * }) * * await kb.context.imports.create({type: 'text', title: 'Refund policy', content: refundMd}) * const {jobId} = await kb.context.build() * ``` * * @beta */ export class ContextClient { #client: SanityClient #httpRequest: HttpRequest constructor(client: SanityClient, httpRequest: HttpRequest) { this.#client = client this.#httpRequest = httpRequest } /** Request against the configured knowledge base. */ #request<R>( suffix: string, reqOptions: { method?: string body?: unknown query?: Record<string, string> } & RequestOptions = {}, ): Promise<R> { return _request<R>(this.#client, this.#httpRequest, { url: _knowledgeBaseUrl(_resolveKnowledgeBaseId(this.#client), suffix), ...reqOptions, }) } /** Shared shape of every paginated list endpoint scoped to the knowledge base. */ #list<R>( suffix: string, params?: ListOptions, extraQuery?: Record<string, string | undefined>, ): Promise<R> { return this.#request<R>(suffix, { query: _query({ cursor: params?.cursor, limit: params?.limit, ...extraQuery, }), signal: params?.signal, tag: params?.tag, }) } async #uploadFile( params: CreateFileImportParams, options?: RequestOptions, ): Promise<JobAccepted> { const staged = await this.#request<StagedUpload>('/imports/uploads', { method: 'POST', body: { filename: params.filename, ...(params.contentType && {contentType: params.contentType}), }, ...options, }) // The signed URL PUT goes straight to storage, outside the API pipeline: // no auth header, and the body is raw bytes rather than JSON. Still uses // the client's fetch resolution so proxy config applies in Node. const config = this.#client.config() const doFetch: FetchFunction = config.resolveFetch?.(config.proxy) ?? globalThis.fetch const putResponse = await doFetch(staged.uploadUrl, { method: 'PUT', body: _fetchBody(params.file), ...(params.contentType && { headers: {'content-type': params.contentType}, }), signal: options?.signal, }) if (!putResponse.ok) { throw new Error(`File upload failed: ${putResponse.status} ${putResponse.statusText}`) } return this.#request<JobAccepted>( `/imports/uploads/${encodeURIComponent(staged.importId)}/complete`, { method: 'POST', body: {}, ...options, }, ) } /** The knowledge base collection: management addressed per call. */ knowledgeBases = { /** Create a knowledge base. Requires the org-level knowledge-base create grant. */ create: (params: CreateKnowledgeBaseParams, options?: RequestOptions): Promise<KnowledgeBase> => _request<KnowledgeBase>(this.#client, this.#httpRequest, { url: COLLECTION_URL, method: 'POST', body: params, ...options, }), /** List the organization's knowledge bases. */ list: (params: {organizationId: string} & ListOptions): Promise<KnowledgeBasesResponse> => _request<KnowledgeBasesResponse>(this.#client, this.#httpRequest, { url: COLLECTION_URL, query: _query({ organizationId: params.organizationId, cursor: params.cursor, limit: params.limit, }), signal: params.signal, tag: params.tag, }), /** Fetch a knowledge base by its id. */ get: (knowledgeBaseId: string, options?: RequestOptions): Promise<KnowledgeBase> => _request<KnowledgeBase>(this.#client, this.#httpRequest, { url: _knowledgeBaseUrl(knowledgeBaseId), ...options, }), /** Edit a knowledge base's configuration. */ edit: ( knowledgeBaseId: string, params: EditKnowledgeBaseParams, options?: RequestOptions, ): Promise<KnowledgeBase> => _request<KnowledgeBase>(this.#client, this.#httpRequest, { url: _knowledgeBaseUrl(knowledgeBaseId), method: 'PATCH', body: params, ...options, }), /** Delete a knowledge base and its generated content. */ delete: async (knowledgeBaseId: string, options?: RequestOptions): Promise<void> => { await _request<void>(this.#client, this.#httpRequest, { url: _knowledgeBaseUrl(knowledgeBaseId), method: 'DELETE', ...options, }) }, } /** * GROQ over the organization's Context documents (conversation telemetry * today; the store holds every Context family and the caller's access * decides what a query returns, so filter on `_type`). * * Requires `context.organizationId` in the client configuration. */ fetch<R = unknown>( query: string, params?: QueryParams, options?: ContextRequestOptions, ): Promise<R> { return lastValueFrom(_fetchStore<R>(this.#client, this.#httpRequest, query, params, options)) } /** * Listen for changes to the organization's Context documents. Mirrors * `client.listen(query, params, options)` and emits mutation events by * default. */ listen<Opts extends ContextListenOptions | undefined = undefined>( query: string, params?: QueryParams, options?: Opts, ): Observable<ListenEventFromOptions<SanityDocument, Opts>> { return _listenStore(this.#client, query, params, options) } /** * Conversation telemetry. `threadId` identifies the conversation within * the organization — reuse means the same conversation. Beyond the canned * `get`, reads go through {@link fetch} and {@link listen} with GROQ * (`_type == "sanity.context.conversation"`). * * Requires `context.organizationId` in the client configuration. */ conversations = { /** * Record a conversation. Messages replace the stored transcript * wholesale; `metadata` and model fields only overwrite when present. * Last write per thread wins — retries are safe. */ save: ( params: {threadId: string} & SaveConversationParams, options?: RequestOptions, ): Promise<Conversation> => { const {threadId, ...body} = params return _request<Conversation>(this.#client, this.#httpRequest, { url: _conversationUrl(this.#client, threadId), method: 'PUT', body, ...options, }) }, /** * Record the classification your own model produced for one thread: * exactly one of `coreMetrics` (a verdict) or `classificationError` * (why classification failed). */ classify: ( params: {threadId: string} & ClassifyConversationParams, options?: RequestOptions, ): Promise<Conversation> => { const {threadId, ...body} = params return _request<Conversation>(this.#client, this.#httpRequest, { url: _conversationUrl(this.#client, threadId), method: 'PATCH', body, ...options, }) }, /** * One recorded conversation by its thread id, or `null` when the thread * was never recorded. Runs: * * `*[_type == "sanity.context.conversation" && organizationId == $org && threadId == $threadId][0]` * * For anything more, use {@link fetch}. */ get: ( params: {threadId: string}, options?: ContextRequestOptions, ): Promise<ConversationDoc | null> => _readConversation(this.#client, this.#httpRequest, params.threadId, options), } /** * Build the configured knowledge base. The server waits for pending import * processing before assembling, so importing and building back to back is * safe. Track the returned job with {@link jobs}. */ build(options?: RequestOptions): Promise<JobAccepted> { return this.#request('/build', {method: 'POST', ...options}) } /** Cancel the running build, if any. */ cancelBuild(options?: RequestOptions): Promise<{cancelled: boolean}> { return this.#request('/build/cancel', {method: 'POST', ...options}) } /** Run an incremental refresh: re-check sources and apply what changed. */ refresh(options?: RequestOptions): Promise<{jobId: string; started: boolean}> { return this.#request('/refresh', {method: 'POST', ...options}) } /** Imports: feed content into the configured knowledge base. */ imports = { /** * Import content. One entry point, discriminated on `type`: inline * `text`, a website `crawl`, a Sanity `dataset` bind, or a `file` * upload. Processing queues automatically. The file variant stages the * upload, PUTs the bytes to a signed storage URL, and confirms; the * bytes never pass through the Context API. */ create: ( params: CreateImportParams | CreateFileImportParams, options?: RequestOptions, ): Promise<JobAccepted> => params.type === 'file' ? this.#uploadFile(params, options) : this.#request('/imports', { method: 'POST', body: params, ...options, }), list: (params?: ListOptions) => this.#list<ImportsResponse>('/imports', params), get: (params: {importId: string}, options?: RequestOptions) => this.#request<ImportDetail>(`/imports/${encodeURIComponent(params.importId)}`, options), /** A short-lived signed URL for the original uploaded bytes. */ download: (params: {importId: string}, options?: RequestOptions) => this.#request<ImportDownloadResponse>( `/imports/${encodeURIComponent(params.importId)}/download`, options, ), delete: async (params: {importId: string}, options?: RequestOptions) => { await this.#request<void>(`/imports/${encodeURIComponent(params.importId)}`, { method: 'DELETE', ...options, }) }, } /** Jobs: poll async work (builds, imports) to a terminal state. */ jobs = { get: (params: {jobId: string}, options?: RequestOptions) => this.#request<Job>(`/jobs/${encodeURIComponent(params.jobId)}`, options), } /** * Issues: findings from builds awaiting triage. Reads are canned GROQ * queries against the organization's document store; for anything more, * use {@link fetch}. Reads require `context.organizationId` alongside the * knowledge-base `resource` in the client configuration. */ issues = { /** * Every issue on the knowledge base, oldest first, optionally narrowed * to one status. Drains keyset pages internally and resolves with the * complete set. Runs: * * `*[_type == "sanity.context.issue" && knowledgeBaseId == $kb && status == $status] | order(_createdAt asc, _id asc)` * * (the status clause only when given). For anything more, use {@link fetch}. */ list: ( params?: {status?: 'open' | 'accepted' | 'rejected'}, options?: ContextRequestOptions, ): Promise<IssueDoc[]> => _listIssues( this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), params?.status, options, ), /** * One issue by its document id, or `null` when it does not exist. Runs: * * `*[_type == "sanity.context.issue" && knowledgeBaseId == $kb && _id == $id][0]` * * For anything more, use {@link fetch}. */ get: (params: {issueId: string}, options?: ContextRequestOptions): Promise<IssueDoc | null> => _readIssue( this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), params.issueId, options, ), /** Resolve a conflict issue. Mints the standing instruction, same as the dashboard. */ resolve: (params: {issueId: string} & ResolveIssueParams, options?: RequestOptions) => { const {issueId, ...body} = params return this.#request<ResolveIssueResponse>(`/issues/${encodeURIComponent(issueId)}/resolve`, { method: 'POST', body, ...options, }) }, dismiss: (params: {issueId: string}, options?: RequestOptions) => this.#request<DismissIssueResponse>(`/issues/${encodeURIComponent(params.issueId)}/dismiss`, { method: 'POST', ...options, }), reopen: (params: {issueId: string}, options?: RequestOptions) => this.#request<ReopenIssueResponse>(`/issues/${encodeURIComponent(params.issueId)}/reopen`, { method: 'POST', ...options, }), /** Apply already-accepted issues to the knowledge base in one batch. */ apply: (params: ApplyIssuesParams, options?: RequestOptions) => this.#request<ApplyIssuesResponse>('/issues/apply', { method: 'POST', body: params, ...options, }), } /** Instructions: standing decisions that steer every build. */ instructions = { create: ( params: CreateInstructionParams, options?: RequestOptions, ): Promise<CreateInstructionResponse> => this.#request<CreateInstructionResponse>('/instructions', { method: 'POST', body: params, ...options, }), /** * Every current-schema instruction on the knowledge base, oldest first. * Drains keyset pages internally and resolves with the complete set. * Runs: * * `*[_type == "sanity.context.instruction" && knowledgeBaseId == $kb && schemaVersion == 1] | order(_createdAt asc, _id asc)` * * For anything more, use {@link fetch}. Requires `context.organizationId` * alongside the knowledge-base `resource` in the client configuration. */ list: (options?: ContextRequestOptions): Promise<InstructionDoc[]> => _listInstructions( this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), options, ), edit: (params: {instructionId: string} & EditInstructionParams, options?: RequestOptions) => { const {instructionId, ...body} = params return this.#request<Instruction>(`/instructions/${encodeURIComponent(instructionId)}`, { method: 'PATCH', body, ...options, }) }, delete: async (params: {instructionId: string}, options?: RequestOptions) => { await this.#request<void>(`/instructions/${encodeURIComponent(params.instructionId)}`, { method: 'DELETE', ...options, }) }, } /** * Entries: the built outline, one entry per node. Reads are canned GROQ * queries against the organization's document store; for anything more, * use {@link fetch}. Requires `context.organizationId` alongside the * knowledge-base `resource` in the client configuration. */ entries = { /** * Every entry, path-ordered, as a metadata view (`_id`, `path`, * `title`, `tldr`, `status`) with bodies excluded. Drains keyset pages * internally and resolves with the complete set. Runs: * * `*[_type == "sanity.context.entry" && knowledgeBaseId == $kb && path > $after] | order(path asc) [0...200] {_id, path, title, tldr, status}` * * For bodies, use `entries.get` or {@link fetch}. */ list: (options?: ContextRequestOptions): Promise<Entry[]> => _listEntries(this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), options), /** * One entry with its full body and citations, by outline path (e.g. * `billing/refunds`), or `null` when no entry sits at that path. Runs: * * `*[_type == "sanity.context.entry" && knowledgeBaseId == $kb && path == $path][0]` * * For anything more, use {@link fetch}. */ get: (params: {path: string}, options?: ContextRequestOptions): Promise<EntryDoc | null> => _readEntry( this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), params.path, options, ), /** * Rebuild one entry from its already-placed sources, by outline path. * Poll the returned job with {@link jobs}; `affectedEntries` lists every * entry the rebuild touches. */ rebuild: (params: {path: string}, options?: RequestOptions): Promise<RebuildEntryResponse> => this.#request<RebuildEntryResponse>(`/entries/${encodeURIComponent(params.path)}/rebuild`, { method: 'POST', ...options, }), } /** * MCP endpoint configurations, org-owned documents read with canned GROQ * queries. Requires `context.organizationId` in the client configuration. */ mcpEndpoints = { /** * The organization's MCP endpoint configurations, oldest first. Runs: * * `*[_type == "sanity.context.mcp" && organizationId == $org] | order(_createdAt asc, _id asc) [0...500]` * * For anything more, use {@link fetch}. */ list: (options?: ContextRequestOptions): Promise<McpDoc[]> => _listMcpEndpoints(this.#client, this.#httpRequest, options), /** * One MCP endpoint configuration by its URL name, or `null` when none * carries that name. Runs: * * `*[_type == "sanity.context.mcp" && organizationId == $org && name == $name][0]` * * For anything more, use {@link fetch}. */ get: (params: {name: string}, options?: ContextRequestOptions): Promise<McpDoc | null> => _readMcpEndpoint(this.#client, this.#httpRequest, params.name, options), } /** Sources: the distilled units builds cite. */ sources = { /** * List sources, optionally filtered by `status` or the `importId` they * came from. `ids` is a lookup mode: it resolves those exact sources * (e.g. from an entry's citations) and overrides `status` and `cursor`. */ list: ( params?: { status?: Source['status'] importId?: string ids?: string[] } & ListOptions, ) => this.#list<SourcesResponse>('/sources', params, { status: params?.status, importId: params?.importId, ids: params?.ids?.join(','), }), get: (params: {sourceId: string}, options?: RequestOptions) => this.#request<SourceDetail>(`/sources/${encodeURIComponent(params.sourceId)}`, options), /** * Distilled source content, optionally a line range: the evidence behind * a citation or an issue. */ content: ( params: {sourceId: string; startLine?: number; endLine?: number}, options?: RequestOptions, ) => this.#request<SourceContentResponse>( `/sources/${encodeURIComponent(params.sourceId)}/content`, { query: _query({ startLine: params.startLine, endLine: params.endLine, }), ...options, }, ), delete: async (params: {sourceId: string}, options?: RequestOptions) => { await this.#request<void>(`/sources/${encodeURIComponent(params.sourceId)}`, { method: 'DELETE', ...options, }) }, } } /** * Observable counterpart of {@link ContextClient}. Collection-level * methods and the GROQ-backed reads; knowledge-base scoped write * operations are promise-based, so use the promise client * (`client.context`) for those. * * @beta */ export class ObservableContextClient { #client: ObservableSanityClient #httpRequest: HttpRequest constructor(client: ObservableSanityClient, httpRequest: HttpRequest) { this.#client = client this.#httpRequest = httpRequest } /** The knowledge base collection: management addressed per call. */ knowledgeBases = { /** Create a knowledge base. Requires the org-level knowledge-base create grant. */ create: ( params: CreateKnowledgeBaseParams, options?: RequestOptions, ): Observable<KnowledgeBase> => _observe(options?.signal, (signal) => _request<KnowledgeBase>(this.#client, this.#httpRequest, { url: COLLECTION_URL, method: 'POST', body: params, tag: options?.tag, signal, }), ), /** List the organization's knowledge bases. */ list: (params: {organizationId: string} & ListOptions): Observable<KnowledgeBasesResponse> => _observe(params.signal, (signal) => _request<KnowledgeBasesResponse>(this.#client, this.#httpRequest, { url: COLLECTION_URL, query: _query({ organizationId: params.organizationId, cursor: params.cursor, limit: params.limit, }), tag: params.tag, signal, }), ), /** Fetch a knowledge base by its id. */ get: (knowledgeBaseId: string, options?: RequestOptions): Observable<KnowledgeBase> => _observe(options?.signal, (signal) => _request<KnowledgeBase>(this.#client, this.#httpRequest, { url: _knowledgeBaseUrl(knowledgeBaseId), tag: options?.tag, signal, }), ), /** Edit a knowledge base's configuration. */ edit: ( knowledgeBaseId: string, params: EditKnowledgeBaseParams, options?: RequestOptions, ): Observable<KnowledgeBase> => _observe(options?.signal, (signal) => _request<KnowledgeBase>(this.#client, this.#httpRequest, { url: _knowledgeBaseUrl(knowledgeBaseId), method: 'PATCH', body: params, tag: options?.tag, signal, }), ), /** Delete a knowledge base and its generated content. */ delete: (knowledgeBaseId: string, options?: RequestOptions): Observable<void> => _observe(options?.signal, (signal) => _request<void>(this.#client, this.#httpRequest, { url: _knowledgeBaseUrl(knowledgeBaseId), method: 'DELETE', tag: options?.tag, signal, }), ), } /** * GROQ over the organization's Context documents (conversation telemetry * today; the store holds every Context family and the caller's access * decides what a query returns, so filter on `_type`). * * Requires `context.organizationId` in the client configuration. */ fetch<R = unknown>( query: string, params?: QueryParams, options?: ContextRequestOptions, ): Observable<R> { return _fetchStore<R>(this.#client, this.#httpRequest, query, params, options) } /** * Listen for changes to the organization's Context documents. Mirrors * `client.listen(query, params, options)` and emits mutation events by * default. */ listen<Opts extends ContextListenOptions | undefined = undefined>( query: string, params?: QueryParams, options?: Opts, ): Observable<ListenEventFromOptions<SanityDocument, Opts>> { return _listenStore(this.#client, query, params, options) } /** * Conversation telemetry. `threadId` identifies the conversation within * the organization — reuse means the same conversation. Beyond the canned * `get`, reads go through {@link fetch} and {@link listen} with GROQ * (`_type == "sanity.context.conversation"`). * * Requires `context.organizationId` in the client configuration. */ conversations = { /** * Record a conversation. Messages replace the stored transcript * wholesale; `metadata` and model fields only overwrite when present. * Last write per thread wins — retries are safe. */ save: ( params: {threadId: string} & SaveConversationParams, options?: RequestOptions, ): Observable<Conversation> => { const {threadId, ...body} = params return _observe(options?.signal, (signal) => _request<Conversation>(this.#client, this.#httpRequest, { url: _conversationUrl(this.#client, threadId), method: 'PUT', body, tag: options?.tag, signal, }), ) }, /** * Record the classification your own model produced for one thread: * exactly one of `coreMetrics` (a verdict) or `classificationError` * (why classification failed). */ classify: ( params: {threadId: string} & ClassifyConversationParams, options?: RequestOptions, ): Observable<Conversation> => { const {threadId, ...body} = params return _observe(options?.signal, (signal) => _request<Conversation>(this.#client, this.#httpRequest, { url: _conversationUrl(this.#client, threadId), method: 'PATCH', body, tag: options?.tag, signal, }), ) }, /** * One recorded conversation by its thread id, or `null` when the thread * was never recorded. Runs: * * `*[_type == "sanity.context.conversation" && organizationId == $org && threadId == $threadId][0]` * * For anything more, use {@link fetch}. */ get: ( params: {threadId: string}, options?: ContextRequestOptions, ): Observable<ConversationDoc | null> => _observe(options?.signal, (signal) => _readConversation(this.#client, this.#httpRequest, params.threadId, { ...options, signal, }), ), } /** * Entries: the built outline, one entry per node. Reads are canned GROQ * queries against the organization's document store; for anything more, * use {@link fetch}. Requires `context.organizationId` alongside the * knowledge-base `resource` in the client configuration. */ entries = { /** * Every entry, path-ordered, as a metadata view (`_id`, `path`, * `title`, `tldr`, `status`) with bodies excluded. Drains keyset pages * internally and emits the complete set. Runs: * * `*[_type == "sanity.context.entry" && knowledgeBaseId == $kb && path > $after] | order(path asc) [0...200] {_id, path, title, tldr, status}` * * For bodies, use `entries.get` or {@link fetch}. */ list: (options?: ContextRequestOptions): Observable<Entry[]> => _observe(options?.signal, (signal) => _listEntries(this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), { ...options, signal, }), ), /** * One entry with its full body and citations, by outline path (e.g. * `billing/refunds`), or `null` when no entry sits at that path. Runs: * * `*[_type == "sanity.context.entry" && knowledgeBaseId == $kb && path == $path][0]` * * For anything more, use {@link fetch}. */ get: (params: {path: string}, options?: ContextRequestOptions): Observable<EntryDoc | null> => _observe(options?.signal, (signal) => _readEntry( this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), params.path, {...options, signal}, ), ), /** * Rebuild one entry from its already-placed sources, by outline path. * Poll the returned job with the promise client's `jobs`; * `affectedEntries` lists every entry the rebuild touches. */ rebuild: (params: {path: string}, options?: RequestOptions): Observable<RebuildEntryResponse> => _observe(options?.signal, (signal) => _request<RebuildEntryResponse>(this.#client, this.#httpRequest, { url: _knowledgeBaseUrl( _resolveKnowledgeBaseId(this.#client), `/entries/${encodeURIComponent(params.path)}/rebuild`, ), method: 'POST', tag: options?.tag, signal, }), ), } /** * Issues: findings from builds awaiting triage. Reads are canned GROQ * queries against the organization's document store; for anything more, * use {@link fetch}. Requires `context.organizationId` alongside the * knowledge-base `resource` in the client configuration. */ issues = { /** * Every issue on the knowledge base, oldest first, optionally narrowed * to one status. Drains keyset pages internally and emits the complete * set. Runs: * * `*[_type == "sanity.context.issue" && knowledgeBaseId == $kb && status == $status] | order(_createdAt asc, _id asc)` * * (the status clause only when given). For anything more, use {@link fetch}. */ list: ( params?: {status?: 'open' | 'accepted' | 'rejected'}, options?: ContextRequestOptions, ): Observable<IssueDoc[]> => _observe(options?.signal, (signal) => _listIssues( this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), params?.status, {...options, signal}, ), ), /** * One issue by its document id, or `null` when it does not exist. Runs: * * `*[_type == "sanity.context.issue" && knowledgeBaseId == $kb && _id == $id][0]` * * For anything more, use {@link fetch}. */ get: ( params: {issueId: string}, options?: ContextRequestOptions, ): Observable<IssueDoc | null> => _observe(options?.signal, (signal) => _readIssue( this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), params.issueId, {...options, signal}, ), ), } /** * Instructions: standing decisions that steer every build. The canned * GROQ read; writes are promise-based on `client.context`. */ instructions = { /** * Every current-schema instruction on the knowledge base, oldest first. * Drains keyset pages internally and emits the complete set. Runs: * * `*[_type == "sanity.context.instruction" && knowledgeBaseId == $kb && schemaVersion == 1] | order(_createdAt asc, _id asc)` * * For anything more, use {@link fetch}. Requires `context.organizationId` * alongside the knowledge-base `resource` in the client configuration. */ list: (options?: ContextRequestOptions): Observable<InstructionDoc[]> => _observe(options?.signal, (signal) => _listInstructions(this.#client, this.#httpRequest, _resolveKnowledgeBaseId(this.#client), { ...options, signal, }), ), } /** * MCP endpoint configurations, org-owned documents read with canned GROQ * queries. Requires `context.organizationId` in the client configuration. */ mcpEndpoints = { /** * The organization's MCP endpoint configurations, oldest first. Runs: * * `*[_type == "sanity.context.mcp" && organizationId == $org] | order(_createdAt asc, _id asc) [0...500]` * * For anything more, use {@link fetch}. */ list: (options?: ContextRequestOptions): Observable<McpDoc[]> => _observe(options?.signal, (signal) => _listMcpEndpoints(this.#client, this.#httpRequest, { ...options, signal, }), ), /** * One MCP endpoint configuration by its URL name, or `null` when none * carries that name. Runs: * * `*[_type == "sanity.context.mcp" && organizationId == $org && name == $name][0]` * * For anything more, use {@link fetch}. */ get: (params: {name: string}, options?: ContextRequestOptions): Observable<McpDoc | null> => _observe(options?.signal, (signal) => _readMcpEndpoint(this.#client, this.#httpRequest, params.name, { ...options, signal, }), ), } }