@onlyworlds/sdk 3.1.0 → 4.0.0-alpha.1

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 CHANGED
@@ -1,332 +1,144 @@
1
- # OnlyWorlds TypeScript SDK
2
-
3
- [![npm version](https://badge.fury.io/js/@onlyworlds%2Fsdk.svg)](https://www.npmjs.com/package/@onlyworlds/sdk)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
-
6
- A type-safe TypeScript SDK for building world-building applications with the OnlyWorlds API.
7
-
8
- The SDK speaks two dialects of the same API:
9
-
10
- - **v2 (`OwV2Client`)** -- the current, recommended default. Local-first shape: one-shape read/write (no `_ids` suffix), an opaque `/changes` sync feed, atomic bulk writes, and client-side UUID minting for safe retries. Base URL `https://www.onlyworlds.com/api/v2`.
11
- - **v1 (`OnlyWorldsClient`)** -- the original resource-style client. Fully served forever; kept below as the legacy section. Existing 2.x code keeps working unchanged -- v2 is purely additive.
12
-
13
- ## Installation
14
-
15
- ```bash
16
- npm install @onlyworlds/sdk
17
- ```
18
-
19
- ## Quick Start (v2 -- recommended)
20
-
21
- ```typescript
22
- import { OwV2Client } from '@onlyworlds/sdk';
23
-
24
- const client = new OwV2Client({
25
- apiKey: 'ow_w_your_world_key', // ow_w_ write / ow_r_ read / ow_a_ account / 10-digit legacy
26
- apiPin: '1234', // needed for writes on a PIN-protected world
27
- });
28
- // baseUrl defaults to https://www.onlyworlds.com/api/v2
29
-
30
- // Read a page, or walk every page of a type
31
- const page = await client.list('character'); // { data, has_more, next_cursor }
32
- for await (const character of client.listAll('character')) {
33
- // ...
34
- }
35
-
36
- // A worked round-trip: two elements, linked, read back. Byte-true v2 dialect:
37
- // link fields use ONE bare name both directions (no _ids suffix — that's v1),
38
- // and you NEVER send a "world" field (identity comes from the key; sending it 422s).
39
- const location = await client.create('location', {
40
- name: 'Dragon Peak',
41
- description: 'A treacherous mountain peak where dragons nest',
42
- }); // id minted client-side when omitted (retries stay idempotent)
43
-
44
- const dragon = await client.create('creature', {
45
- name: 'Vorrath the Ember-Scaled',
46
- location: location.id, // single link: UUID (or null)
47
- });
48
-
49
- const fetched = await client.get('creature', dragon.id);
50
- // fetched.location === location.id — reads the way it writes
51
-
52
- // Partial update (PATCH is destructive on sent fields; arrays replace wholesale)
53
- await client.patch('location', location.id, { supertype: 'Mountain' });
54
-
55
- // For relationships, prefer the atomic link merge -- returns the full updated element
56
- const fireBreath = await client.create('ability', { name: 'Ember Breath' });
57
- await client.editLinks('creature', dragon.id, 'abilities', { add: [fireBreath.id], remove: [] });
58
-
59
- // Bulk write (up to ~1000; partial success by default, atomic:true for all-or-nothing)
60
- const res = await client.bulk([
61
- { type: 'character', element: { name: 'A' } },
62
- { type: 'event', element: { name: 'B' } },
63
- ]);
64
- // res.items[i].status is the per-slot HTTP status; res.errors flags any failure
65
-
66
- // Sync: walk the opaque change feed and persist the final cursor
67
- let cursor;
68
- for await (const change of client.changesAll(cursor)) {
69
- // apply change in order
70
- }
71
- ```
72
-
73
- ### Canonical element colours
74
-
75
- ```typescript
76
- import { elementColor, ELEMENT_FAMILIES, FAMILY_COLORS } from '@onlyworlds/sdk';
77
-
78
- elementColor('character', 'dark'); // '#3987e5' — colour carries the FAMILY
79
- // (agents / world / abstract / temporal); ELEMENT_ICONS carries the TYPE.
80
- // CVD-validated: always pair colour with icon + label, never colour alone.
81
- ```
82
-
83
- ### AI-assistant access (MCP) — and when to use which
84
-
85
- An MCP server exists at `https://www.onlyworlds.com/mcp` for AI assistants (Claude and other MCP clients) to read and write worlds directly -- no SDK code required. See the [docs](https://onlyworlds.github.io) for setup.
86
-
87
- Division of labor: for **known, deterministic operations** (CRUD, sync, bulk) use this SDK — typed calls, no tool-schema overhead. For **live exploration of a user's world from a chat/agent context**, use the MCP server. Agents: see `AGENTS.md` in this package.
88
-
89
- ## Legacy: v1 client (`OnlyWorldsClient`)
90
-
91
- The v1 resource-style client remains fully supported and served forever. Prefer `OwV2Client` for new code.
92
-
93
- ```typescript
94
- import { OnlyWorldsClient } from '@onlyworlds/sdk';
95
-
96
- const client = new OnlyWorldsClient({
97
- apiKey: 'your-api-key',
98
- apiPin: '1234'
99
- });
100
- // baseUrl defaults to https://www.onlyworlds.com/api/worldapi — only override it
101
- // when pointing at a different host (it must include the full API path)
102
-
103
- // Get your world (API keys are world-scoped)
104
- const world = await client.worlds.get();
105
-
106
- // Fetch characters (paginated)
107
- const characters = await client.characters.list();
108
-
109
- // Create a new location
110
- const location = await client.locations.create({
111
- name: 'Dragon Peak',
112
- description: 'A treacherous mountain peak where dragons nest',
113
- supertype: 'mountain'
114
- });
115
-
116
- // Get a specific element
117
- const character = await client.characters.get('element-id');
118
-
119
- // Update an element
120
- await client.characters.update('element-id', {
121
- name: 'Updated Name'
122
- });
123
-
124
- // Delete an element
125
- await client.characters.delete('element-id');
126
- ```
127
-
128
- ## Features
129
-
130
- - ✅ **Full Type Safety** - Complete TypeScript definitions for all 22 OnlyWorlds element types
131
- - ✅ **Schema-Aligned Types** - Type definitions track the OnlyWorlds schema
132
- - ✅ **CRUD Operations** - Create, read, update, and delete operations for all elements
133
- - ✅ **Token Management** - Built-in support for OnlyWorlds token rating system
134
- - ✅ **Branded Types** - Compile-time safety for element relationships
135
- - ✅ **Zero Runtime Overhead** - Type system has no runtime cost
136
- - ✅ **Modern ESM/CJS** - Supports both ES modules and CommonJS
137
-
138
- ## API Reference
139
-
140
- ### World Endpoint
141
-
142
- The `worlds` resource is special because API keys are world-scoped (one key = one world). The endpoint returns a single `World` object directly, not a paginated list.
143
-
144
- ```typescript
145
- // Get your world
146
- const world = await client.worlds.get();
147
-
148
- // Update your world
149
- const updated = await client.worlds.update({
150
- description: 'A dark fantasy realm'
151
- });
152
- ```
153
-
154
- **Note**: The `worlds` resource only has `get()` and `update()` methods (no `list()`, `create()`, or `delete()`) because API keys are world-scoped.
155
-
156
- ### Element Endpoints
157
-
158
- All other element types return paginated results:
159
-
160
- ```typescript
161
- // List with pagination
162
- const response = await client.characters.list({
163
- limit: 10,
164
- offset: 0,
165
- ordering: '-created_at',
166
- search: 'dragon'
167
- });
168
-
169
- console.log(response.count); // Total count
170
- console.log(response.results); // Array of Characters
171
- console.log(response.next); // URL for next page
172
- console.log(response.previous); // URL for previous page
173
- ```
174
-
175
- ## Type-Safe Relationships
176
-
177
- ```typescript
178
- import { ElementId, Character, Location } from '@onlyworlds/sdk';
179
-
180
- // Branded types ensure you can't mix up element IDs
181
- const locationId: ElementId<'Location'> = 'some-location-id';
182
- const character: Character = {
183
- name: 'Aragorn',
184
- location: locationId // Type-safe!
185
- };
186
- ```
187
-
188
- ## Token Management
189
-
190
- OnlyWorlds provides a token rating system for tracking API usage and enabling AI-powered features. Users get a daily token allowance (default: 10,000 tokens) that resets every day.
191
-
192
- ### Working Reference Implementation
193
-
194
- See [base-tool/src/llm/token-service.ts](https://github.com/OnlyWorlds/base-tool) for the complete working implementation that this SDK enables.
195
-
196
- ### Check Token Status
197
-
198
- ```typescript
199
- // Get current token status
200
- const status = await client.tokens.getStatus();
201
-
202
- console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
203
- console.log(`Used today: ${status.tokens_used_today}`);
204
- console.log(`Active sessions: ${status.sessions_active}`);
205
- console.log(`Last reset: ${status.last_reset}`);
206
- ```
207
-
208
- ### Consume Tokens
209
-
210
- Report token consumption when your tool uses AI features or other token-tracked services:
211
-
212
- ```typescript
213
- // Report token usage
214
- const result = await client.tokens.consume({
215
- amount: 500,
216
- service: 'my_worldbuilding_tool',
217
- metadata: {
218
- feature: 'character_generation',
219
- model: 'gpt-4',
220
- prompt_tokens: 300,
221
- completion_tokens: 200
222
- }
223
- });
224
-
225
- // Check if consumption succeeded
226
- if (result.error) {
227
- console.warn('Token warning:', result.error);
228
- }
229
-
230
- console.log(`Consumed ${result.tokens_consumed} tokens`);
231
- console.log(`${result.tokens_remaining} tokens remaining`);
232
- ```
233
-
234
- **Important**: The API allows consumption even when exceeding available tokens (tracks as debt), but returns a warning in the `error` field.
235
-
236
- ### Advanced: Encrypted API Key Access
237
-
238
- For tools that need direct OpenAI API access using OnlyWorlds tokens:
239
-
240
- ```typescript
241
- // Get encrypted OpenAI key (requires 100+ tokens)
242
- const access = await client.tokens.getAccessKey();
243
-
244
- console.log('Session ID:', access.session_id);
245
- console.log('Expires:', access.expires_at);
246
- console.log('Available tokens:', access.tokens_available);
247
-
248
- // Decrypt the key client-side (see base-tool for full implementation)
249
- // 1. Derive decryption key from world ID using SHA-256
250
- // 2. Use 'fernet' npm package to decrypt
251
- // 3. Use decrypted OpenAI key for direct API calls
252
- // 4. Report usage with session_id
253
-
254
- // See base-tool/src/llm/token-service.ts:99-235 for complete example
255
- ```
256
-
257
- **Full decryption implementation** (based on base-tool):
258
-
259
- ```typescript
260
- import { fernet } from 'fernet';
261
-
262
- // Derive decryption key from world ID
263
- async function deriveKey(worldId: string): Promise<string> {
264
- const salt = 'onlyworlds-token-api-2024-public-salt';
265
- const keyMaterial = `${worldId}:${salt}`;
266
- const encoder = new TextEncoder();
267
- const data = encoder.encode(keyMaterial);
268
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
269
- const hashArray = Array.from(new Uint8Array(hashBuffer));
270
- const base64 = btoa(String.fromCharCode(...hashArray));
271
- return base64.replace(/\+/g, '-').replace(/\//g, '_');
272
- }
273
-
274
- // Decrypt the API key
275
- async function decryptApiKey(encryptedKey: string, worldId: string): Promise<string> {
276
- const derivedKey = await deriveKey(worldId);
277
- const secret = new fernet.Secret(derivedKey);
278
- const token = new fernet.Token({
279
- secret: secret,
280
- token: encryptedKey,
281
- ttl: 0 // Don't enforce TTL client-side
282
- });
283
- return token.decode();
284
- }
285
-
286
- // Usage
287
- const world = await client.worlds.get();
288
- const access = await client.tokens.getAccessKey();
289
- const apiKey = await decryptApiKey(access.encrypted_key, world.id);
290
-
291
- // Use apiKey for OpenAI API calls, then report usage:
292
- await client.tokens.consume({
293
- amount: tokensUsed,
294
- sessionId: access.session_id,
295
- service: 'direct_openai',
296
- metadata: { model: 'gpt-4', /* ... */ }
297
- });
298
- ```
299
-
300
- ### Session Management
301
-
302
- ```typescript
303
- // Revoke a specific session
304
- await client.tokens.revokeSession(sessionId);
305
-
306
- // Revoke all sessions (emergency cleanup)
307
- const result = await client.tokens.revokeAllSessions();
308
- console.log(`Revoked ${result.sessions_revoked} sessions`);
309
- ```
310
-
311
- ### Encryption Info
312
-
313
- Get public encryption details (no auth required):
314
-
315
- ```typescript
316
- const info = await client.tokens.getEncryptionInfo();
317
- console.log('Algorithm:', info.algorithm);
318
- console.log('Key derivation:', info.key_derivation);
319
- console.log('Public salt:', info.salt);
320
- console.log(info.javascript_example);
321
- ```
322
-
323
- ## License
324
-
325
- MIT License - see [LICENSE](LICENSE) file for details
326
-
327
- ## Links
328
-
329
- - [OnlyWorlds Website](https://onlyworlds.com)
330
- - [Documentation](https://onlyworlds.github.io/)
331
- - [NPM Package](https://www.npmjs.com/package/@onlyworlds/sdk)
332
- - [Report Issues](https://github.com/OnlyWorlds/sdk/issues)
1
+ # OnlyWorlds TypeScript SDK
2
+
3
+ [![npm version](https://badge.fury.io/js/@onlyworlds%2Fsdk.svg)](https://www.npmjs.com/package/@onlyworlds/sdk)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ The canonical typed client for the [OnlyWorlds](https://onlyworlds.github.io) v2 API, plus the
7
+ canonical constants (element types, icons, colour families, field schema) — generated from the
8
+ same schema source the server runs on.
9
+
10
+ **4.x is v2-native and ESM-only (Node 18+).** If you need the legacy v1 API dialect
11
+ (`OnlyWorldsClient`) or CommonJS `require()`, stay on 3.x — it remains published and the v1 API
12
+ remains served. See [docs/migrating-3-to-4.md](docs/migrating-3-to-4.md).
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @onlyworlds/sdk
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { OwV2Client } from '@onlyworlds/sdk';
24
+
25
+ const client = new OwV2Client({ apiKey: 'ow_r_your_key' }); // read-only key: that's all you need
26
+ const page = await client.list('character'); // { data, has_more, next_cursor }
27
+ ```
28
+
29
+ Key kinds: `ow_w_` write / `ow_r_` read-only (no PIN — the "give this to your players" key) /
30
+ `ow_a_` account Bearer / 10-digit legacy. Writes on a PIN-protected world also pass `apiPin`.
31
+ Demo keys `0000000000`–`0000000009` read real sample worlds.
32
+
33
+ ## The full round-trip
34
+
35
+ ```typescript
36
+ const writer = new OwV2Client({ apiKey: 'ow_w_your_key', apiPin: '1234' });
37
+
38
+ // v2 dialect rules, worth internalizing once:
39
+ // - link fields use ONE bare name both directions (no _ids suffix — that's v1)
40
+ // - NEVER send a "world" field (identity comes from the key; the client strips it anyway)
41
+ const location = await writer.create('location', {
42
+ name: 'Dragon Peak',
43
+ description: 'A treacherous mountain peak where dragons nest',
44
+ }); // id minted client-side when omitted (retries stay idempotent)
45
+
46
+ const dragon = await writer.create('creature', {
47
+ name: 'Vorrath the Ember-Scaled',
48
+ location: location.id, // single link: UUID (or null)
49
+ });
50
+
51
+ const fetched = await writer.get('creature', dragon.id);
52
+ // fetched.location === location.id — reads the way it writes
53
+
54
+ // PATCH is destructive on sent fields; arrays replace wholesale
55
+ await writer.patch('location', location.id, { supertype: 'Mountain' });
56
+
57
+ // For relationships, use the atomic link merge — returns the full updated element
58
+ const fireBreath = await writer.create('ability', { name: 'Ember Breath' });
59
+ await writer.editLinks('creature', dragon.id, 'abilities', { add: [fireBreath.id], remove: [] });
60
+
61
+ // Walk every page of a type
62
+ for await (const character of writer.listAll('character')) { /* ... */ }
63
+ ```
64
+
65
+ ## Bulk writes — always check `errors`
66
+
67
+ `/bulk` **succeeds partially by default** (HTTP 200 with per-slot statuses). The one mistake to
68
+ never make: treating a 200 as "everything landed."
69
+
70
+ ```typescript
71
+ const res = await writer.bulk(
72
+ [
73
+ { type: 'character', element: { name: 'A' } },
74
+ { type: 'event', element: { name: 'B' } },
75
+ ],
76
+ { idempotencyKey: crypto.randomUUID() }, // mint FRESH per attempt — a failed batch is
77
+ ); // cached under its key; never reuse across retries
78
+ if (res.errors) {
79
+ for (const slot of res.items.filter((s) => s.status >= 400)) {
80
+ console.warn(slot.error?.code, slot.error?.message, slot.error?.doc_url);
81
+ }
82
+ }
83
+ // Pass { atomic: true } for all-or-nothing instead. res.wasReplay flags idempotent replays.
84
+ ```
85
+
86
+ ## Sync
87
+
88
+ ```typescript
89
+ let cursor; // opaque, never expires; persist it
90
+ for await (const change of client.changesAll(cursor)) {
91
+ // ops arrive in (change_seq, id) order — apply in order → convergence
92
+ }
93
+ // GOTCHA: world-meta edits (name, calendar, public_read) do NOT enter /changes.
94
+ // Poll client.getWorld() and compare updated_at separately.
95
+ ```
96
+
97
+ ## Errors
98
+
99
+ Every non-2xx throws `OwApiError` carrying the platform envelope: `.status`, `.type`, `.code`,
100
+ `.param` (the exact field that failed), and `.docUrl` — surface `docUrl` in your UX. Transport
101
+ failures throw `OwNetworkError`. `err.isValidationError` is the common branch.
102
+
103
+ ## Canonical constants (all generated or test-gated from schema)
104
+
105
+ ```typescript
106
+ import {
107
+ ELEMENT_TYPES, // the 22 slugs (and the ElementType union)
108
+ ELEMENT_ICONS, // Material Symbols icon per type
109
+ ELEMENT_LABELS, // plural display labels
110
+ ELEMENT_SECTIONS, // canonical field grouping + display order
111
+ FIELD_SCHEMA, // per-field type/target metadata
112
+ elementColor, // canonical colour: family carries COLOUR, icon carries TYPE
113
+ } from '@onlyworlds/sdk';
114
+
115
+ elementColor('character', 'dark'); // '#3987e5' — four CVD-validated families
116
+ // Always pair colour with icon + label; colour alone is not accessible.
117
+ ```
118
+
119
+ The full schema with per-field meaning lives in [SCHEMA.md](SCHEMA.md) (generated, ships in this
120
+ package). AI agents: read [AGENTS.md](AGENTS.md) first.
121
+
122
+ ## Token rating system
123
+
124
+ ```typescript
125
+ import { TokenResource } from '@onlyworlds/sdk';
126
+ const tokens = new TokenResource(writer); // OwV2Client.request() satisfies TokenTransport
127
+ const status = await tokens.getStatus();
128
+ ```
129
+
130
+ ## AI access — when to use which
131
+
132
+ For **known, deterministic operations** (CRUD, sync, bulk) use this SDK — typed calls, no
133
+ tool-schema overhead. For **live exploration of a user's world from a chat/agent context**, use
134
+ the MCP server at `https://www.onlyworlds.com/mcp` (same `API-Key`/`API-Pin` headers, 11 tools;
135
+ unaffected by SDK versioning).
136
+
137
+ ## License
138
+
139
+ MIT
140
+
141
+ ## Links
142
+
143
+ - [OnlyWorlds](https://www.onlyworlds.com) · [Docs](https://onlyworlds.github.io) · [API reference](https://www.onlyworlds.com/api/docs)
144
+ - [Issues](https://github.com/OnlyWorlds/sdk/issues) · [CHANGELOG](CHANGELOG.md) · [Migration 3→4](docs/migrating-3-to-4.md)