@andesine/sdk 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/README.md ADDED
@@ -0,0 +1,728 @@
1
+ # @andesine/sdk
2
+
3
+ MIT-licensed TypeScript client for the public Andesine HTTP API. ESM only. Requires
4
+ Node.js 22 or later, or a browser/worker runtime with Fetch, Blob, FormData, and
5
+ AbortSignal.any. Keep private API keys in server code.
6
+
7
+ ## Use
8
+
9
+ ```sh
10
+ npm install @andesine/sdk
11
+ ```
12
+
13
+ ```ts
14
+ import { createClient } from "@andesine/sdk";
15
+
16
+ const client = createClient({
17
+ baseURL: "https://api.andesine.app",
18
+ apiKey: process.env.ANDESINE_API_KEY
19
+ });
20
+
21
+ const entry = await client.entries.get({ id: "ent_123" });
22
+ const published = await client.content.get({
23
+ entryID: entry.id,
24
+ channel: "published"
25
+ });
26
+ ```
27
+
28
+ `baseURL` is required. A trailing slash is optional; path prefixes are preserved.
29
+ Each client has its own configuration. You can supply `fetch` and default
30
+ `headers` for proxies, instrumentation, or other authentication arrangements.
31
+
32
+ Resource methods are generated from the checked-in OpenAPI document. They cover
33
+ assets, entries, collections, content delivery, roles, search, schemas, schema
34
+ migrations, schema versions, memberships, publishing, entry versions, and instance information. Internal
35
+ session-only operations are excluded. Published asset delivery does not require
36
+ an API key.
37
+
38
+ Methods accept one object containing path, query, and body fields. Operations
39
+ without required fields can be called without arguments. Date/time values remain
40
+ JSON strings. File inputs and binary outputs use `Blob`.
41
+
42
+ ```ts
43
+ await client.assets.upload({ assetID: "ast_123", file: new Blob([bytes]) });
44
+ const image = await client.assets.get({ assetID: "ast_123", entryID: entry.id });
45
+ ```
46
+
47
+ The client does not add a content-save endpoint. Use converters to produce JSON;
48
+ storage remains a separate responsibility.
49
+
50
+ ## Errors and responses
51
+
52
+ Methods return data directly. HTTP failures throw `AndesineAPIError`, with
53
+ `status`, `code`, `message`, `data`, `defined`, `hints`, and the Fetch `response`. Network failures and
54
+ cancellation retain their native errors.
55
+
56
+ ```ts
57
+ import { AndesineAPIError } from "@andesine/sdk";
58
+
59
+ try {
60
+ await client.entries.update({ id: "ent_123", name: "Installation" });
61
+ } catch (error) {
62
+ if (error instanceof AndesineAPIError) {
63
+ console.error(error.status, error.code, error.message, error.hints);
64
+
65
+ if (error.is("SCHEMA_MIGRATION_IN_PROGRESS")) {
66
+ // data is typed for this code.
67
+ console.log(error.data.migrationID);
68
+ }
69
+
70
+ if (error.is("TOO_MANY_REQUESTS")) {
71
+ console.log(error.data?.retryAfterSeconds);
72
+ }
73
+ }
74
+ }
75
+
76
+ const result = await client.content.get(
77
+ { entryID: "ent_123" },
78
+ { response: "full", headers: { "If-None-Match": cachedETag } }
79
+ );
80
+
81
+ if (result.notModified) {
82
+ // Reuse the cached content.
83
+ } else {
84
+ console.log(result.data, result.headers.get("ETag"));
85
+ }
86
+ ```
87
+
88
+ Use full-response mode for conditional requests. A 304 response has
89
+ `notModified: true` and no data. A 304 in data mode causes a clear error because
90
+ the client does not store a response cache.
91
+
92
+ ## Cancellation, timeouts, and retries
93
+
94
+ The default timeout is 30 seconds. Set `timeout: 0` to disable it. Timeouts include
95
+ request execution, response reading, and retry delays. Supply `signal` per call
96
+ for cancellation.
97
+
98
+ Retries are **disabled by default**. Set `retries: 2` on the client or a call to
99
+ allow up to two retries of GET requests. Reads retry network `TypeError` failures
100
+ and HTTP 429, 500, 502, 503, and 504. They respect `Retry-After`, otherwise use
101
+ increasing delays with jitter. Writes are never retried automatically.
102
+
103
+ ```ts
104
+ await client.entries.get(
105
+ { id: "ent_123" },
106
+ { signal: controller.signal, timeout: 10_000, retries: 2 }
107
+ );
108
+ ```
109
+
110
+ ## Snapshots and pagination
111
+
112
+ Pin related content reads to the snapshot returned by the first read:
113
+
114
+ ```ts
115
+ const tree = await client.content.getTree({ collectionID: "col_123" });
116
+ const content = client.atSnapshot(tree.snapshotID);
117
+ const entry = await content.get({ entryID: "ent_123" });
118
+ ```
119
+
120
+ `atSnapshot()` returns content resource methods. It rejects a conflicting channel
121
+ or snapshot selector. It does not pin unrelated management operations. Snapshot
122
+ image URLs can expire; copying images for archival export is the caller's task.
123
+
124
+ ```ts
125
+ import { paginate } from "@andesine/sdk";
126
+
127
+ for await (const entry of paginate((cursor) => client.entries.list({ cursor }))) {
128
+ console.log(entry.name);
129
+ }
130
+ ```
131
+
132
+ Cursor lists default to 50 items and accept at most 100. `memberships.list()` and
133
+ `memberships.listInvites()` return `{ data, pagination }`. Migration status no longer
134
+ includes `contentLossEntries`; use `schemaMigrations.listContentLossEntries()` for
135
+ that paginated list. Mutable management lists are not snapshots: concurrent edits
136
+ can change their contents between requests.
137
+
138
+ Use `paginatePages()` to keep page metadata and save a cursor after processing a
139
+ whole page. Both iterators accept `{ cursor }` to resume. Save the same filters with
140
+ your cursor. A missing or repeated continuation cursor causes an error.
141
+
142
+ Published listings also require the snapshot ID on later pages. The first page
143
+ selects a channel; later pages select that exact snapshot:
144
+
145
+ ```ts
146
+ import { paginatePages } from "@andesine/sdk";
147
+
148
+ let snapshotID: string | undefined; // Restore this together with a saved cursor.
149
+ const savedCursor: string | undefined = undefined;
150
+
151
+ for await (const page of paginatePages(
152
+ (cursor) =>
153
+ client.content.listEntries({
154
+ ...(snapshotID ? { snapshotID } : { channel: "published" }),
155
+ cursor,
156
+ limit: 100
157
+ }),
158
+ { cursor: savedCursor }
159
+ )) {
160
+ snapshotID = page.snapshotID;
161
+ // Process page.data, then save snapshotID and page.pagination.nextCursor.
162
+ // A null nextCursor means the listing is complete.
163
+ }
164
+ ```
165
+
166
+ `content.listCollections()` uses the same format. Both return flat records ordered
167
+ by ID, with parent/collection IDs for links. Entry summaries include a version ID and
168
+ hash. Use `includeContent: true` for full entry records, or fetch one entry with
169
+ `client.atSnapshot(snapshotID).get()`. Snapshot expiry
170
+ still applies. If a saved snapshot is unavailable, start a new listing from its
171
+ channel; do not combine the old cursor with a new snapshot.
172
+
173
+ ### Filtered full-content lists
174
+
175
+ ```ts
176
+ const page = await client.content.listEntries({
177
+ collectionPath: "/Tutorials",
178
+ descendants: true,
179
+ includeContent: true,
180
+ filters: [
181
+ { kind: "text", key: "audience", values: ["beginner"] },
182
+ { kind: "boolean", key: "enabled", value: true }
183
+ ],
184
+ limit: 50
185
+ });
186
+
187
+ for (const entry of page.data) {
188
+ console.log(entry.path, entry.content, entry.properties, entry.schema);
189
+ }
190
+
191
+ if (page.pagination.nextCursor) {
192
+ const next = await client.atSnapshot(page.snapshotID).listEntries({
193
+ collectionPath: "/Tutorials",
194
+ descendants: true,
195
+ includeContent: true,
196
+ filters: [
197
+ { kind: "text", key: "audience", values: ["beginner"] },
198
+ { kind: "boolean", key: "enabled", value: true }
199
+ ],
200
+ cursor: page.pagination.nextCursor,
201
+ limit: 50
202
+ });
203
+ }
204
+ ```
205
+
206
+ `includeContent` defaults to false. A literal true gives full items; false or an
207
+ omitted option gives summaries. A runtime boolean gives their union. This also
208
+ works with full-response mode, `atSnapshot()`, and `paginatePages()`. Full items
209
+ add `content`, `properties`, `fragments`, `assets`, `schema`, and full version
210
+ metadata. Channel and snapshot metadata stay in the page envelope. The generated
211
+ `PublishedEntryContent` and `PublishedEntrySummary` types are public.
212
+
213
+ With a collection selector, the list returns direct entries by default.
214
+ `descendants: true` includes nested collections and requires a collection selector.
215
+ Use `collectionPath: "/"` with descendants for the complete snapshot tree. Without
216
+ a scope, the list includes all snapshot entries.
217
+
218
+ Filters use the same contract as search: text `any`/`all`/`none`, number and date
219
+ comparisons, and boolean equality. All filters must match. Text comparison ignores
220
+ case; missing or incompatible properties do not match negative operators. Filters
221
+ use the assigned version's property records, including fields between content
222
+ blocks. They do not use the current draft or the search index. The SDK encodes
223
+ nested filters automatically.
224
+
225
+ Keep the same scope and filters across pages. Limits remain 1–100 entries and up
226
+ to 20 filters, with the existing per-filter bounds. Assets are URLs and metadata.
227
+ The server validates each returned full document against its recorded schema. An
228
+ invalid document fails the page with `CONTENT_SCHEMA_INVALID`; it is not skipped.
229
+ Summary reads do not load or validate full documents.
230
+
231
+ ### Structured content
232
+
233
+ Use `toStructuredContent()` to remove property and fragment wrappers:
234
+
235
+ ```ts
236
+ import { toStructuredContent } from "@andesine/sdk";
237
+
238
+ const page = await client.content.get({ path: "/Tutorials/Welcome" });
239
+ const tutorial = toStructuredContent(page);
240
+
241
+ console.log(tutorial.properties); // Each property's value, keyed by derived key.
242
+ console.log(tutorial.fragments); // Each fragment's content, keyed by derived key.
243
+ console.log(tutorial.schema, tutorial.path); // Other response fields are preserved.
244
+ ```
245
+
246
+ The helper also accepts current full entries and full published list items:
247
+ `page.data.map(toStructuredContent)`. Property and fragment maps stay separate.
248
+ Values such as `null`, false, zero, empty strings, and empty arrays are preserved.
249
+ The helper is synchronous. It creates a new object and maps, does not change its
250
+ input, and retains references to values and other fields. It does not validate,
251
+ coerce, fetch, or generate collection-specific types. Types follow the supplied
252
+ input; API property values retain their generated union types. `StructuredContent<T>`
253
+ is available for explicit annotations. Collection type generation remains deferred.
254
+
255
+ ## Publishing preconditions
256
+
257
+ Pass `expectedSnapshotID` to publish, unpublish, or delete a channel only if its
258
+ publication state still matches a prior read:
259
+
260
+ ```ts
261
+ const reviewed = await client.content.listEntries({ channel: "published", limit: 1 });
262
+
263
+ await client.publishing.publishEntry({
264
+ entryID: "ent_123",
265
+ channel: "published",
266
+ expectedSnapshotID: reviewed.snapshotID
267
+ });
268
+ ```
269
+
270
+ The server checks the channel inside the mutation transaction. A mismatch returns
271
+ HTTP 409 with code `PUBLISHING_SNAPSHOT_CHANGED`, the expected and current snapshot
272
+ IDs, and a hint. Read the current state and review it before another write. Omitting
273
+ the precondition makes no comparison with your prior read. Draft edits are not
274
+ covered by this check.
275
+
276
+ `setCollection()` and `bulkSetCollections()` can affect multiple channels. Their
277
+ optional `expectedSnapshots` map checks each supplied channel before the change,
278
+ for example `{ published: "snp_123", preview: "snp_456" }`. Omitted channels are not
279
+ checked. `revertChanges()` continues to require its reviewed `snapshotID`.
280
+
281
+ Bulk inputs accept at most 100 selected items. Revert selections share that limit
282
+ across entries and collections. A selected collection can contain more descendants;
283
+ this is an input limit, not a limit on the number of affected records. Separate bulk
284
+ requests are separate transactions.
285
+
286
+ ## Instance information
287
+
288
+ ```ts
289
+ const instance = await client.instance.get();
290
+ console.log(instance.features.imageStorage, instance.features.semanticSearch);
291
+ console.log(instance.features.aiAnswers, instance.features.aiAnswerStreaming);
292
+ console.log(instance.limits.maxUploadBytes, instance.limits.assetStorageBytes);
293
+ console.log(instance.limits.maxPageSize, instance.limits.maxBulkItems);
294
+ ```
295
+
296
+ This authenticated call returns the API contract version, configured features,
297
+ and effective workspace limits, including the search result limit. Features do
298
+ not grant permissions or report live service health. Storage limits are capacity,
299
+ not remaining space. `aiAnswers` requires search, embedding, and answer-model
300
+ configuration. `aiAnswerStreaming` reports the configured streaming implementation;
301
+ the AI provider must support Chat Completions SSE. These flags do not report
302
+ remaining quota. The SDK makes no discovery request automatically.
303
+
304
+ ## Types
305
+
306
+ Named types are generated from the public OpenAPI components. Import them directly
307
+ or through the type-only `Andesine` namespace:
308
+
309
+ ```ts
310
+ import type { Entry, PropertyFilter, Andesine } from "@andesine/sdk";
311
+
312
+ const renderEntry = (entry: Entry): string => entry.name;
313
+ const renderAsset = (asset: Andesine.Asset): string => asset.filename;
314
+ const filter: PropertyFilter = {
315
+ kind: "text",
316
+ key: "category",
317
+ values: ["guides"]
318
+ };
319
+ ```
320
+
321
+ The exports cover entries and properties; assets, files, and analysis; collections;
322
+ entry and schema versions; schema fields, inheritance, and migrations; memberships,
323
+ invites, roles, and permissions; published content and trees; publishing channels;
324
+ search results and filters; and pagination.
325
+
326
+ `Entry` includes content, fragments, and properties. `EntrySummary` is the metadata
327
+ returned by entry lists and creation. `SchemaDefinition` describes a collection's
328
+ content schema; `CollectionSchema` contains its local and effective schema details.
329
+ Types describe the HTTP data: dates are strings and binary values are `Blob`.
330
+ Search filter types describe inputs, so fields with server defaults can be omitted.
331
+
332
+ Every public operation also has generated input, output, and error aliases. Their names
333
+ combine the resource and method with `Input`, `Output`, or `Error`:
334
+
335
+ ```ts
336
+ import type { EntriesCreateInput, EntriesListOutput, Andesine } from "@andesine/sdk";
337
+
338
+ const input: EntriesCreateInput = { name: "Getting started" };
339
+ type EntryPage = EntriesListOutput;
340
+ type SearchRequest = Andesine.SearchCurrentInput;
341
+ ```
342
+
343
+ Operation inputs match the SDK's single argument object, including path, query,
344
+ and body fields. Outputs match the returned data, excluding the full-response
345
+ wrapper. All these exports, including `Andesine`, are types only and add no runtime
346
+ code. The generic helpers and raw `components`, `operations`, and `paths` types
347
+ remain available:
348
+
349
+ ```ts
350
+ import type { OperationInput, OperationOutput } from "@andesine/sdk";
351
+
352
+ type CreateEntry = OperationInput<"entries.create">;
353
+ type EntryPage = OperationOutput<"entries.list">;
354
+ ```
355
+
356
+ Content types are structurally compatible with `@andesine/converters`. Neither
357
+ public package imports the private editor or backend at runtime.
358
+
359
+ ## Maintainer workflow
360
+
361
+ From the repository root:
362
+
363
+ ```sh
364
+ pnpm --filter @andesine/backend openapi:export
365
+ pnpm --filter @andesine/sdk generate
366
+ pnpm --filter @andesine/sdk build
367
+ ```
368
+
369
+ The export calls oRPC's OpenAPI generator on the same contracts implemented by
370
+ the server. It imports no request handlers or service initialization and needs no
371
+ backend environment variables, database, queue, or running server. The export
372
+ uses the default upload-size limit; self-hosted instances can set a different
373
+ limit, which their own OpenAPI document reflects. The checked-in document's
374
+ server URL is a placeholder; consumers must supply their instance URL.
375
+
376
+ Commit `openapi.json` and `src/generated/` together. Do not edit generated files.
377
+ Public schema names are defined in the backend's
378
+ `src/contracts/schemas/public.ts` registry. Adding a component there generates its
379
+ named SDK export and its `Andesine` member; no SDK alias list needs to be updated.
380
+ The generator also creates input/output/error aliases for each public operation and
381
+ rejects invalid or duplicate public type names.
382
+ Generation is a maintainer operation, never an install-time operation. `prepack`
383
+ checks types, bundles ESM and declarations with Rolldown, and checks
384
+ package metadata with publint. These 0.x packages are pre-release; review
385
+ generated changes together with API changes.
386
+
387
+ See the [release guide](https://github.com/vriteio/andesine/blob/andesine/packages/public/README.md)
388
+ for archive checks, versioning, and npm publishing. This package uses the
389
+ [MIT license](./LICENSE).
390
+
391
+ ### Generator choice
392
+
393
+ Both Hey API 0.99.0 and openapi-typescript 7.13.0 generated output from the Andesine
394
+ spec. This package uses openapi-typescript and openapi-fetch. A small generated
395
+ resource layer supplies the flat argument objects and instance methods; a shared
396
+ transport supplies errors, retries, multipart serialization, and full responses.
397
+
398
+ Hey API generated operation functions and a larger client layer, but still needed
399
+ adaptation for the selected public interface and multipart operation defaults.
400
+ The selected approach keeps these behaviors in one maintained transport and
401
+ keeps runtime dependencies small.
402
+
403
+ ## Content names
404
+
405
+ Sibling entries and collections share names. Names are trimmed, normalized to
406
+ Unicode NFC, and compared case-sensitively. Names cannot contain `/` or equal
407
+ `.` or `..`. The limit is 300 JavaScript string units. The collection name `~`
408
+ is reserved for the internal workspace root. Root entries and root collections
409
+ share the same level.
410
+
411
+ Creation chooses an available name, adding suffixes such as ` (2)` when needed.
412
+ Use the returned name rather than assuming that your requested name was saved.
413
+ Rename, move, and restore operations reject conflicts. Deleted items release
414
+ names; a schema move temporarily reserves its source name for rollback.
415
+
416
+ `CONTENT_NAME_CONFLICT` (409) includes `name`, `parentID`, and optional `hints`.
417
+ Use `error.is("CONTENT_NAME_CONFLICT")` to access typed data. Renaming never
418
+ chooses a different name for you.
419
+
420
+ ## Content paths
421
+
422
+ Read an entry by ID or by path. Use exactly one selector:
423
+
424
+ ```ts
425
+ const current = await client.entries.get({ path: "/Docs/Getting started" });
426
+ const tree = await client.content.getTree({ collectionPath: "/Docs" });
427
+ const content = client.atSnapshot(tree.snapshotID);
428
+ const page = await content.get({ path: "/Docs/Getting started" });
429
+ const definition = await content.getSchema({ path: page.path });
430
+ const activeSchema = await client.schemas.get({ collectionPath: "/Docs" });
431
+ ```
432
+
433
+ Paths use names from the tree, with `/` between levels. Names are case-sensitive;
434
+ each segment is trimmed and NFC-normalized. Pass decoded names to the SDK. It
435
+ encodes query parameters once, so a name containing the literal text `%2F` keeps
436
+ that text. Empty segments, trailing slashes, `.` and `..` are invalid.
437
+
438
+ An absolute path starts with `/`. An anchored path starts with a collection ID,
439
+ such as `coll_67bqzSOb9kct2DaVkTwT53/Getting started`. The anchor must exist in the
440
+ selected current tree or snapshot. A name that looks like an ID is still a name
441
+ when it occurs in an absolute path. A bare collection ID is also a valid
442
+ `collectionPath`. `/` selects the root. Neither selects an entry on its own.
443
+
444
+ Current entry and collection responses include their canonical absolute `path`.
445
+ Published content, trees, and list results use snapshot collection names and the
446
+ titles of assigned versions. Draft renames, moves, and deletions do not change
447
+ those snapshot paths. Full published content also includes `id` and `collectionID`.
448
+ Saved versions remain ID-based because a version alone has no snapshot location.
449
+
450
+ `content.getTree({ collectionPath: "/" })` returns a virtual root collection with
451
+ `id: null`, `name: ""`, and `path: "/"`. It contains top-level entries and collections.
452
+
453
+ `entries.list`, `collections.list`, `content.listEntries`, and
454
+ `content.listCollections` accept either `collectionID` or `collectionPath` to
455
+ select direct children. Keep the same scope while paging. With no scope,
456
+ `collections.list` selects root children; the other lists select all accessible
457
+ items. `collections.list` now uses these fields instead of `ancestorID`.
458
+ Search accepts the same collection selectors and includes descendants. Its `/`
459
+ scope covers the whole accessible tree. Search results include a canonical entry
460
+ `path` and an optional heading `anchor`.
461
+
462
+ HTTP reads use query selectors on `GET /entries/get`, `GET /content/entries/get`,
463
+ `GET /content/tree`, and `GET /schemas/collection`. The published schema route
464
+ remains `GET /content/entries/schema`. These replace the former ID-in-path GET
465
+ routes; mutation routes are unchanged. SDK method names are unchanged.
466
+
467
+ Publication checks names across the complete next snapshot, including retained
468
+ versions. `PUBLISHING_NAME_CONFLICT` (409) includes `name`, `parentID`, and `hints`.
469
+ Publish related replacements together or unpublish the conflict first. A draft
470
+ rename alone does not remove a name from an existing publication. A rejected
471
+ publication leaves the channel and its snapshot unchanged.
472
+
473
+ ## Search and source links
474
+
475
+ Published search results include required `channel`, `snapshotID`, `versionID`,
476
+ and canonical entry `path` fields. `entryID` remains the stable identity.
477
+ An optional `anchor` identifies a heading in that version; a result before any
478
+ heading has no anchor. Current results have their indexed working path. Search
479
+ index updates are asynchronous, so current paths can briefly lag draft changes.
480
+
481
+ ```ts
482
+ const matches = await client.search.published({
483
+ channel: "published",
484
+ query: "installation"
485
+ });
486
+
487
+ for (const result of matches.results) {
488
+ const page = await client.atSnapshot(result.snapshotID).get({
489
+ entryID: result.entryID
490
+ });
491
+ const pathname = result.path.split("/").map(encodeURIComponent).join("/");
492
+ const link = pathname + (result.anchor ? `#${encodeURIComponent(result.anchor)}` : "");
493
+ // Map pathname to your docs site's route and render page with matching heading IDs.
494
+ }
495
+ ```
496
+
497
+ Paths and anchors are decoded data, not application URLs. Encode each path segment
498
+ when building a URL. Reading by `entryID` at the source snapshot preserves the
499
+ content version even if the channel advances. A retained snapshot can expire;
500
+ handle an unavailable snapshot with a new search. Search still uses the current
501
+ channel and does not support historical snapshot queries.
502
+
503
+ The converters' `getHeadingAnchors()` helper derives matching targets. HTML
504
+ conversion applies those IDs; other renderers must preserve AST IDs or apply the
505
+ helper themselves. Duplicate headings are numbered across all fragments. Existing
506
+ `headingPath` is retained for editor navigation. Answer sources use the same path,
507
+ snapshot, and anchor metadata, plus a numbered citation `id` and `relevance`.
508
+
509
+ ## AI answers
510
+
511
+ Call these methods from your server with an API key. For published documentation,
512
+ grant `ai-answers` and `read:publishing`. For current workspace content, grant
513
+ `ai-answers`, `read:entries`, and `read:collections`. Write permissions also supply
514
+ the corresponding read access, but never imply `ai-answers`. The key editor has a
515
+ separate AI answers option.
516
+
517
+ ```ts
518
+ import type { AnswerHistoryMessage, PublishedAnswerSource } from "@andesine/sdk";
519
+
520
+ const history: AnswerHistoryMessage[] = [];
521
+ const result = await client.search.askPublished({
522
+ channel: "published",
523
+ collectionPath: "/Docs",
524
+ question: "How do I install Andesine?",
525
+ history
526
+ });
527
+ const sources: PublishedAnswerSource[] = result.sources;
528
+
529
+ // [1] in result.answer refers to the source with id === 1.
530
+ // Use each source's path/anchor for links and snapshotID/entryID for content reads.
531
+ history.push(
532
+ { role: "user", content: "How do I install Andesine?" },
533
+ { role: "assistant", content: result.answer }
534
+ );
535
+ ```
536
+
537
+ `client.search.askCurrent({ question, collectionPath, history })` reads current
538
+ content with the caller's collection access rules. Both methods accept optional
539
+ property `filters` and either `collectionID` or `collectionPath`. Published scopes
540
+ resolve within the channel's current snapshot. Omit the collection selector to
541
+ search all content available to the caller.
542
+
543
+ Responses contain the complete `answer` and `sources`. The generated `Answer`,
544
+ `PublishedAnswer`, `AnswerSource`, `PublishedAnswerSource`, and
545
+ `AnswerHistoryMessage` types are also available through `Andesine`.
546
+ Both complete and streamed answers use the same inputs and source-selection rules.
547
+
548
+ The limits are 1,000 characters per question, 10 history messages of up to 4,000
549
+ characters each, and 20 property filters. History is supplied per request; the SDK
550
+ does not store it. Trim history before sending a later question. The existing Ask
551
+ AI rate limit is 10 requests per 60 seconds per credential. API usage metering
552
+ is unchanged: a successful complete response counts once when billing is enabled.
553
+ A rate-limit error includes `Retry-After` and `data.retryAfterSeconds`. Provider
554
+ failures return `SERVICE_UNAVAILABLE` with hints. Answer POST requests are never
555
+ automatically retried, even when read retries are enabled.
556
+
557
+ ## Recorded content schemas
558
+
559
+ Full entry, version, and published-content responses include `schema`, containing
560
+ `revisionID` and `hash`, or `null` for content without a recorded schema. Reads and
561
+ publication validate the document against that recorded revision, including
562
+ inherited fields. They do not repair content or use the latest schema instead.
563
+
564
+ ```ts
565
+ const page = await client.content.get({ entryID: "ent_123" });
566
+ const definition = await client.atSnapshot(page.snapshotID).getSchema({
567
+ entryID: page.id
568
+ });
569
+
570
+ // Use a hash saved by your application when it requires a specific definition.
571
+ const checked = await client.content.get({
572
+ entryID: page.id,
573
+ snapshotID: page.snapshotID,
574
+ expectedSchemaHash: "YOUR_SAVED_64_CHARACTER_SHA256_HASH"
575
+ });
576
+ ```
577
+
578
+ `content.getSchema()` uses published-content access. It returns the selected
579
+ entry's recorded definition or `null`. `schemas.getRevision({ revisionID })`
580
+ requires collection-read access and returns an exact revision, including inactive
581
+ revisions. Both return `{ revisionID, hash, definition }`. The definition combines
582
+ inherited fields without internal source metadata and matches the recorded hash.
583
+
584
+ `expectedSchemaHash` is also available on `entries.get()`, `versions.get()`, and
585
+ `publishing.getEntryVersion()`. A mismatch, including content without a schema,
586
+ returns `CONTENT_SCHEMA_MISMATCH` (409) with the expected hash and actual metadata.
587
+ Invalid stored content returns `CONTENT_SCHEMA_INVALID` (500) on delivery or (409)
588
+ on publication, with issue paths and available entry, version, and revision IDs.
589
+ `SCHEMA_FIELD_KEY_CONFLICT` (409) identifies conflicting derived field keys.
590
+ Use `error.is(code)` to read the generated error data types. Schema definitions
591
+ remain readable when their associated document is invalid.
592
+
593
+ ## Operation documentation and error hints
594
+
595
+ All public methods include generated summaries, descriptions, API key permissions,
596
+ and input examples in editor tooltips. The same examples appear in OpenAPI parameters
597
+ and request bodies. IDs and file contents in examples are placeholders; replace them
598
+ with values from your instance. Role permissions differ from API key permissions.
599
+
600
+ Error declarations come from the backend contracts. `error.is(code)` narrows `data`
601
+ only when the server marks the error as defined by its contract. An undeclared error,
602
+ proxy response, or network failure does not become a typed domain error. Handle the
603
+ remaining error cases as well. `data` stays `unknown` until narrowed.
604
+
605
+ `APIErrorCode`, `APIErrorBody`, `APIErrorData<Code>`, and
606
+ `OperationError<"entries.create">` are available for application types. Operation
607
+ aliases such as `EntriesCreateError` are also available as
608
+ `Andesine.EntriesCreateError`. These types describe declared errors, not every
609
+ possible failure. TypeScript does not enforce the types of thrown errors.
610
+
611
+ `data.hints` is optional advice. `error.hints` returns an empty array when no hints
612
+ are present. Use stable codes and structured data for program logic; never parse
613
+ hint text. Hints do not trigger retries. Automatic retries remain disabled by default.
614
+
615
+ Common errors can include validation `issues`, `missingPermissions`, `requiredPlan`,
616
+ or `retryAfterSeconds`. Domain errors include `SCHEMA_MIGRATION_IN_PROGRESS`,
617
+ `PUBLISHING_SNAPSHOT_CHANGED`, `ROLE_NAME_DUPLICATE`, `ROLE_NAME_INVALID`,
618
+ `MEMBERSHIP_ALREADY_EXISTS`, and `INVITE_ALREADY_PENDING`. Migration and snapshot
619
+ conflicts now use their specific codes instead of the generic `CONFLICT` code.
620
+
621
+ Asset operation errors can also include the current `assetStatus`, expected and
622
+ actual file sizes and checksums, or storage `limitBytes`, `usedBytes`, and
623
+ `requiredBytes`. Storage reservations include processed image variants.
624
+ These fields are declared on the applicable operation error types, such as
625
+ `AssetsUploadError` and `AssetsRegisterError`; not every error with the same code
626
+ contains asset data. With `AndesineAPIError`, check for the field before using it.
627
+ Known rate-limit delays appear in both `data.retryAfterSeconds` and `Retry-After`.
628
+ Concurrency limits have no fixed delay and only supply a hint.
629
+
630
+ ## Streaming AI answers
631
+
632
+ `search.askCurrentStream()` and `search.askPublishedStream()` return a promise of
633
+ an async iterator. They require the same permissions and inputs as their complete
634
+ answer methods. Published source types require `snapshotID`, `versionID`, and
635
+ `channel`. `AnswerEvent` and `PublishedAnswerEvent` are available directly and
636
+ through `Andesine`.
637
+
638
+ ```ts
639
+ const controller = new AbortController();
640
+ const events = await client.search.askPublishedStream(
641
+ { channel: "published", collectionPath: "/Docs", question: "How do I install Andesine?" },
642
+ { signal: controller.signal, timeout: 120_000 }
643
+ );
644
+
645
+ for await (const event of events) {
646
+ if (event.type === "sources") console.log(event.sources);
647
+ if (event.type === "textDelta") process.stdout.write(event.text);
648
+ if (event.type === "completed") console.log(event.answer);
649
+ }
650
+ ```
651
+
652
+ The order is one `sources` event, non-empty `textDelta` events, then one
653
+ `completed` event. A no-source answer may complete without deltas. The completed
654
+ answer contains the final text and sources. Deltas contain plain text, including
655
+ any Markdown syntax; choose how to render it in your application.
656
+
657
+ Use `controller.abort()` to cancel, or `break` from the loop to stop early. If you
658
+ open a stream but do not iterate it, call `await events.return?.()` to close it.
659
+ The request timeout covers both opening and consuming the stream; `timeout: 0`
660
+ disables it. The SDK never reconnects or retries generation, even if read retries
661
+ are enabled. `response: "full"` returns the iterator in `data`, with the actual
662
+ response status and headers.
663
+
664
+ Errors before SSE headers are normal `AndesineAPIError` failures. A declared
665
+ error after headers throws `AndesineStreamError`, which extends `AndesineAPIError`
666
+ and supports `.is()`, `code`, `data`, and `hints`. Its `status` is the error status
667
+ from the frame; `response.status` remains the actual HTTP status, usually 200.
668
+ Malformed streams throw `AndesineStreamProtocolError` with `INVALID_STREAM`.
669
+ Missing completion or an interrupted body uses `INCOMPLETE_STREAM`. These are
670
+ local protocol errors, not API errors with an invented HTTP status. Abort and
671
+ timeout reasons are preserved.
672
+
673
+ A stream counts as one API call when generation starts, including generation that
674
+ later fails or is cancelled. Source preparation alone is not charged. The existing
675
+ Ask AI rate limit and quota checks run before opening the stream. Usage headers
676
+ show the allowance when the stream opens; chunks do not update headers or create
677
+ additional usage records.
678
+
679
+ ### Forward a stream from your server
680
+
681
+ The optional `@andesine/sdk/streaming` entry has no framework dependency. It uses
682
+ standard Fetch types, preserves backpressure and cancellation, and forwards no
683
+ upstream headers or credentials.
684
+
685
+ ```ts
686
+ import { toAnswerResponse } from "@andesine/sdk/streaming";
687
+
688
+ async function answerRequest(request: Request): Promise<Response> {
689
+ const { question } = await request.json();
690
+ const events = await client.search.askPublishedStream(
691
+ { channel: "published", collectionPath: "/Docs", question },
692
+ { signal: request.signal, timeout: 120_000 }
693
+ );
694
+
695
+ return toAnswerResponse(events);
696
+ }
697
+ ```
698
+
699
+ Keep the configured `client` and API key on your server. Handle errors before
700
+ `toAnswerResponse()` with your application's normal HTTP error handler. Once
701
+ streaming starts, the helper preserves typed API error frames and uses a generic
702
+ error frame for other failures. Its response disables caching and requests that
703
+ proxies do not buffer the stream. Your hosting platform must support streamed
704
+ responses.
705
+
706
+ A browser can read the forwarded response without an Andesine API key:
707
+
708
+ ```ts
709
+ import { readAnswerStream } from "@andesine/sdk/streaming";
710
+
711
+ const controller = new AbortController();
712
+ const response = await fetch("/api/ask", {
713
+ method: "POST",
714
+ headers: { "Content-Type": "application/json" },
715
+ body: JSON.stringify({ question: "How do I install Andesine?" }),
716
+ signal: controller.signal
717
+ });
718
+
719
+ for await (const event of await readAnswerStream(response, { signal: controller.signal })) {
720
+ console.log(event);
721
+ }
722
+ ```
723
+
724
+ Both helpers use oRPC's SSE framing: `event: message` carries an answer event as
725
+ JSON; `event: error` carries the error envelope. Completion is an application
726
+ `completed` event. Comments are ignored, and a transport `done` event cannot
727
+ replace application completion. Event IDs and retry fields never trigger
728
+ reconnection.