@onlyworlds/sdk 4.11.0 → 4.13.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/AGENTS.md CHANGED
@@ -40,6 +40,12 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
40
40
  - Images: `uploadImage(bytes)` returns `{ url, ... }`; set `url` as `image_url`. The bytes go
41
41
  to the edge with a single-use ticket from keel, never with the API key. `removeImage(urlOrKey)`
42
42
  takes one back (its uploader or the world's owner); it leaves `image_url` on elements as is.
43
+ - Ask (since 4.13.0, account token): `buildAskContext(hits, index)` then
44
+ `account.ask({ worldId, question, context })`; answers only from the context. 50 a day per
45
+ account (`err.isQuotaExceeded`); `isAskUnavailable`, `isAskFailed` count nothing.
46
+ - Invite links (since 4.12.0): `previewJoin(code)` (no credential sent) shows who invites, to
47
+ which world, in what role; `acceptJoin(code)` with an `ow_a_` account token joins. Bad code:
48
+ `err.isNotFound`; `err.isAlreadyMember`, `err.isPinRequired` for the two 409s.
43
49
  - Lookup without a model (since 4.11.0): `buildWorldIndex(elements)` then
44
50
  `lookupWorld(index, question)` ranks the elements a question is about (names, then kind, one
45
51
  link step, then text); `renderElement(el, index)` is a compact card with links by name.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.13.0] — 2026-10-12
9
+
10
+ ### Added
11
+ - **Ask your world (keel #159).** `client.ask({ worldId, question, context })` sends a
12
+ question to keel's AI proxy with the account's `ow_a_` token (a world key is a `TypeError`
13
+ before sending) and returns `OwAskAnswer` (`answer`, `model`, `stop_reason`, `usage`,
14
+ `remaining`). The answer comes from `context` only, or is "The world doesn't say.".
15
+ `buildAskContext(items, index)` turns the elements the user sees (lookup hits or plain
16
+ elements, in the caller's order, never re-looked-up) into that `context` within keel's
17
+ limits (`ASK_LIMITS`: 8 items, 2,000 characters each, 12,000 in all, counted in characters
18
+ as keel counts them) and says what it did: `kept`, `dropped` (past the 8th, or from the first
19
+ one the total cannot hold, so what is sent is always the top of the list) and `truncated`.
20
+ New getters: `isQuotaExceeded` (429; `retryAfter` runs to midnight UTC), `isAskUnavailable`
21
+ (503), `isAskFailed` (502). keel's document gives no request schema for `/account/ask`, so
22
+ the body (`world_id`, `question`, `context`) is read from keel's code (`accounts/ask.py`).
23
+ Not yet run live: no crew session holds an account token.
24
+
25
+ ## [4.12.0] — 2026-10-12
26
+
27
+ ### Added
28
+ - **Joining a world by link (keel #212).** `previewJoin(code)` reads what a join code holds
29
+ without using it (`OwJoinPreview`: `reader`, `world`, `invited_by`, `owner_character`,
30
+ `role`, `with_character`, `expires_at`) and sends no credential, whatever key the client
31
+ holds. `acceptJoin(code)` accepts an invite link as the account behind the client's `ow_a_`
32
+ token and returns the new membership (`OwJoinedMembership`, `roster_character` included);
33
+ a world key is a `TypeError` before anything is sent. New getters on `OwApiError`:
34
+ `isNotFound` (every bad code is one 404), `isAlreadyMember` and `isPinRequired` (the two
35
+ 409s, told apart by code). Shapes read from keel's code (`accounts/join_api.py`,
36
+ `accounts/api.py`), since the OpenAPI document gives no response bodies for these routes.
37
+ Checked live: a bad code on production answers `isNotFound`. A real accept has not been
38
+ run from here (no account token); Lantern's door is its first live use.
39
+
8
40
  ## [4.11.0] — 2026-10-12
9
41
 
10
42
  ### Added
package/README.md CHANGED
@@ -110,6 +110,17 @@ await writer.patch('character', id, { image_url: null });
110
110
 
111
111
  The image's uploader or the world's owner can remove an uploaded image; the space goes back to the uploader's storage. Removal leaves elements as they are, so clear their `image_url` yourself (`createRemovalTicket(key).referenced` says how many still show it). A copy the network already holds can keep answering for a while.
112
112
 
113
+ ## Invite links
114
+
115
+ A world's owner can invite a person with a link (`https://www.onlyworlds.com/join#ow_j_…`). The code is the part after `#`:
116
+
117
+ ```typescript
118
+ const invite = await client.previewJoin(code); // who invites, which world, what role; sends no key
119
+ const membership = await account.acceptJoin(code); // account = a client with an ow_a_ token
120
+ ```
121
+
122
+ A bad code (unknown, used, revoked, expired) is one 404 (`err.isNotFound`). Accepting can answer `err.isAlreadyMember` or `err.isPinRequired` (set a PIN first); a refusal never uses up the code.
123
+
113
124
  ## Sync
114
125
 
115
126
  ```typescript
@@ -150,7 +161,16 @@ hits[0].element; hits[0].reasons; // best first, with why each m
150
161
  renderElement(hits[0].element, index); // a short text card, links by name
151
162
  ```
152
163
 
153
- Names come first, then supertype and subtype, then one link step in either direction, then the elements' other text. To have a model phrase an answer, give it the cards of the top hits and ask it to answer only from them.
164
+ Names come first, then supertype and subtype, then one link step in either direction, then the elements' other text.
165
+
166
+ To have an answer written from what you found, OnlyWorlds runs a small AI proxy for signed-in accounts (50 questions a day):
167
+
168
+ ```typescript
169
+ const shown = hits.slice(0, 8); // what your screen lists
170
+ const { context, kept } = buildAskContext(shown, index); // within the proxy's limits; kept = what was sent
171
+ const reply = await account.ask({ worldId, question, context }); // account: a client with an ow_a_ token
172
+ reply.answer; // from those elements only, or "The world doesn't say."
173
+ ```
154
174
 
155
175
  ## World files
156
176
 
package/dist/index.d.ts CHANGED
@@ -1988,6 +1988,51 @@ interface OwRemovalTicket {
1988
1988
  referenced: number;
1989
1989
  }
1990
1990
  /** The edge's 200 for one removal. */
1991
+ /** POST /account/ask: keel's AI proxy's answer (keel #159). */
1992
+ interface OwAskAnswer {
1993
+ /** The answer, from the context only; "The world doesn't say." when it does not hold one. */
1994
+ answer: string;
1995
+ model: string;
1996
+ stop_reason: string | null;
1997
+ usage: {
1998
+ input_tokens: number;
1999
+ output_tokens: number;
2000
+ };
2001
+ /** Questions the account has left today. */
2002
+ remaining: number;
2003
+ [k: string]: unknown;
2004
+ }
2005
+ /** POST /join/preview: what a join code holds, read without using it (keel #212). */
2006
+ interface OwJoinPreview {
2007
+ /** `person`: an invite link, accepted with acceptJoin. `agent`: redeemed by an AI agent at POST /join. */
2008
+ reader: 'person' | 'agent' | (string & {});
2009
+ world: {
2010
+ name: string;
2011
+ description: string | null;
2012
+ };
2013
+ /** The name the inviter signed the link with, or null. */
2014
+ invited_by: string | null;
2015
+ /** The world owner's chosen Character's name, never a username; null when none is set. */
2016
+ owner_character: string | null;
2017
+ role: 'co-builder' | 'contributor' | 'guest' | (string & {});
2018
+ /** Whether joining gives the joiner a Character of their own (agent links). */
2019
+ with_character: boolean | null;
2020
+ /** ISO 8601, UTC. */
2021
+ expires_at: string;
2022
+ [k: string]: unknown;
2023
+ }
2024
+ /** POST /account/join: the new membership (the same row as accepting an email invite). */
2025
+ interface OwJoinedMembership {
2026
+ /** The membership's id. */
2027
+ id: string;
2028
+ world_id: string;
2029
+ world_name: string;
2030
+ role: 'co-builder' | 'contributor' | 'guest' | (string & {});
2031
+ joined_at: string | null;
2032
+ /** The id of the joiner's roster Character, or null. */
2033
+ roster_character: string | null;
2034
+ [k: string]: unknown;
2035
+ }
1991
2036
  interface OwRemovedImage {
1992
2037
  /** The object key that was removed. */
1993
2038
  removed: string;
@@ -2139,6 +2184,18 @@ declare class OwApiError extends Error {
2139
2184
  * is now, so no second read is needed.
2140
2185
  */
2141
2186
  get isPreconditionFailed(): boolean;
2187
+ /** 404 `not_found`: no such element or route, or a join code that is unknown, used, revoked or expired. */
2188
+ get isNotFound(): boolean;
2189
+ /** acceptJoin: the account is already a member of that world (409 `already_member`). The code is not spent. */
2190
+ get isAlreadyMember(): boolean;
2191
+ /** acceptJoin: the account has no PIN yet, and a member writes with their own (409 `pin_required`). The code is not spent. */
2192
+ get isPinRequired(): boolean;
2193
+ /** A daily allowance is used up (429 `quota_exceeded`): the world's image tickets, or the account's asks. See retryAfter. */
2194
+ get isQuotaExceeded(): boolean;
2195
+ /** ask: the proxy is switched off, or paused for the day by the server-wide cap (503 `ask_unavailable`). Not counted. */
2196
+ get isAskUnavailable(): boolean;
2197
+ /** ask: the model gave no answer (502 `ask_failed`). Not counted; try again in a moment. */
2198
+ get isAskFailed(): boolean;
2142
2199
  }
2143
2200
  /**
2144
2201
  * 412 `precondition_failed` (keel #210): the element changed since the `change_seq` sent as
@@ -2368,6 +2425,38 @@ declare class OwV2Client {
2368
2425
  removeImage(keyOrUrl: string, opts?: {
2369
2426
  ticket?: OwRemovalTicket;
2370
2427
  }): Promise<OwRemovedImage>;
2428
+ /**
2429
+ * POST /join/preview -- what a join code holds, without using it: who invites, to
2430
+ * which world, in what role, and `reader` (`person`: accept it with acceptJoin;
2431
+ * `agent`: an AI agent redeems it at POST /join). Sends no credential, whatever key
2432
+ * this client holds. Every bad code (unknown, used, revoked, expired) is the same
2433
+ * 404 `not_found` (`err.isNotFound`). `code` is the part of the link after `#`.
2434
+ */
2435
+ previewJoin(code: string): Promise<OwJoinPreview>;
2436
+ /**
2437
+ * POST /account/join -- accept an invite link's code as the account this client's
2438
+ * `ow_a_` token belongs to: 201 with the new membership. Refusals: 404 `not_found`
2439
+ * (every bad code, and a code made for an AI agent), 409 `already_member`
2440
+ * (`err.isAlreadyMember`) and 409 `pin_required` (`err.isPinRequired`: the account
2441
+ * has no PIN yet). A refusal never spends the code. A world key here is a TypeError
2442
+ * before anything is sent.
2443
+ */
2444
+ acceptJoin(code: string): Promise<OwJoinedMembership>;
2445
+ /**
2446
+ * POST /account/ask -- a question about a world the account owns or is in, answered by
2447
+ * keel's AI proxy from `context` only (build it with buildAskContext from the elements
2448
+ * the user sees). The answer is "The world doesn't say." when the context does not hold
2449
+ * it. Needs the client's `ow_a_` token (a world key is a TypeError before sending).
2450
+ * Refusals: 429 `quota_exceeded` (`err.isQuotaExceeded`, the account's daily allowance;
2451
+ * Retry-After runs to midnight UTC), 503 `ask_unavailable` (`err.isAskUnavailable`:
2452
+ * switched off, or paused for the day), 502 `ask_failed` (`err.isAskFailed`); none of
2453
+ * them counts. 422 for a question over 500 characters or context past its limits.
2454
+ */
2455
+ ask(req: {
2456
+ worldId: string;
2457
+ question: string;
2458
+ context: string[];
2459
+ }): Promise<OwAskAnswer>;
2371
2460
  /**
2372
2461
  * Raw authenticated request against this client's baseUrl. Public since 4.0
2373
2462
  * so auxiliary resources can ride the same transport — it structurally
@@ -2660,6 +2749,45 @@ declare function lookupWorld<T extends OwLookupElement>(index: OwWorldIndex<T>,
2660
2749
  */
2661
2750
  declare function renderElement<T extends OwLookupElement>(element: T, index: OwWorldIndex<T>, { descMax, linkMax, incomingMax }?: OwRenderOptions): string;
2662
2751
 
2752
+ /**
2753
+ * Context for keel's AI proxy (POST /api/v2/account/ask, Keel #159): the elements a
2754
+ * question is about, as text, within the proxy's limits. The caller passes the
2755
+ * elements it shows the user, in its own order, and gets back exactly which were sent:
2756
+ * the answer's sources are what the user saw, never a second lookup's.
2757
+ *
2758
+ * const hits = lookupWorld(index, question).slice(0, 8); // what the screen lists
2759
+ * const { context, kept } = buildAskContext(hits, index);
2760
+ * const reply = await account.ask({ worldId, question, context });
2761
+ *
2762
+ * The limits are keel's (accounts/ask.py), counted the way keel counts (characters,
2763
+ * not UTF-16 units): at most 8 items, 2,000 characters each, 12,000 in all.
2764
+ */
2765
+
2766
+ /** keel's limits on one ask. */
2767
+ declare const ASK_LIMITS: {
2768
+ readonly questionMax: 500;
2769
+ readonly itemsMax: 8;
2770
+ readonly itemMax: 2000;
2771
+ readonly totalMax: 12000;
2772
+ };
2773
+ interface OwAskContext {
2774
+ /** The texts to send as `context`, in the order given. */
2775
+ context: string[];
2776
+ /** Ids of the elements sent, in order (one per `context` item). */
2777
+ kept: string[];
2778
+ /** Ids of the elements left out: past the 8th, or from the first one the 12,000 total could not hold. */
2779
+ dropped: string[];
2780
+ /** Ids among `kept` whose text was cut to 2,000 characters. */
2781
+ truncated: string[];
2782
+ }
2783
+ /**
2784
+ * Turn the elements on screen into the proxy's `context`. Takes lookup hits or plain
2785
+ * elements, keeps their order, renders each with `renderElement`, cuts a text over
2786
+ * 2,000 characters, and stops at the 8th or at the first one the 12,000 total cannot
2787
+ * hold, so what is sent is always the top of the list as shown.
2788
+ */
2789
+ declare function buildAskContext<T extends OwLookupElement>(items: Array<OwLookupHit<T> | T>, index: OwWorldIndex<T>): OwAskContext;
2790
+
2663
2791
  /**
2664
2792
  * Photo preparation: make a picture from a phone's camera or files ready for
2665
2793
  * `uploadImage` (webp, png, jpeg or avif, at most 15 MB), in the browser,
@@ -3009,4 +3137,4 @@ declare class TokenResource {
3009
3137
  getEncryptionInfo(): Promise<EncryptionInfo>;
3010
3138
  }
3011
3139
 
3012
- export { type AccessKeyResponse, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, OwApiError, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwChange, type OwChangesPage, type OwClientConfig, type OwElement, type OwElementBase, type OwErrorBody, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMediaTicket, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldIndex, type OwWorldMeta, PHOTO_MAX_BYTES, PHOTO_TYPES, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, buildWorldIndex, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, lookupWorld, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readWorldFile, renderElement, writeWorldFile };
3140
+ export { ASK_LIMITS, type AccessKeyResponse, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwChange, type OwChangesPage, type OwClientConfig, type OwElement, type OwElementBase, type OwErrorBody, type OwJoinPreview, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMediaTicket, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldIndex, type OwWorldMeta, PHOTO_MAX_BYTES, PHOTO_TYPES, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, buildAskContext, buildWorldIndex, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, lookupWorld, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readWorldFile, renderElement, writeWorldFile };
package/dist/index.js CHANGED
@@ -75,6 +75,30 @@ var OwApiError = class extends Error {
75
75
  get isPreconditionFailed() {
76
76
  return this.status === 412 && this.code === "precondition_failed";
77
77
  }
78
+ /** 404 `not_found`: no such element or route, or a join code that is unknown, used, revoked or expired. */
79
+ get isNotFound() {
80
+ return this.status === 404 && this.code === "not_found";
81
+ }
82
+ /** acceptJoin: the account is already a member of that world (409 `already_member`). The code is not spent. */
83
+ get isAlreadyMember() {
84
+ return this.status === 409 && this.code === "already_member";
85
+ }
86
+ /** acceptJoin: the account has no PIN yet, and a member writes with their own (409 `pin_required`). The code is not spent. */
87
+ get isPinRequired() {
88
+ return this.status === 409 && this.code === "pin_required";
89
+ }
90
+ /** A daily allowance is used up (429 `quota_exceeded`): the world's image tickets, or the account's asks. See retryAfter. */
91
+ get isQuotaExceeded() {
92
+ return this.status === 429 && this.code === "quota_exceeded";
93
+ }
94
+ /** ask: the proxy is switched off, or paused for the day by the server-wide cap (503 `ask_unavailable`). Not counted. */
95
+ get isAskUnavailable() {
96
+ return this.status === 503 && this.code === "ask_unavailable";
97
+ }
98
+ /** ask: the model gave no answer (502 `ask_failed`). Not counted; try again in a moment. */
99
+ get isAskFailed() {
100
+ return this.status === 502 && this.code === "ask_failed";
101
+ }
78
102
  };
79
103
  var OwPreconditionFailedError = class extends OwApiError {
80
104
  constructor(status, code, message, docUrl, detail, type = null, param = null, retryAfter = null) {
@@ -410,6 +434,50 @@ var OwV2Client = class {
410
434
  if (!res.ok) throw await errorFromResponse(res);
411
435
  return await res.json();
412
436
  }
437
+ // -- Joining a world by link (keel #212) ----------------------------------
438
+ /**
439
+ * POST /join/preview -- what a join code holds, without using it: who invites, to
440
+ * which world, in what role, and `reader` (`person`: accept it with acceptJoin;
441
+ * `agent`: an AI agent redeems it at POST /join). Sends no credential, whatever key
442
+ * this client holds. Every bad code (unknown, used, revoked, expired) is the same
443
+ * 404 `not_found` (`err.isNotFound`). `code` is the part of the link after `#`.
444
+ */
445
+ async previewJoin(code) {
446
+ return this.request("POST", "/join/preview", { auth: false, body: { code } });
447
+ }
448
+ /**
449
+ * POST /account/join -- accept an invite link's code as the account this client's
450
+ * `ow_a_` token belongs to: 201 with the new membership. Refusals: 404 `not_found`
451
+ * (every bad code, and a code made for an AI agent), 409 `already_member`
452
+ * (`err.isAlreadyMember`) and 409 `pin_required` (`err.isPinRequired`: the account
453
+ * has no PIN yet). A refusal never spends the code. A world key here is a TypeError
454
+ * before anything is sent.
455
+ */
456
+ async acceptJoin(code) {
457
+ if (this.keyKind !== "account") {
458
+ throw new TypeError("acceptJoin needs an account token (ow_a_): a world key cannot join a world");
459
+ }
460
+ return this.request("POST", "/account/join", { body: { code } });
461
+ }
462
+ // -- Ask your world (keel #159) -------------------------------------------
463
+ /**
464
+ * POST /account/ask -- a question about a world the account owns or is in, answered by
465
+ * keel's AI proxy from `context` only (build it with buildAskContext from the elements
466
+ * the user sees). The answer is "The world doesn't say." when the context does not hold
467
+ * it. Needs the client's `ow_a_` token (a world key is a TypeError before sending).
468
+ * Refusals: 429 `quota_exceeded` (`err.isQuotaExceeded`, the account's daily allowance;
469
+ * Retry-After runs to midnight UTC), 503 `ask_unavailable` (`err.isAskUnavailable`:
470
+ * switched off, or paused for the day), 502 `ask_failed` (`err.isAskFailed`); none of
471
+ * them counts. 422 for a question over 500 characters or context past its limits.
472
+ */
473
+ async ask(req) {
474
+ if (this.keyKind !== "account") {
475
+ throw new TypeError("ask needs an account token (ow_a_): the proxy meters per account");
476
+ }
477
+ return this.request("POST", "/account/ask", {
478
+ body: { world_id: req.worldId, question: req.question, context: req.context }
479
+ });
480
+ }
413
481
  // -- Core request machinery ----------------------------------------------
414
482
  /**
415
483
  * Raw authenticated request against this client's baseUrl. Public since 4.0
@@ -1834,6 +1902,38 @@ function renderElement(element, index, { descMax = 500, linkMax = 12, incomingMa
1834
1902
  return lines.join("\n");
1835
1903
  }
1836
1904
 
1905
+ // src/ask.ts
1906
+ var ASK_LIMITS = { questionMax: 500, itemsMax: 8, itemMax: 2e3, totalMax: 12e3 };
1907
+ var chars = (s) => [...s].length;
1908
+ var cut = (s, n) => [...s].slice(0, n).join("");
1909
+ function buildAskContext(items, index) {
1910
+ const out = { context: [], kept: [], dropped: [], truncated: [] };
1911
+ let total = 0;
1912
+ let full = false;
1913
+ for (const item of items) {
1914
+ const el = item && typeof item === "object" && "element" in item ? item.element : item;
1915
+ if (full || out.context.length >= ASK_LIMITS.itemsMax) {
1916
+ out.dropped.push(el.id);
1917
+ continue;
1918
+ }
1919
+ let text = renderElement(el, index);
1920
+ if (chars(text) > ASK_LIMITS.itemMax) {
1921
+ text = cut(text, ASK_LIMITS.itemMax);
1922
+ out.truncated.push(el.id);
1923
+ }
1924
+ if (total + chars(text) > ASK_LIMITS.totalMax) {
1925
+ full = true;
1926
+ out.truncated = out.truncated.filter((id) => id !== el.id);
1927
+ out.dropped.push(el.id);
1928
+ continue;
1929
+ }
1930
+ total += chars(text);
1931
+ out.context.push(text);
1932
+ out.kept.push(el.id);
1933
+ }
1934
+ return out;
1935
+ }
1936
+
1837
1937
  // src/photo.ts
1838
1938
  var PHOTO_TYPES = ["image/webp", "image/png", "image/jpeg", "image/avif"];
1839
1939
  var PHOTO_MAX_BYTES = 15 * 1024 * 1024;
@@ -2068,6 +2168,7 @@ var GameTier = /* @__PURE__ */ ((GameTier2) => {
2068
2168
  return GameTier2;
2069
2169
  })(GameTier || {});
2070
2170
  export {
2171
+ ASK_LIMITS,
2071
2172
  ELEMENT_FAMILIES,
2072
2173
  ELEMENT_ICONS,
2073
2174
  ELEMENT_LABELS,
@@ -2089,6 +2190,7 @@ export {
2089
2190
  TokenResource,
2090
2191
  WORLD_FILE_MAX_BYTES,
2091
2192
  WORLD_FILE_MAX_ENTRIES,
2193
+ buildAskContext,
2092
2194
  buildWorldIndex,
2093
2195
  detectKeyKind,
2094
2196
  elementColor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlyworlds/sdk",
3
- "version": "4.11.0",
3
+ "version": "4.13.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {