@onlyworlds/sdk 2.1.3 → 2.2.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/dist/index.mjs CHANGED
@@ -1,310 +1,3 @@
1
- // src/token-resource.ts
2
- var TokenResource = class {
3
- constructor(client) {
4
- this.client = client;
5
- }
6
- /**
7
- * Get current token status for authenticated user
8
- *
9
- * Returns daily token allowance, usage, and availability.
10
- * Matches base-tool's checkStatus() pattern.
11
- *
12
- * @returns Current token status
13
- * @example
14
- * ```typescript
15
- * const status = await client.tokens.getStatus();
16
- * console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
17
- * console.log(`Used today: ${status.tokens_used_today}`);
18
- * console.log(`Active sessions: ${status.sessions_active}`);
19
- * ```
20
- */
21
- async getStatus() {
22
- return this.client.request("GET", "/tokens/status/");
23
- }
24
- /**
25
- * Consume tokens for service usage
26
- *
27
- * Reports token consumption to track daily usage. Allows consumption even if
28
- * exceeds available tokens (tracks as debt), but warns via error field.
29
- * Matches base-tool's reportUsage() pattern.
30
- *
31
- * @param params - Token consumption parameters
32
- * @returns Consumption result with updated balance
33
- * @example
34
- * ```typescript
35
- * const result = await client.tokens.consume({
36
- * amount: 500,
37
- * service: 'worldbuilding_tool',
38
- * metadata: {
39
- * feature: 'character_generation',
40
- * model: 'gpt-4',
41
- * prompt_tokens: 300,
42
- * completion_tokens: 200
43
- * }
44
- * });
45
- *
46
- * if (result.error) {
47
- * console.warn('Token warning:', result.error);
48
- * }
49
- * console.log(`${result.tokens_remaining} tokens remaining`);
50
- * ```
51
- */
52
- async consume(params) {
53
- return this.client.request("POST", "/tokens/consume/", {
54
- body: {
55
- amount: params.amount,
56
- service: params.service || "sdk_client",
57
- session_id: params.sessionId ?? null,
58
- metadata: params.metadata ?? null
59
- }
60
- });
61
- }
62
- /**
63
- * Get encrypted OpenAI API key (advanced use case)
64
- *
65
- * Requires minimum 100 tokens available. Creates a 1-hour session for tracking.
66
- * Returns encrypted key that must be decrypted client-side using Fernet.
67
- *
68
- * See base-tool/src/llm/token-service.ts:99-155 for full implementation example
69
- * including client-side decryption with the 'fernet' npm package.
70
- *
71
- * @returns Encrypted access key and session info
72
- * @throws Error if insufficient tokens (< 100)
73
- * @example
74
- * ```typescript
75
- * // Get encrypted key
76
- * const access = await client.tokens.getAccessKey();
77
- *
78
- * // Decrypt using fernet library (see base-tool for full example)
79
- * // 1. Derive key from world ID using SHA-256
80
- * // 2. Use 'fernet' npm package to decrypt
81
- * // 3. Use decrypted OpenAI key for direct API calls
82
- * // 4. Report usage with access.session_id
83
- *
84
- * console.log('Session:', access.session_id);
85
- * console.log('Expires:', access.expires_at);
86
- * ```
87
- */
88
- async getAccessKey() {
89
- return this.client.request("GET", "/tokens/access-key/");
90
- }
91
- /**
92
- * Revoke a specific token session
93
- *
94
- * Invalidates the session ID obtained from getAccessKey().
95
- * Use when cleaning up or on logout.
96
- *
97
- * @param sessionId - Session ID to revoke
98
- * @returns Revocation result
99
- * @example
100
- * ```typescript
101
- * await client.tokens.revokeSession('session-id-here');
102
- * ```
103
- */
104
- async revokeSession(sessionId) {
105
- return this.client.request(
106
- "POST",
107
- `/tokens/revoke-session/?session_id=${encodeURIComponent(sessionId)}`
108
- );
109
- }
110
- /**
111
- * Revoke all active sessions (emergency use)
112
- *
113
- * Invalidates all token sessions for the authenticated user.
114
- * Use for security cleanup or when sessions are stuck.
115
- *
116
- * @returns Revocation result with count of revoked sessions
117
- * @example
118
- * ```typescript
119
- * const result = await client.tokens.revokeAllSessions();
120
- * console.log(`Revoked ${result.sessions_revoked} sessions`);
121
- * ```
122
- */
123
- async revokeAllSessions() {
124
- return this.client.request("POST", "/tokens/revoke-all-sessions/");
125
- }
126
- /**
127
- * Get public encryption info (no auth required)
128
- *
129
- * Returns algorithm details and example code for client-side decryption.
130
- * Public endpoint - can be called without authentication.
131
- *
132
- * @returns Encryption algorithm and implementation details
133
- * @example
134
- * ```typescript
135
- * const info = await client.tokens.getEncryptionInfo();
136
- * console.log('Algorithm:', info.algorithm);
137
- * console.log('Key derivation:', info.key_derivation);
138
- * console.log(info.javascript_example);
139
- * ```
140
- */
141
- async getEncryptionInfo() {
142
- return this.client.request("GET", "/tokens/encryption-info/");
143
- }
144
- };
145
-
146
- // src/client.ts
147
- var Resource = class {
148
- constructor(client, elementType) {
149
- this.client = client;
150
- this.elementType = elementType;
151
- }
152
- async list(options) {
153
- const response = await this.client.request("GET", `/${this.elementType}/`, { params: options });
154
- if (Array.isArray(response)) {
155
- return {
156
- count: response.length,
157
- next: null,
158
- previous: null,
159
- results: response
160
- };
161
- }
162
- return response;
163
- }
164
- async get(id) {
165
- return this.client.request("GET", `/${this.elementType}/${id}/`);
166
- }
167
- async create(data) {
168
- const body = this.elementType === "pin" ? this.roundPinCoordinates(data) : data;
169
- return this.client.request("POST", `/${this.elementType}/`, { body });
170
- }
171
- async update(id, data) {
172
- const body = this.elementType === "pin" ? this.roundPinCoordinates(data) : data;
173
- return this.client.request("PATCH", `/${this.elementType}/${id}/`, { body });
174
- }
175
- async delete(id) {
176
- return this.client.request("DELETE", `/${this.elementType}/${id}/`);
177
- }
178
- /**
179
- * Round Pin coordinates to integers (API requirement)
180
- * @private
181
- */
182
- roundPinCoordinates(data) {
183
- const rounded = { ...data };
184
- if (typeof rounded.x === "number") rounded.x = Math.round(rounded.x);
185
- if (typeof rounded.y === "number") rounded.y = Math.round(rounded.y);
186
- if (typeof rounded.z === "number") rounded.z = Math.round(rounded.z);
187
- return rounded;
188
- }
189
- };
190
- var WorldResource = class {
191
- constructor(client) {
192
- this.client = client;
193
- }
194
- /**
195
- * Get the world associated with the current API key
196
- * Returns the world directly (not wrapped in pagination)
197
- */
198
- async get() {
199
- return this.client.request("GET", "/world/");
200
- }
201
- /**
202
- * Update the current world
203
- */
204
- async update(data) {
205
- return this.client.request("PATCH", "/world/", { body: data });
206
- }
207
- };
208
- var OnlyWorldsClient = class {
209
- constructor(config) {
210
- this.baseUrl = config.baseUrl || "https://www.onlyworlds.com/api/worldapi";
211
- this.headers = {
212
- "Content-Type": "application/json",
213
- "API-Key": config.apiKey,
214
- "API-Pin": config.apiPin
215
- };
216
- this.worlds = new WorldResource(this);
217
- this.tokens = new TokenResource(this);
218
- this.abilities = new Resource(this, "ability");
219
- this.characters = new Resource(this, "character");
220
- this.collectives = new Resource(this, "collective");
221
- this.constructs = new Resource(this, "construct");
222
- this.creatures = new Resource(this, "creature");
223
- this.events = new Resource(this, "event");
224
- this.families = new Resource(this, "family");
225
- this.institutions = new Resource(this, "institution");
226
- this.languages = new Resource(this, "language");
227
- this.laws = new Resource(this, "law");
228
- this.locations = new Resource(this, "location");
229
- this.maps = new Resource(this, "map");
230
- this.markers = new Resource(this, "marker");
231
- this.narratives = new Resource(this, "narrative");
232
- this.objects = new Resource(this, "object");
233
- this.phenomena = new Resource(this, "phenomenon");
234
- this.pins = new Resource(this, "pin");
235
- this.relations = new Resource(this, "relation");
236
- this.species = new Resource(this, "species");
237
- this.titles = new Resource(this, "title");
238
- this.traits = new Resource(this, "trait");
239
- this.zones = new Resource(this, "zone");
240
- }
241
- /**
242
- * Make a request to the OnlyWorlds API
243
- */
244
- async request(method, path, options) {
245
- const url = new URL(`${this.baseUrl}${path}`);
246
- if (options?.params) {
247
- Object.entries(options.params).forEach(([key, value]) => {
248
- if (value !== void 0 && value !== null) {
249
- url.searchParams.append(key, String(value));
250
- }
251
- });
252
- }
253
- const fetchOptions = {
254
- method,
255
- headers: this.headers
256
- };
257
- if (options?.body && ["POST", "PATCH", "PUT"].includes(method)) {
258
- fetchOptions.body = JSON.stringify(options.body);
259
- }
260
- const response = await fetch(url.toString(), fetchOptions);
261
- if (!response.ok) {
262
- let errorMessage = `API Error ${response.status}`;
263
- try {
264
- const errorText = await response.text();
265
- if (errorText) {
266
- try {
267
- const errorJson = JSON.parse(errorText);
268
- if (Array.isArray(errorJson.detail)) {
269
- const validationErrors = errorJson.detail.map((err) => {
270
- const location = err.loc ? err.loc.join(".") : "unknown";
271
- return `${location}: ${err.msg}`;
272
- }).join("; ");
273
- errorMessage += `: ${validationErrors}`;
274
- } else {
275
- errorMessage += `: ${errorJson.detail || errorJson.error || errorText}`;
276
- }
277
- } catch {
278
- errorMessage += `: ${errorText}`;
279
- }
280
- }
281
- } catch {
282
- }
283
- throw new Error(errorMessage);
284
- }
285
- if (response.status === 204) {
286
- return void 0;
287
- }
288
- return response.json();
289
- }
290
- /**
291
- * Helper to convert nested objects to _id/_ids format
292
- */
293
- static prepareInput(data) {
294
- const result = { ...data };
295
- for (const [key, value] of Object.entries(result)) {
296
- if (value && typeof value === "object" && "id" in value) {
297
- delete result[key];
298
- result[`${key}_id`] = value.id;
299
- } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && "id" in value[0]) {
300
- delete result[key];
301
- result[`${key}_ids`] = value.map((item) => item.id);
302
- }
303
- }
304
- return result;
305
- }
306
- };
307
-
308
1
  // src/types.ts
309
2
  var ElementType = /* @__PURE__ */ ((ElementType2) => {
310
3
  ElementType2["Ability"] = "ability";
@@ -489,36 +182,6 @@ var ELEMENT_ICONS = {
489
182
  ["trait" /* Trait */]: "flaky",
490
183
  ["zone" /* Zone */]: "architecture"
491
184
  };
492
- var ELEMENT_UNICODE_ICONS = {
493
- ["ability" /* Ability */]: "\u2728",
494
- ["character" /* Character */]: "\u{1F464}",
495
- ["collective" /* Collective */]: "\u{1F465}",
496
- ["construct" /* Construct */]: "\u2699\uFE0F",
497
- ["creature" /* Creature */]: "\u{1F43E}",
498
- ["event" /* Event */]: "\u{1F4C5}",
499
- ["family" /* Family */]: "\u{1F468}\u200D\u{1F469}\u200D\u{1F467}\u200D\u{1F466}",
500
- ["institution" /* Institution */]: "\u{1F3DB}\uFE0F",
501
- ["language" /* Language */]: "\u{1F4AC}",
502
- ["law" /* Law */]: "\u2696\uFE0F",
503
- ["location" /* Location */]: "\u{1F3F0}",
504
- ["map" /* Map */]: "\u{1F5FA}\uFE0F",
505
- ["marker" /* Marker */]: "\u{1F4CD}",
506
- ["narrative" /* Narrative */]: "\u{1F4D6}",
507
- ["object" /* Object */]: "\u{1F4E6}",
508
- ["phenomenon" /* Phenomenon */]: "\u26A1",
509
- ["pin" /* Pin */]: "\u{1F4CC}",
510
- ["relation" /* Relation */]: "\u{1F517}",
511
- ["species" /* Species */]: "\u{1F9EC}",
512
- ["title" /* Title */]: "\u{1F451}",
513
- ["trait" /* Trait */]: "\u2744\uFE0F",
514
- ["zone" /* Zone */]: "\u{1F5FA}\uFE0F"
515
- };
516
- function getElementIcon(elementType) {
517
- return ELEMENT_ICONS[elementType];
518
- }
519
- function getElementUnicodeIcon(elementType) {
520
- return ELEMENT_UNICODE_ICONS[elementType];
521
- }
522
185
  var FIELD_SCHEMA = {
523
186
  ability: {
524
187
  // Base fields (shared by all elements)
@@ -1113,15 +776,368 @@ var FIELD_SCHEMA = {
1113
776
  principles: { type: "multi_link", target: "construct" }
1114
777
  }
1115
778
  };
1116
- function createElementId(id) {
1117
- return id;
1118
- }
1119
- function createElementIds(ids) {
1120
- return ids;
1121
- }
1122
- function createAnyElementId(id) {
1123
- return id;
1124
- }
779
+ function createElementId(id) {
780
+ return id;
781
+ }
782
+ function createElementIds(ids) {
783
+ return ids;
784
+ }
785
+ function createAnyElementId(id) {
786
+ return id;
787
+ }
788
+
789
+ // src/token-resource.ts
790
+ var TokenResource = class {
791
+ constructor(client) {
792
+ this.client = client;
793
+ }
794
+ /**
795
+ * Get current token status for authenticated user
796
+ *
797
+ * Returns daily token allowance, usage, and availability.
798
+ * Matches base-tool's checkStatus() pattern.
799
+ *
800
+ * @returns Current token status
801
+ * @example
802
+ * ```typescript
803
+ * const status = await client.tokens.getStatus();
804
+ * console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
805
+ * console.log(`Used today: ${status.tokens_used_today}`);
806
+ * console.log(`Active sessions: ${status.sessions_active}`);
807
+ * ```
808
+ */
809
+ async getStatus() {
810
+ return this.client.request("GET", "/tokens/status/");
811
+ }
812
+ /**
813
+ * Consume tokens for service usage
814
+ *
815
+ * Reports token consumption to track daily usage. Allows consumption even if
816
+ * exceeds available tokens (tracks as debt), but warns via error field.
817
+ * Matches base-tool's reportUsage() pattern.
818
+ *
819
+ * @param params - Token consumption parameters
820
+ * @returns Consumption result with updated balance
821
+ * @example
822
+ * ```typescript
823
+ * const result = await client.tokens.consume({
824
+ * amount: 500,
825
+ * service: 'worldbuilding_tool',
826
+ * metadata: {
827
+ * feature: 'character_generation',
828
+ * model: 'gpt-4',
829
+ * prompt_tokens: 300,
830
+ * completion_tokens: 200
831
+ * }
832
+ * });
833
+ *
834
+ * if (result.error) {
835
+ * console.warn('Token warning:', result.error);
836
+ * }
837
+ * console.log(`${result.tokens_remaining} tokens remaining`);
838
+ * ```
839
+ */
840
+ async consume(params) {
841
+ return this.client.request("POST", "/tokens/consume/", {
842
+ body: {
843
+ amount: params.amount,
844
+ service: params.service || "sdk_client",
845
+ session_id: params.sessionId ?? null,
846
+ metadata: params.metadata ?? null
847
+ }
848
+ });
849
+ }
850
+ /**
851
+ * Get encrypted OpenAI API key (advanced use case)
852
+ *
853
+ * Requires minimum 100 tokens available. Creates a 1-hour session for tracking.
854
+ * Returns encrypted key that must be decrypted client-side using Fernet.
855
+ *
856
+ * See base-tool/src/llm/token-service.ts:99-155 for full implementation example
857
+ * including client-side decryption with the 'fernet' npm package.
858
+ *
859
+ * @returns Encrypted access key and session info
860
+ * @throws Error if insufficient tokens (< 100)
861
+ * @example
862
+ * ```typescript
863
+ * // Get encrypted key
864
+ * const access = await client.tokens.getAccessKey();
865
+ *
866
+ * // Decrypt using fernet library (see base-tool for full example)
867
+ * // 1. Derive key from world ID using SHA-256
868
+ * // 2. Use 'fernet' npm package to decrypt
869
+ * // 3. Use decrypted OpenAI key for direct API calls
870
+ * // 4. Report usage with access.session_id
871
+ *
872
+ * console.log('Session:', access.session_id);
873
+ * console.log('Expires:', access.expires_at);
874
+ * ```
875
+ */
876
+ async getAccessKey() {
877
+ return this.client.request("GET", "/tokens/access-key/");
878
+ }
879
+ /**
880
+ * Revoke a specific token session
881
+ *
882
+ * Invalidates the session ID obtained from getAccessKey().
883
+ * Use when cleaning up or on logout.
884
+ *
885
+ * @param sessionId - Session ID to revoke
886
+ * @returns Revocation result
887
+ * @example
888
+ * ```typescript
889
+ * await client.tokens.revokeSession('session-id-here');
890
+ * ```
891
+ */
892
+ async revokeSession(sessionId) {
893
+ return this.client.request(
894
+ "POST",
895
+ `/tokens/revoke-session/?session_id=${encodeURIComponent(sessionId)}`
896
+ );
897
+ }
898
+ /**
899
+ * Revoke all active sessions (emergency use)
900
+ *
901
+ * Invalidates all token sessions for the authenticated user.
902
+ * Use for security cleanup or when sessions are stuck.
903
+ *
904
+ * @returns Revocation result with count of revoked sessions
905
+ * @example
906
+ * ```typescript
907
+ * const result = await client.tokens.revokeAllSessions();
908
+ * console.log(`Revoked ${result.sessions_revoked} sessions`);
909
+ * ```
910
+ */
911
+ async revokeAllSessions() {
912
+ return this.client.request("POST", "/tokens/revoke-all-sessions/");
913
+ }
914
+ /**
915
+ * Get public encryption info (no auth required)
916
+ *
917
+ * Returns algorithm details and example code for client-side decryption.
918
+ * Public endpoint - can be called without authentication.
919
+ *
920
+ * @returns Encryption algorithm and implementation details
921
+ * @example
922
+ * ```typescript
923
+ * const info = await client.tokens.getEncryptionInfo();
924
+ * console.log('Algorithm:', info.algorithm);
925
+ * console.log('Key derivation:', info.key_derivation);
926
+ * console.log(info.javascript_example);
927
+ * ```
928
+ */
929
+ async getEncryptionInfo() {
930
+ return this.client.request("GET", "/tokens/encryption-info/");
931
+ }
932
+ };
933
+
934
+ // src/client.ts
935
+ var Resource = class {
936
+ constructor(client, elementType) {
937
+ this.client = client;
938
+ this.elementType = elementType;
939
+ }
940
+ async list(options) {
941
+ const response = await this.client.request("GET", `/${this.elementType}/`, { params: options });
942
+ if (Array.isArray(response)) {
943
+ return {
944
+ count: response.length,
945
+ next: null,
946
+ previous: null,
947
+ results: response
948
+ };
949
+ }
950
+ return response;
951
+ }
952
+ async get(id) {
953
+ return this.client.request("GET", `/${this.elementType}/${id}/`);
954
+ }
955
+ async create(data) {
956
+ let body = OnlyWorldsClient.prepareRelations(data, this.elementType);
957
+ if (this.elementType === "pin") body = this.roundPinCoordinates(body);
958
+ return this.client.request("POST", `/${this.elementType}/`, { body });
959
+ }
960
+ async update(id, data) {
961
+ let body = OnlyWorldsClient.prepareRelations(data, this.elementType);
962
+ if (this.elementType === "pin") body = this.roundPinCoordinates(body);
963
+ return this.client.request("PATCH", `/${this.elementType}/${id}/`, { body });
964
+ }
965
+ async delete(id) {
966
+ return this.client.request("DELETE", `/${this.elementType}/${id}/`);
967
+ }
968
+ /**
969
+ * Round Pin coordinates to integers (API requirement)
970
+ * @private
971
+ */
972
+ roundPinCoordinates(data) {
973
+ const rounded = { ...data };
974
+ if (typeof rounded.x === "number") rounded.x = Math.round(rounded.x);
975
+ if (typeof rounded.y === "number") rounded.y = Math.round(rounded.y);
976
+ if (typeof rounded.z === "number") rounded.z = Math.round(rounded.z);
977
+ return rounded;
978
+ }
979
+ };
980
+ var WorldResource = class {
981
+ constructor(client) {
982
+ this.client = client;
983
+ }
984
+ /**
985
+ * Get the world associated with the current API key
986
+ * Returns the world directly (not wrapped in pagination)
987
+ */
988
+ async get() {
989
+ return this.client.request("GET", "/world/");
990
+ }
991
+ /**
992
+ * Update the current world
993
+ */
994
+ async update(data) {
995
+ return this.client.request("PATCH", "/world/", { body: data });
996
+ }
997
+ };
998
+ var OnlyWorldsClient = class {
999
+ constructor(config) {
1000
+ this.baseUrl = config.baseUrl || "https://www.onlyworlds.com/api/worldapi";
1001
+ this.headers = {
1002
+ "Content-Type": "application/json",
1003
+ "API-Key": config.apiKey,
1004
+ "API-Pin": config.apiPin
1005
+ };
1006
+ this.worlds = new WorldResource(this);
1007
+ this.tokens = new TokenResource(this);
1008
+ this.abilities = new Resource(this, "ability");
1009
+ this.characters = new Resource(this, "character");
1010
+ this.collectives = new Resource(this, "collective");
1011
+ this.constructs = new Resource(this, "construct");
1012
+ this.creatures = new Resource(this, "creature");
1013
+ this.events = new Resource(this, "event");
1014
+ this.families = new Resource(this, "family");
1015
+ this.institutions = new Resource(this, "institution");
1016
+ this.languages = new Resource(this, "language");
1017
+ this.laws = new Resource(this, "law");
1018
+ this.locations = new Resource(this, "location");
1019
+ this.maps = new Resource(this, "map");
1020
+ this.markers = new Resource(this, "marker");
1021
+ this.narratives = new Resource(this, "narrative");
1022
+ this.objects = new Resource(this, "object");
1023
+ this.phenomena = new Resource(this, "phenomenon");
1024
+ this.pins = new Resource(this, "pin");
1025
+ this.relations = new Resource(this, "relation");
1026
+ this.species = new Resource(this, "species");
1027
+ this.titles = new Resource(this, "title");
1028
+ this.traits = new Resource(this, "trait");
1029
+ this.zones = new Resource(this, "zone");
1030
+ }
1031
+ /**
1032
+ * Make a request to the OnlyWorlds API
1033
+ */
1034
+ async request(method, path, options) {
1035
+ const url = new URL(`${this.baseUrl}${path}`);
1036
+ if (options?.params) {
1037
+ Object.entries(options.params).forEach(([key, value]) => {
1038
+ if (value !== void 0 && value !== null) {
1039
+ url.searchParams.append(key, String(value));
1040
+ }
1041
+ });
1042
+ }
1043
+ const fetchOptions = {
1044
+ method,
1045
+ headers: this.headers
1046
+ };
1047
+ if (options?.body && ["POST", "PATCH", "PUT"].includes(method)) {
1048
+ fetchOptions.body = JSON.stringify(options.body);
1049
+ }
1050
+ const response = await fetch(url.toString(), fetchOptions);
1051
+ if (!response.ok) {
1052
+ let errorMessage = `API Error ${response.status}`;
1053
+ try {
1054
+ const errorText = await response.text();
1055
+ if (errorText) {
1056
+ try {
1057
+ const errorJson = JSON.parse(errorText);
1058
+ if (Array.isArray(errorJson.detail)) {
1059
+ const validationErrors = errorJson.detail.map((err) => {
1060
+ const location = err.loc ? err.loc.join(".") : "unknown";
1061
+ return `${location}: ${err.msg}`;
1062
+ }).join("; ");
1063
+ errorMessage += `: ${validationErrors}`;
1064
+ } else {
1065
+ errorMessage += `: ${errorJson.detail || errorJson.error || errorText}`;
1066
+ }
1067
+ } catch {
1068
+ errorMessage += `: ${errorText}`;
1069
+ }
1070
+ }
1071
+ } catch {
1072
+ }
1073
+ throw new Error(errorMessage);
1074
+ }
1075
+ if (response.status === 204) {
1076
+ return void 0;
1077
+ }
1078
+ return response.json();
1079
+ }
1080
+ /**
1081
+ * Helper to convert nested objects to _id/_ids format (legacy method)
1082
+ * @deprecated Use prepareRelations instead - it's called automatically in create/update
1083
+ */
1084
+ static prepareInput(data) {
1085
+ const result = { ...data };
1086
+ for (const [key, value] of Object.entries(result)) {
1087
+ if (value && typeof value === "object" && !Array.isArray(value) && "id" in value) {
1088
+ delete result[key];
1089
+ result[`${key}_id`] = value.id;
1090
+ } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && "id" in value[0]) {
1091
+ delete result[key];
1092
+ result[`${key}_ids`] = value.map((item) => item.id);
1093
+ }
1094
+ }
1095
+ return result;
1096
+ }
1097
+ /**
1098
+ * Convert relation fields to API format (_id/_ids suffix)
1099
+ *
1100
+ * The OnlyWorlds API expects:
1101
+ * - single_link fields: fieldname_id (e.g., birthplace_id)
1102
+ * - multi_link fields: fieldname_ids (e.g., species_ids)
1103
+ *
1104
+ * This method auto-converts based on FIELD_SCHEMA:
1105
+ * - { species: ["id1", "id2"] } → { species_ids: ["id1", "id2"] }
1106
+ * - { birthplace: "location-id" } → { birthplace_id: "location-id" }
1107
+ * - { species: [{id: "id1", name: "X"}] } → { species_ids: ["id1"] }
1108
+ *
1109
+ * Called automatically by create() and update() methods.
1110
+ */
1111
+ static prepareRelations(data, elementType) {
1112
+ const schema = FIELD_SCHEMA[elementType];
1113
+ if (!schema) return data;
1114
+ const result = { ...data };
1115
+ for (const [key, value] of Object.entries(result)) {
1116
+ const fieldDef = schema[key];
1117
+ if (!fieldDef) continue;
1118
+ if (fieldDef.type === "single_link") {
1119
+ delete result[key];
1120
+ if (value && typeof value === "object" && "id" in value) {
1121
+ result[`${key}_id`] = value.id;
1122
+ } else if (typeof value === "string" || value === null) {
1123
+ result[`${key}_id`] = value;
1124
+ }
1125
+ } else if (fieldDef.type === "multi_link") {
1126
+ delete result[key];
1127
+ if (Array.isArray(value)) {
1128
+ if (value.length > 0 && typeof value[0] === "object" && "id" in value[0]) {
1129
+ result[`${key}_ids`] = value.map((item) => item.id);
1130
+ } else {
1131
+ result[`${key}_ids`] = value;
1132
+ }
1133
+ } else {
1134
+ result[`${key}_ids`] = [];
1135
+ }
1136
+ }
1137
+ }
1138
+ return result;
1139
+ }
1140
+ };
1125
1141
 
1126
1142
  // src/token-types.ts
1127
1143
  var GameTier = /* @__PURE__ */ ((GameTier2) => {
@@ -1133,11 +1149,51 @@ var GameTier = /* @__PURE__ */ ((GameTier2) => {
1133
1149
  GameTier2["DELUXE"] = "deluxe";
1134
1150
  return GameTier2;
1135
1151
  })(GameTier || {});
1152
+
1153
+ // src/icon-utils.ts
1154
+ var PLURAL_TO_SINGULAR = {
1155
+ abilities: "ability" /* Ability */,
1156
+ characters: "character" /* Character */,
1157
+ collectives: "collective" /* Collective */,
1158
+ constructs: "construct" /* Construct */,
1159
+ creatures: "creature" /* Creature */,
1160
+ events: "event" /* Event */,
1161
+ families: "family" /* Family */,
1162
+ institutions: "institution" /* Institution */,
1163
+ languages: "language" /* Language */,
1164
+ laws: "law" /* Law */,
1165
+ locations: "location" /* Location */,
1166
+ maps: "map" /* Map */,
1167
+ markers: "marker" /* Marker */,
1168
+ narratives: "narrative" /* Narrative */,
1169
+ objects: "object" /* Object */,
1170
+ phenomena: "phenomenon" /* Phenomenon */,
1171
+ // irregular plural
1172
+ pins: "pin" /* Pin */,
1173
+ relations: "relation" /* Relation */,
1174
+ species: "species" /* Species */,
1175
+ // same singular/plural
1176
+ titles: "title" /* Title */,
1177
+ traits: "trait" /* Trait */,
1178
+ zones: "zone" /* Zone */
1179
+ };
1180
+ function getElementIcon(type) {
1181
+ const lower = type.toLowerCase();
1182
+ if (lower in PLURAL_TO_SINGULAR) {
1183
+ return ELEMENT_ICONS[PLURAL_TO_SINGULAR[lower]];
1184
+ }
1185
+ const singular = Object.values(ElementType).find(
1186
+ (et) => et.toLowerCase() === lower
1187
+ );
1188
+ if (singular) {
1189
+ return ELEMENT_ICONS[singular];
1190
+ }
1191
+ return "help_outline";
1192
+ }
1136
1193
  export {
1137
1194
  ELEMENT_ICONS,
1138
1195
  ELEMENT_LABELS,
1139
1196
  ELEMENT_SECTIONS,
1140
- ELEMENT_UNICODE_ICONS,
1141
1197
  ElementType,
1142
1198
  FIELD_SCHEMA,
1143
1199
  GameTier,
@@ -1148,6 +1204,5 @@ export {
1148
1204
  createElementIds,
1149
1205
  getElementIcon,
1150
1206
  getElementLabel,
1151
- getElementSections,
1152
- getElementUnicodeIcon
1207
+ getElementSections
1153
1208
  };