@onlyworlds/sdk 4.12.0 → 4.14.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,9 @@ 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.
43
46
  - Invite links (since 4.12.0): `previewJoin(code)` (no credential sent) shows who invites, to
44
47
  which world, in what role; `acceptJoin(code)` with an `ow_a_` account token joins. Bad code:
45
48
  `err.isNotFound`; `err.isAlreadyMember`, `err.isPinRequired` for the two 409s.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.14.0] — 2026-10-12
9
+
10
+ ### Fixed
11
+ - **The co-builder role is `co_builder` on the wire.** `OwJoinPreview.role` and
12
+ `OwJoinedMembership.role` listed `'co-builder'`, which is only the label keel shows (keel
13
+ `MemberRole.CO_BUILDER = "co_builder"`). The types were open, so nothing broke at runtime, but
14
+ a comparison written from them would never match. Caught by Temper from Lantern's fakes.
15
+
16
+ ### Added
17
+ - `OwMemberRole` (`'co_builder' | 'contributor' | 'guest'`, open), the type both now use.
18
+
19
+ ## [4.13.0] — 2026-10-12
20
+
21
+ ### Added
22
+ - **Ask your world (keel #159).** `client.ask({ worldId, question, context })` sends a
23
+ question to keel's AI proxy with the account's `ow_a_` token (a world key is a `TypeError`
24
+ before sending) and returns `OwAskAnswer` (`answer`, `model`, `stop_reason`, `usage`,
25
+ `remaining`). The answer comes from `context` only, or is "The world doesn't say.".
26
+ `buildAskContext(items, index)` turns the elements the user sees (lookup hits or plain
27
+ elements, in the caller's order, never re-looked-up) into that `context` within keel's
28
+ limits (`ASK_LIMITS`: 8 items, 2,000 characters each, 12,000 in all, counted in characters
29
+ as keel counts them) and says what it did: `kept`, `dropped` (past the 8th, or from the first
30
+ one the total cannot hold, so what is sent is always the top of the list) and `truncated`.
31
+ New getters: `isQuotaExceeded` (429; `retryAfter` runs to midnight UTC), `isAskUnavailable`
32
+ (503), `isAskFailed` (502). keel's document gives no request schema for `/account/ask`, so
33
+ the body (`world_id`, `question`, `context`) is read from keel's code (`accounts/ask.py`).
34
+ Not yet run live: no crew session holds an account token.
35
+
8
36
  ## [4.12.0] — 2026-10-12
9
37
 
10
38
  ### Added
package/README.md CHANGED
@@ -161,7 +161,16 @@ hits[0].element; hits[0].reasons; // best first, with why each m
161
161
  renderElement(hits[0].element, index); // a short text card, links by name
162
162
  ```
163
163
 
164
- 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
+ ```
165
174
 
166
175
  ## World files
167
176
 
package/dist/index.d.ts CHANGED
@@ -1988,6 +1988,25 @@ interface OwRemovalTicket {
1988
1988
  referenced: number;
1989
1989
  }
1990
1990
  /** The edge's 200 for one removal. */
1991
+ /**
1992
+ * A member's role as the wire sends it (keel `MemberRole`, accounts/models.py). `co_builder`
1993
+ * is the value; "co-builder" is only its label in prose. Open, so a new role still types.
1994
+ */
1995
+ type OwMemberRole = 'co_builder' | 'contributor' | 'guest' | (string & {});
1996
+ /** POST /account/ask: keel's AI proxy's answer (keel #159). */
1997
+ interface OwAskAnswer {
1998
+ /** The answer, from the context only; "The world doesn't say." when it does not hold one. */
1999
+ answer: string;
2000
+ model: string;
2001
+ stop_reason: string | null;
2002
+ usage: {
2003
+ input_tokens: number;
2004
+ output_tokens: number;
2005
+ };
2006
+ /** Questions the account has left today. */
2007
+ remaining: number;
2008
+ [k: string]: unknown;
2009
+ }
1991
2010
  /** POST /join/preview: what a join code holds, read without using it (keel #212). */
1992
2011
  interface OwJoinPreview {
1993
2012
  /** `person`: an invite link, accepted with acceptJoin. `agent`: redeemed by an AI agent at POST /join. */
@@ -2000,7 +2019,7 @@ interface OwJoinPreview {
2000
2019
  invited_by: string | null;
2001
2020
  /** The world owner's chosen Character's name, never a username; null when none is set. */
2002
2021
  owner_character: string | null;
2003
- role: 'co-builder' | 'contributor' | 'guest' | (string & {});
2022
+ role: OwMemberRole;
2004
2023
  /** Whether joining gives the joiner a Character of their own (agent links). */
2005
2024
  with_character: boolean | null;
2006
2025
  /** ISO 8601, UTC. */
@@ -2013,7 +2032,7 @@ interface OwJoinedMembership {
2013
2032
  id: string;
2014
2033
  world_id: string;
2015
2034
  world_name: string;
2016
- role: 'co-builder' | 'contributor' | 'guest' | (string & {});
2035
+ role: OwMemberRole;
2017
2036
  joined_at: string | null;
2018
2037
  /** The id of the joiner's roster Character, or null. */
2019
2038
  roster_character: string | null;
@@ -2176,6 +2195,12 @@ declare class OwApiError extends Error {
2176
2195
  get isAlreadyMember(): boolean;
2177
2196
  /** acceptJoin: the account has no PIN yet, and a member writes with their own (409 `pin_required`). The code is not spent. */
2178
2197
  get isPinRequired(): boolean;
2198
+ /** A daily allowance is used up (429 `quota_exceeded`): the world's image tickets, or the account's asks. See retryAfter. */
2199
+ get isQuotaExceeded(): boolean;
2200
+ /** ask: the proxy is switched off, or paused for the day by the server-wide cap (503 `ask_unavailable`). Not counted. */
2201
+ get isAskUnavailable(): boolean;
2202
+ /** ask: the model gave no answer (502 `ask_failed`). Not counted; try again in a moment. */
2203
+ get isAskFailed(): boolean;
2179
2204
  }
2180
2205
  /**
2181
2206
  * 412 `precondition_failed` (keel #210): the element changed since the `change_seq` sent as
@@ -2422,6 +2447,21 @@ declare class OwV2Client {
2422
2447
  * before anything is sent.
2423
2448
  */
2424
2449
  acceptJoin(code: string): Promise<OwJoinedMembership>;
2450
+ /**
2451
+ * POST /account/ask -- a question about a world the account owns or is in, answered by
2452
+ * keel's AI proxy from `context` only (build it with buildAskContext from the elements
2453
+ * the user sees). The answer is "The world doesn't say." when the context does not hold
2454
+ * it. Needs the client's `ow_a_` token (a world key is a TypeError before sending).
2455
+ * Refusals: 429 `quota_exceeded` (`err.isQuotaExceeded`, the account's daily allowance;
2456
+ * Retry-After runs to midnight UTC), 503 `ask_unavailable` (`err.isAskUnavailable`:
2457
+ * switched off, or paused for the day), 502 `ask_failed` (`err.isAskFailed`); none of
2458
+ * them counts. 422 for a question over 500 characters or context past its limits.
2459
+ */
2460
+ ask(req: {
2461
+ worldId: string;
2462
+ question: string;
2463
+ context: string[];
2464
+ }): Promise<OwAskAnswer>;
2425
2465
  /**
2426
2466
  * Raw authenticated request against this client's baseUrl. Public since 4.0
2427
2467
  * so auxiliary resources can ride the same transport — it structurally
@@ -2714,6 +2754,45 @@ declare function lookupWorld<T extends OwLookupElement>(index: OwWorldIndex<T>,
2714
2754
  */
2715
2755
  declare function renderElement<T extends OwLookupElement>(element: T, index: OwWorldIndex<T>, { descMax, linkMax, incomingMax }?: OwRenderOptions): string;
2716
2756
 
2757
+ /**
2758
+ * Context for keel's AI proxy (POST /api/v2/account/ask, Keel #159): the elements a
2759
+ * question is about, as text, within the proxy's limits. The caller passes the
2760
+ * elements it shows the user, in its own order, and gets back exactly which were sent:
2761
+ * the answer's sources are what the user saw, never a second lookup's.
2762
+ *
2763
+ * const hits = lookupWorld(index, question).slice(0, 8); // what the screen lists
2764
+ * const { context, kept } = buildAskContext(hits, index);
2765
+ * const reply = await account.ask({ worldId, question, context });
2766
+ *
2767
+ * The limits are keel's (accounts/ask.py), counted the way keel counts (characters,
2768
+ * not UTF-16 units): at most 8 items, 2,000 characters each, 12,000 in all.
2769
+ */
2770
+
2771
+ /** keel's limits on one ask. */
2772
+ declare const ASK_LIMITS: {
2773
+ readonly questionMax: 500;
2774
+ readonly itemsMax: 8;
2775
+ readonly itemMax: 2000;
2776
+ readonly totalMax: 12000;
2777
+ };
2778
+ interface OwAskContext {
2779
+ /** The texts to send as `context`, in the order given. */
2780
+ context: string[];
2781
+ /** Ids of the elements sent, in order (one per `context` item). */
2782
+ kept: string[];
2783
+ /** Ids of the elements left out: past the 8th, or from the first one the 12,000 total could not hold. */
2784
+ dropped: string[];
2785
+ /** Ids among `kept` whose text was cut to 2,000 characters. */
2786
+ truncated: string[];
2787
+ }
2788
+ /**
2789
+ * Turn the elements on screen into the proxy's `context`. Takes lookup hits or plain
2790
+ * elements, keeps their order, renders each with `renderElement`, cuts a text over
2791
+ * 2,000 characters, and stops at the 8th or at the first one the 12,000 total cannot
2792
+ * hold, so what is sent is always the top of the list as shown.
2793
+ */
2794
+ declare function buildAskContext<T extends OwLookupElement>(items: Array<OwLookupHit<T> | T>, index: OwWorldIndex<T>): OwAskContext;
2795
+
2717
2796
  /**
2718
2797
  * Photo preparation: make a picture from a phone's camera or files ready for
2719
2798
  * `uploadImage` (webp, png, jpeg or avif, at most 15 MB), in the browser,
@@ -3063,4 +3142,4 @@ declare class TokenResource {
3063
3142
  getEncryptionInfo(): Promise<EncryptionInfo>;
3064
3143
  }
3065
3144
 
3066
- 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 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, buildWorldIndex, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, lookupWorld, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readWorldFile, renderElement, writeWorldFile };
3145
+ 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, type OwMemberRole, 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
@@ -87,6 +87,18 @@ var OwApiError = class extends Error {
87
87
  get isPinRequired() {
88
88
  return this.status === 409 && this.code === "pin_required";
89
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
+ }
90
102
  };
91
103
  var OwPreconditionFailedError = class extends OwApiError {
92
104
  constructor(status, code, message, docUrl, detail, type = null, param = null, retryAfter = null) {
@@ -447,6 +459,25 @@ var OwV2Client = class {
447
459
  }
448
460
  return this.request("POST", "/account/join", { body: { code } });
449
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
+ }
450
481
  // -- Core request machinery ----------------------------------------------
451
482
  /**
452
483
  * Raw authenticated request against this client's baseUrl. Public since 4.0
@@ -1871,6 +1902,38 @@ function renderElement(element, index, { descMax = 500, linkMax = 12, incomingMa
1871
1902
  return lines.join("\n");
1872
1903
  }
1873
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
+
1874
1937
  // src/photo.ts
1875
1938
  var PHOTO_TYPES = ["image/webp", "image/png", "image/jpeg", "image/avif"];
1876
1939
  var PHOTO_MAX_BYTES = 15 * 1024 * 1024;
@@ -2105,6 +2168,7 @@ var GameTier = /* @__PURE__ */ ((GameTier2) => {
2105
2168
  return GameTier2;
2106
2169
  })(GameTier || {});
2107
2170
  export {
2171
+ ASK_LIMITS,
2108
2172
  ELEMENT_FAMILIES,
2109
2173
  ELEMENT_ICONS,
2110
2174
  ELEMENT_LABELS,
@@ -2126,6 +2190,7 @@ export {
2126
2190
  TokenResource,
2127
2191
  WORLD_FILE_MAX_BYTES,
2128
2192
  WORLD_FILE_MAX_ENTRIES,
2193
+ buildAskContext,
2129
2194
  buildWorldIndex,
2130
2195
  detectKeyKind,
2131
2196
  elementColor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlyworlds/sdk",
3
- "version": "4.12.0",
3
+ "version": "4.14.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {