@character-foundry/character-foundry 0.1.3 → 0.1.5

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.
Files changed (100) hide show
  1. package/README.md +70 -0
  2. package/dist/app-framework.cjs +1742 -0
  3. package/dist/app-framework.cjs.map +1 -0
  4. package/dist/app-framework.d.cts +881 -0
  5. package/dist/app-framework.d.ts +881 -2
  6. package/dist/app-framework.js +1718 -1
  7. package/dist/app-framework.js.map +1 -1
  8. package/dist/charx.cjs +917 -0
  9. package/dist/charx.cjs.map +1 -0
  10. package/dist/charx.d.cts +640 -0
  11. package/dist/charx.d.ts +640 -2
  12. package/dist/charx.js +893 -1
  13. package/dist/charx.js.map +1 -1
  14. package/dist/core.cjs +668 -0
  15. package/dist/core.cjs.map +1 -0
  16. package/dist/core.d.cts +363 -0
  17. package/dist/core.d.ts +363 -2
  18. package/dist/core.js +644 -1
  19. package/dist/core.js.map +1 -1
  20. package/dist/exporter.cjs +7539 -0
  21. package/dist/exporter.cjs.map +1 -0
  22. package/dist/exporter.d.cts +681 -0
  23. package/dist/exporter.d.ts +681 -2
  24. package/dist/exporter.js +7522 -1
  25. package/dist/exporter.js.map +1 -1
  26. package/dist/federation.cjs +3915 -0
  27. package/dist/federation.cjs.map +1 -0
  28. package/dist/federation.d.cts +2951 -0
  29. package/dist/federation.d.ts +2951 -2
  30. package/dist/federation.js +3891 -1
  31. package/dist/federation.js.map +1 -1
  32. package/dist/index.cjs +9109 -0
  33. package/dist/index.cjs.map +1 -0
  34. package/dist/index.d.cts +1119 -0
  35. package/dist/index.d.ts +1113 -20
  36. package/dist/index.js +9092 -26
  37. package/dist/index.js.map +1 -1
  38. package/dist/loader.cjs +8923 -0
  39. package/dist/loader.cjs.map +1 -0
  40. package/dist/loader.d.cts +1037 -0
  41. package/dist/loader.d.ts +1037 -2
  42. package/dist/loader.js +8906 -1
  43. package/dist/loader.js.map +1 -1
  44. package/dist/lorebook.cjs +865 -0
  45. package/dist/lorebook.cjs.map +1 -0
  46. package/dist/lorebook.d.cts +1008 -0
  47. package/dist/lorebook.d.ts +1008 -2
  48. package/dist/lorebook.js +841 -1
  49. package/dist/lorebook.js.map +1 -1
  50. package/dist/media.cjs +6660 -0
  51. package/dist/media.cjs.map +1 -0
  52. package/dist/media.d.cts +87 -0
  53. package/dist/media.d.ts +87 -2
  54. package/dist/media.js +6643 -1
  55. package/dist/media.js.map +1 -1
  56. package/dist/normalizer.cjs +502 -0
  57. package/dist/normalizer.cjs.map +1 -0
  58. package/dist/normalizer.d.cts +1216 -0
  59. package/dist/normalizer.d.ts +1216 -2
  60. package/dist/normalizer.js +478 -1
  61. package/dist/normalizer.js.map +1 -1
  62. package/dist/png.cjs +778 -0
  63. package/dist/png.cjs.map +1 -0
  64. package/dist/png.d.cts +786 -0
  65. package/dist/png.d.ts +786 -2
  66. package/dist/png.js +754 -1
  67. package/dist/png.js.map +1 -1
  68. package/dist/schemas.cjs +799 -0
  69. package/dist/schemas.cjs.map +1 -0
  70. package/dist/schemas.d.cts +2178 -0
  71. package/dist/schemas.d.ts +2178 -2
  72. package/dist/schemas.js +775 -1
  73. package/dist/schemas.js.map +1 -1
  74. package/dist/tokenizers.cjs +153 -0
  75. package/dist/tokenizers.cjs.map +1 -0
  76. package/dist/tokenizers.d.cts +155 -0
  77. package/dist/tokenizers.d.ts +155 -2
  78. package/dist/tokenizers.js +129 -1
  79. package/dist/tokenizers.js.map +1 -1
  80. package/dist/voxta.cjs +7995 -0
  81. package/dist/voxta.cjs.map +1 -0
  82. package/dist/voxta.d.cts +1349 -0
  83. package/dist/voxta.d.ts +1349 -2
  84. package/dist/voxta.js +7978 -1
  85. package/dist/voxta.js.map +1 -1
  86. package/package.json +177 -45
  87. package/dist/app-framework.d.ts.map +0 -1
  88. package/dist/charx.d.ts.map +0 -1
  89. package/dist/core.d.ts.map +0 -1
  90. package/dist/exporter.d.ts.map +0 -1
  91. package/dist/federation.d.ts.map +0 -1
  92. package/dist/index.d.ts.map +0 -1
  93. package/dist/loader.d.ts.map +0 -1
  94. package/dist/lorebook.d.ts.map +0 -1
  95. package/dist/media.d.ts.map +0 -1
  96. package/dist/normalizer.d.ts.map +0 -1
  97. package/dist/png.d.ts.map +0 -1
  98. package/dist/schemas.d.ts.map +0 -1
  99. package/dist/tokenizers.d.ts.map +0 -1
  100. package/dist/voxta.d.ts.map +0 -1
package/dist/charx.js CHANGED
@@ -1,2 +1,894 @@
1
- export * from '@character-foundry/charx';
1
+ // ../core/dist/index.js
2
+ function fromString(str) {
3
+ return new TextEncoder().encode(str);
4
+ }
5
+ function toString(data) {
6
+ return new TextDecoder().decode(data);
7
+ }
8
+ var isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
9
+ function decode(base64) {
10
+ if (isNode) {
11
+ return new Uint8Array(Buffer.from(base64, "base64"));
12
+ }
13
+ const binary = atob(base64);
14
+ const result = new Uint8Array(binary.length);
15
+ for (let i = 0; i < binary.length; i++) {
16
+ result[i] = binary.charCodeAt(i);
17
+ }
18
+ return result;
19
+ }
20
+ var ENCODE_CHUNK_SIZE = 64 * 1024;
21
+ var FOUNDRY_ERROR_MARKER = /* @__PURE__ */ Symbol.for("@character-foundry/core:FoundryError");
22
+ var FoundryError = class extends Error {
23
+ constructor(message, code) {
24
+ super(message);
25
+ this.code = code;
26
+ this.name = "FoundryError";
27
+ if (Error.captureStackTrace) {
28
+ Error.captureStackTrace(this, this.constructor);
29
+ }
30
+ }
31
+ /** @internal Marker for cross-module identification */
32
+ [FOUNDRY_ERROR_MARKER] = true;
33
+ };
34
+ var ParseError = class extends FoundryError {
35
+ constructor(message, format) {
36
+ super(message, "PARSE_ERROR");
37
+ this.format = format;
38
+ this.name = "ParseError";
39
+ }
40
+ };
41
+ var SizeLimitError = class extends FoundryError {
42
+ constructor(actualSize, maxSize, context) {
43
+ const actualMB = (actualSize / 1024 / 1024).toFixed(2);
44
+ const maxMB = (maxSize / 1024 / 1024).toFixed(2);
45
+ const msg = context ? `${context}: Size ${actualMB}MB exceeds limit ${maxMB}MB` : `Size ${actualMB}MB exceeds limit ${maxMB}MB`;
46
+ super(msg, "SIZE_LIMIT_EXCEEDED");
47
+ this.actualSize = actualSize;
48
+ this.maxSize = maxSize;
49
+ this.name = "SizeLimitError";
50
+ }
51
+ };
52
+ function normalizeURI(uri) {
53
+ const trimmed = uri.trim();
54
+ if (trimmed.startsWith("embedded://")) {
55
+ return "embeded://" + trimmed.substring("embedded://".length);
56
+ }
57
+ if (trimmed.startsWith("__asset:")) {
58
+ const id = trimmed.substring("__asset:".length);
59
+ return `pngchunk:${id}`;
60
+ }
61
+ if (trimmed.startsWith("asset:")) {
62
+ const id = trimmed.substring("asset:".length);
63
+ return `pngchunk:${id}`;
64
+ }
65
+ if (trimmed.startsWith("chara-ext-asset_:")) {
66
+ const id = trimmed.substring("chara-ext-asset_:".length);
67
+ return `pngchunk:${id}`;
68
+ }
69
+ if (trimmed.startsWith("chara-ext-asset_")) {
70
+ const id = trimmed.substring("chara-ext-asset_".length);
71
+ return `pngchunk:${id}`;
72
+ }
73
+ return trimmed;
74
+ }
75
+ function parseURI(uri) {
76
+ const trimmed = uri.trim();
77
+ const normalized = normalizeURI(trimmed);
78
+ if (trimmed.startsWith("__asset:") || trimmed.startsWith("asset:") || trimmed.startsWith("chara-ext-asset_") || trimmed.startsWith("pngchunk:")) {
79
+ let assetId;
80
+ if (trimmed.startsWith("__asset:")) {
81
+ assetId = trimmed.substring("__asset:".length);
82
+ } else if (trimmed.startsWith("asset:")) {
83
+ assetId = trimmed.substring("asset:".length);
84
+ } else if (trimmed.startsWith("chara-ext-asset_:")) {
85
+ assetId = trimmed.substring("chara-ext-asset_:".length);
86
+ } else if (trimmed.startsWith("pngchunk:")) {
87
+ assetId = trimmed.substring("pngchunk:".length);
88
+ } else {
89
+ assetId = trimmed.substring("chara-ext-asset_".length);
90
+ }
91
+ const candidates = [
92
+ assetId,
93
+ // "0" or "filename.png"
94
+ trimmed,
95
+ // Original URI
96
+ `asset:${assetId}`,
97
+ // "asset:0"
98
+ `__asset:${assetId}`,
99
+ // "__asset:0"
100
+ `__asset_${assetId}`,
101
+ // "__asset_0"
102
+ `chara-ext-asset_${assetId}`,
103
+ // "chara-ext-asset_0"
104
+ `chara-ext-asset_:${assetId}`,
105
+ // "chara-ext-asset_:0"
106
+ `pngchunk:${assetId}`
107
+ // "pngchunk:0"
108
+ ];
109
+ return {
110
+ scheme: "pngchunk",
111
+ originalUri: uri,
112
+ normalizedUri: normalized,
113
+ chunkKey: assetId,
114
+ chunkCandidates: candidates
115
+ };
116
+ }
117
+ if (trimmed === "ccdefault:" || trimmed.startsWith("ccdefault:")) {
118
+ return {
119
+ scheme: "ccdefault",
120
+ originalUri: uri,
121
+ normalizedUri: normalized
122
+ };
123
+ }
124
+ if (trimmed.startsWith("embeded://") || trimmed.startsWith("embedded://")) {
125
+ const path = trimmed.startsWith("embeded://") ? trimmed.substring("embeded://".length) : trimmed.substring("embedded://".length);
126
+ return {
127
+ scheme: "embeded",
128
+ originalUri: uri,
129
+ normalizedUri: normalized,
130
+ path
131
+ };
132
+ }
133
+ if (trimmed.startsWith("https://")) {
134
+ return {
135
+ scheme: "https",
136
+ originalUri: uri,
137
+ normalizedUri: normalized,
138
+ url: trimmed
139
+ };
140
+ }
141
+ if (trimmed.startsWith("http://")) {
142
+ return {
143
+ scheme: "http",
144
+ originalUri: uri,
145
+ normalizedUri: normalized,
146
+ url: trimmed
147
+ };
148
+ }
149
+ if (trimmed.startsWith("data:")) {
150
+ const parsed = parseDataURI(trimmed);
151
+ return {
152
+ scheme: "data",
153
+ originalUri: uri,
154
+ normalizedUri: normalized,
155
+ ...parsed
156
+ };
157
+ }
158
+ if (trimmed.startsWith("file://")) {
159
+ const path = trimmed.substring("file://".length);
160
+ return {
161
+ scheme: "file",
162
+ originalUri: uri,
163
+ normalizedUri: normalized,
164
+ path
165
+ };
166
+ }
167
+ if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
168
+ return {
169
+ scheme: "internal",
170
+ originalUri: uri,
171
+ normalizedUri: normalized,
172
+ path: trimmed
173
+ };
174
+ }
175
+ return {
176
+ scheme: "unknown",
177
+ originalUri: uri,
178
+ normalizedUri: normalized
179
+ };
180
+ }
181
+ function parseDataURI(uri) {
182
+ const match = uri.match(/^data:([^;,]+)?(;base64)?,(.*)$/);
183
+ if (!match) {
184
+ return {};
185
+ }
186
+ return {
187
+ mimeType: match[1] || "text/plain",
188
+ encoding: match[2] ? "base64" : void 0,
189
+ data: match[3]
190
+ };
191
+ }
192
+ function getMimeTypeFromExt(ext) {
193
+ const extToMime = {
194
+ // Images
195
+ "png": "image/png",
196
+ "jpg": "image/jpeg",
197
+ "jpeg": "image/jpeg",
198
+ "webp": "image/webp",
199
+ "gif": "image/gif",
200
+ "avif": "image/avif",
201
+ "svg": "image/svg+xml",
202
+ "bmp": "image/bmp",
203
+ "ico": "image/x-icon",
204
+ // Audio
205
+ "mp3": "audio/mpeg",
206
+ "wav": "audio/wav",
207
+ "ogg": "audio/ogg",
208
+ "flac": "audio/flac",
209
+ "m4a": "audio/mp4",
210
+ "aac": "audio/aac",
211
+ // Video
212
+ "mp4": "video/mp4",
213
+ "webm": "video/webm",
214
+ "avi": "video/x-msvideo",
215
+ "mov": "video/quicktime",
216
+ "mkv": "video/x-matroska",
217
+ // Text/Data
218
+ "json": "application/json",
219
+ "txt": "text/plain",
220
+ "html": "text/html",
221
+ "css": "text/css",
222
+ "js": "application/javascript"
223
+ };
224
+ return extToMime[ext.toLowerCase()] || "application/octet-stream";
225
+ }
226
+
227
+ // ../core/dist/zip.js
228
+ import { Unzip, UnzipInflate, UnzipPassThrough } from "fflate";
229
+ function indexOf(data, search, fromIndex = 0) {
230
+ outer: for (let i = fromIndex; i <= data.length - search.length; i++) {
231
+ for (let j = 0; j < search.length; j++) {
232
+ if (data[i + j] !== search[j]) continue outer;
233
+ }
234
+ return i;
235
+ }
236
+ return -1;
237
+ }
238
+ function concat(...arrays) {
239
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
240
+ const result = new Uint8Array(totalLength);
241
+ let offset = 0;
242
+ for (const arr of arrays) {
243
+ result.set(arr, offset);
244
+ offset += arr.length;
245
+ }
246
+ return result;
247
+ }
248
+ var ZIP_SIGNATURE = new Uint8Array([80, 75, 3, 4]);
249
+ var JPEG_SIGNATURE = new Uint8Array([255, 216, 255]);
250
+ var DEFAULT_ZIP_LIMITS = {
251
+ maxFileSize: 50 * 1024 * 1024,
252
+ // 50MB per file (Risu standard)
253
+ maxTotalSize: 500 * 1024 * 1024,
254
+ // 500MB total (CharX can have many expression assets)
255
+ maxFiles: 1e4,
256
+ // CharX cards can have 2k+ expression assets
257
+ unsafePathHandling: "skip"
258
+ // Backwards compatible default
259
+ };
260
+ function isJPEG(data) {
261
+ return data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255;
262
+ }
263
+ function isJpegCharX(data) {
264
+ if (!isJPEG(data)) return false;
265
+ return indexOf(data, ZIP_SIGNATURE) > 0;
266
+ }
267
+ function findZipStart(data) {
268
+ const index = indexOf(data, ZIP_SIGNATURE);
269
+ if (index > 0) {
270
+ return data.subarray(index);
271
+ }
272
+ return data;
273
+ }
274
+ function getZipOffset(data) {
275
+ return indexOf(data, ZIP_SIGNATURE);
276
+ }
277
+ function isPathSafe(path) {
278
+ if (path.startsWith("/") || /^[a-zA-Z]:/.test(path)) {
279
+ return false;
280
+ }
281
+ if (path.includes("..")) {
282
+ return false;
283
+ }
284
+ if (path.includes("\\")) {
285
+ return false;
286
+ }
287
+ return true;
288
+ }
289
+ var ZipPreflightError = class extends Error {
290
+ constructor(message, totalSize, maxSize, oversizedEntry, entrySize, maxEntrySize) {
291
+ super(message);
292
+ this.totalSize = totalSize;
293
+ this.maxSize = maxSize;
294
+ this.oversizedEntry = oversizedEntry;
295
+ this.entrySize = entrySize;
296
+ this.maxEntrySize = maxEntrySize;
297
+ this.name = "ZipPreflightError";
298
+ }
299
+ };
300
+ function streamingUnzipSync(data, limits = DEFAULT_ZIP_LIMITS) {
301
+ const zipData = findZipStart(data);
302
+ const result = {};
303
+ let totalBytes = 0;
304
+ let fileCount = 0;
305
+ let error = null;
306
+ const unsafePathHandling = limits.unsafePathHandling ?? "skip";
307
+ const fileChunks = /* @__PURE__ */ new Map();
308
+ const unzipper = new Unzip((file) => {
309
+ if (error) return;
310
+ if (file.name.endsWith("/")) {
311
+ file.start();
312
+ return;
313
+ }
314
+ if (!isPathSafe(file.name)) {
315
+ const reason = file.name.includes("..") ? "path traversal (..)" : file.name.startsWith("/") || /^[a-zA-Z]:/.test(file.name) ? "absolute path" : "backslash in path";
316
+ if (unsafePathHandling === "reject") {
317
+ error = new ZipPreflightError(
318
+ `Unsafe path detected: "${file.name}" - ${reason}. This may be a path traversal attack.`
319
+ );
320
+ file.terminate();
321
+ return;
322
+ }
323
+ if (unsafePathHandling === "warn" && limits.onUnsafePath) {
324
+ limits.onUnsafePath(file.name, reason);
325
+ }
326
+ file.ondata = () => {
327
+ };
328
+ file.start();
329
+ return;
330
+ }
331
+ fileCount++;
332
+ if (fileCount > limits.maxFiles) {
333
+ error = new ZipPreflightError(
334
+ `File count ${fileCount} exceeds limit ${limits.maxFiles}`
335
+ );
336
+ file.terminate();
337
+ return;
338
+ }
339
+ const chunks = [];
340
+ fileChunks.set(file.name, chunks);
341
+ let fileBytes = 0;
342
+ file.ondata = (err, chunk, final) => {
343
+ if (error) return;
344
+ if (err) {
345
+ error = err;
346
+ return;
347
+ }
348
+ if (chunk && chunk.length > 0) {
349
+ fileBytes += chunk.length;
350
+ totalBytes += chunk.length;
351
+ if (fileBytes > limits.maxFileSize) {
352
+ error = new ZipPreflightError(
353
+ `File "${file.name}" actual size ${fileBytes} exceeds limit ${limits.maxFileSize}`,
354
+ void 0,
355
+ void 0,
356
+ file.name,
357
+ fileBytes,
358
+ limits.maxFileSize
359
+ );
360
+ file.terminate();
361
+ return;
362
+ }
363
+ if (totalBytes > limits.maxTotalSize) {
364
+ error = new ZipPreflightError(
365
+ `Total actual size ${totalBytes} exceeds limit ${limits.maxTotalSize}`,
366
+ totalBytes,
367
+ limits.maxTotalSize
368
+ );
369
+ file.terminate();
370
+ return;
371
+ }
372
+ chunks.push(chunk);
373
+ }
374
+ if (final && !error) {
375
+ result[file.name] = concat(...chunks);
376
+ }
377
+ };
378
+ file.start();
379
+ });
380
+ unzipper.register(UnzipInflate);
381
+ unzipper.register(UnzipPassThrough);
382
+ unzipper.push(zipData, true);
383
+ if (error) {
384
+ throw error;
385
+ }
386
+ return result;
387
+ }
388
+
389
+ // ../schemas/dist/index.js
390
+ import { z } from "zod";
391
+ import { z as z2 } from "zod";
392
+ import { z as z3 } from "zod";
393
+ import "zod";
394
+ var ISO8601Schema = z.string().datetime();
395
+ var UUIDSchema = z.string().uuid();
396
+ var SpecSchema = z.enum(["v2", "v3"]);
397
+ var SourceFormatSchema = z.enum([
398
+ "png_v2",
399
+ // PNG with 'chara' chunk (v2)
400
+ "png_v3",
401
+ // PNG with 'ccv3' chunk (v3)
402
+ "json_v2",
403
+ // Raw JSON v2
404
+ "json_v3",
405
+ // Raw JSON v3
406
+ "charx",
407
+ // ZIP with card.json (v3 spec)
408
+ "charx_risu",
409
+ // ZIP with card.json + module.risum
410
+ "charx_jpeg",
411
+ // JPEG with appended ZIP (read-only)
412
+ "voxta"
413
+ // VoxPkg format
414
+ ]);
415
+ var OriginalShapeSchema = z.enum(["wrapped", "unwrapped", "legacy"]);
416
+ var AssetTypeSchema = z.enum([
417
+ "icon",
418
+ "background",
419
+ "emotion",
420
+ "user_icon",
421
+ "sound",
422
+ "video",
423
+ "custom",
424
+ "x-risu-asset"
425
+ ]);
426
+ var AssetDescriptorSchema = z.object({
427
+ type: AssetTypeSchema,
428
+ uri: z.string(),
429
+ name: z.string(),
430
+ ext: z.string()
431
+ });
432
+ var ExtractedAssetSchema = z.object({
433
+ descriptor: AssetDescriptorSchema,
434
+ data: z.instanceof(Uint8Array),
435
+ mimeType: z.string()
436
+ });
437
+ var CCv2LorebookEntrySchema = z2.object({
438
+ keys: z2.array(z2.string()),
439
+ content: z2.string(),
440
+ enabled: z2.boolean(),
441
+ insertion_order: z2.number().int(),
442
+ // Optional fields
443
+ extensions: z2.record(z2.unknown()).optional(),
444
+ case_sensitive: z2.boolean().optional(),
445
+ name: z2.string().optional(),
446
+ priority: z2.number().int().optional(),
447
+ id: z2.number().int().optional(),
448
+ comment: z2.string().optional(),
449
+ selective: z2.boolean().optional(),
450
+ secondary_keys: z2.array(z2.string()).optional(),
451
+ constant: z2.boolean().optional(),
452
+ position: z2.enum(["before_char", "after_char"]).optional()
453
+ });
454
+ var CCv2CharacterBookSchema = z2.object({
455
+ name: z2.string().optional(),
456
+ description: z2.string().optional(),
457
+ scan_depth: z2.number().int().nonnegative().optional(),
458
+ token_budget: z2.number().int().nonnegative().optional(),
459
+ recursive_scanning: z2.boolean().optional(),
460
+ extensions: z2.record(z2.unknown()).optional(),
461
+ entries: z2.array(CCv2LorebookEntrySchema)
462
+ });
463
+ var CCv2DataSchema = z2.object({
464
+ // Core fields - use .default('') to handle missing fields in malformed cards
465
+ name: z2.string().default(""),
466
+ description: z2.string().default(""),
467
+ personality: z2.string().nullable().default(""),
468
+ // Can be null in wild (141 cards)
469
+ scenario: z2.string().default(""),
470
+ first_mes: z2.string().default(""),
471
+ mes_example: z2.string().nullable().default(""),
472
+ // Can be null in wild (186 cards)
473
+ // Optional fields
474
+ creator_notes: z2.string().optional(),
475
+ system_prompt: z2.string().optional(),
476
+ post_history_instructions: z2.string().optional(),
477
+ alternate_greetings: z2.array(z2.string()).optional(),
478
+ character_book: CCv2CharacterBookSchema.optional().nullable(),
479
+ tags: z2.array(z2.string()).optional(),
480
+ creator: z2.string().optional(),
481
+ character_version: z2.string().optional(),
482
+ extensions: z2.record(z2.unknown()).optional()
483
+ });
484
+ var CCv2WrappedSchema = z2.object({
485
+ spec: z2.literal("chara_card_v2"),
486
+ spec_version: z2.literal("2.0"),
487
+ data: CCv2DataSchema
488
+ });
489
+ var CCv3LorebookEntrySchema = z3.object({
490
+ keys: z3.array(z3.string()),
491
+ content: z3.string(),
492
+ enabled: z3.boolean(),
493
+ insertion_order: z3.number().int(),
494
+ // Optional fields
495
+ case_sensitive: z3.boolean().optional(),
496
+ name: z3.string().optional(),
497
+ priority: z3.number().int().optional(),
498
+ id: z3.number().int().optional(),
499
+ comment: z3.string().optional(),
500
+ selective: z3.boolean().optional(),
501
+ secondary_keys: z3.array(z3.string()).optional(),
502
+ constant: z3.boolean().optional(),
503
+ position: z3.enum(["before_char", "after_char"]).optional(),
504
+ extensions: z3.record(z3.unknown()).optional(),
505
+ // v3 specific
506
+ automation_id: z3.string().optional(),
507
+ role: z3.enum(["system", "user", "assistant"]).optional(),
508
+ group: z3.string().optional(),
509
+ scan_frequency: z3.number().int().nonnegative().optional(),
510
+ probability: z3.number().min(0).max(1).optional(),
511
+ use_regex: z3.boolean().optional(),
512
+ depth: z3.number().int().nonnegative().optional(),
513
+ selective_logic: z3.enum(["AND", "NOT"]).optional()
514
+ });
515
+ var CCv3CharacterBookSchema = z3.object({
516
+ name: z3.string().optional(),
517
+ description: z3.string().optional(),
518
+ scan_depth: z3.number().int().nonnegative().optional(),
519
+ token_budget: z3.number().int().nonnegative().optional(),
520
+ recursive_scanning: z3.boolean().optional(),
521
+ extensions: z3.record(z3.unknown()).optional(),
522
+ entries: z3.array(CCv3LorebookEntrySchema)
523
+ });
524
+ var CCv3DataInnerSchema = z3.object({
525
+ // Core fields - use .default('') to handle missing fields in malformed cards
526
+ name: z3.string().default(""),
527
+ description: z3.string().default(""),
528
+ personality: z3.string().nullable().default(""),
529
+ // Can be null in wild (141 cards)
530
+ scenario: z3.string().default(""),
531
+ first_mes: z3.string().default(""),
532
+ mes_example: z3.string().nullable().default(""),
533
+ // Can be null in wild (186 cards)
534
+ // "Required" per spec but often missing in wild - use defaults for leniency
535
+ creator: z3.string().default(""),
536
+ character_version: z3.string().default(""),
537
+ tags: z3.array(z3.string()).default([]),
538
+ group_only_greetings: z3.array(z3.string()).default([]),
539
+ // Optional fields
540
+ creator_notes: z3.string().optional(),
541
+ system_prompt: z3.string().optional(),
542
+ post_history_instructions: z3.string().optional(),
543
+ alternate_greetings: z3.array(z3.string()).optional(),
544
+ character_book: CCv3CharacterBookSchema.optional().nullable(),
545
+ extensions: z3.record(z3.unknown()).optional(),
546
+ // v3 specific
547
+ assets: z3.array(AssetDescriptorSchema).optional(),
548
+ nickname: z3.string().optional(),
549
+ creator_notes_multilingual: z3.record(z3.string()).optional(),
550
+ source: z3.array(z3.string()).optional(),
551
+ creation_date: z3.number().int().nonnegative().optional(),
552
+ // Unix timestamp in seconds
553
+ modification_date: z3.number().int().nonnegative().optional()
554
+ // Unix timestamp in seconds
555
+ });
556
+ var CCv3DataSchema = z3.object({
557
+ spec: z3.literal("chara_card_v3"),
558
+ spec_version: z3.literal("3.0"),
559
+ data: CCv3DataInnerSchema
560
+ });
561
+ function hasRisuExtensions(extensions) {
562
+ if (!extensions) return false;
563
+ return "risuai" in extensions || "risu" in extensions;
564
+ }
565
+
566
+ // ../charx/dist/index.js
567
+ import { zipSync } from "fflate";
568
+ var DEFAULT_OPTIONS = {
569
+ maxFileSize: 10 * 1024 * 1024,
570
+ // 10MB
571
+ maxAssetSize: 50 * 1024 * 1024,
572
+ // 50MB (Risu standard)
573
+ maxTotalSize: 200 * 1024 * 1024,
574
+ // 200MB
575
+ preserveXMeta: true,
576
+ preserveModuleRisum: true
577
+ };
578
+ function isCharX(data) {
579
+ const zipOffset = getZipOffset(data);
580
+ if (zipOffset < 0) return false;
581
+ const zipData = data.subarray(zipOffset);
582
+ const cardJsonMarker = new TextEncoder().encode("card.json");
583
+ const scanSize = Math.min(zipData.length, 65536);
584
+ const startOffset = zipData.length - scanSize;
585
+ for (let i = startOffset; i < zipData.length - cardJsonMarker.length; i++) {
586
+ let found = true;
587
+ for (let j = 0; j < cardJsonMarker.length; j++) {
588
+ if (zipData[i + j] !== cardJsonMarker[j]) {
589
+ found = false;
590
+ break;
591
+ }
592
+ }
593
+ if (found) return true;
594
+ }
595
+ return false;
596
+ }
597
+ function readCharX(data, options = {}) {
598
+ const opts = { ...DEFAULT_OPTIONS, ...options };
599
+ let unzipped;
600
+ try {
601
+ unzipped = streamingUnzipSync(data, {
602
+ maxFileSize: opts.maxAssetSize,
603
+ maxTotalSize: opts.maxTotalSize,
604
+ maxFiles: 1e4
605
+ // CharX can have many assets
606
+ });
607
+ } catch (err) {
608
+ if (err instanceof ZipPreflightError) {
609
+ throw new SizeLimitError(
610
+ err.totalSize || err.entrySize || 0,
611
+ err.maxSize || err.maxEntrySize || opts.maxTotalSize,
612
+ err.oversizedEntry || "CharX archive"
613
+ );
614
+ }
615
+ throw new ParseError(
616
+ `Failed to unzip CharX: ${err instanceof Error ? err.message : String(err)}`,
617
+ "charx"
618
+ );
619
+ }
620
+ let cardJson = null;
621
+ const assets = [];
622
+ const metadata = /* @__PURE__ */ new Map();
623
+ let moduleRisum;
624
+ for (const [fileName, fileData] of Object.entries(unzipped)) {
625
+ if (fileName.endsWith("/") || fileData.length === 0) continue;
626
+ if (fileName === "card.json") {
627
+ if (fileData.length > opts.maxFileSize) {
628
+ throw new SizeLimitError(fileData.length, opts.maxFileSize, "card.json");
629
+ }
630
+ try {
631
+ const content = toString(fileData);
632
+ cardJson = JSON.parse(content);
633
+ } catch (err) {
634
+ throw new ParseError(
635
+ `Failed to parse card.json: ${err instanceof Error ? err.message : String(err)}`,
636
+ "charx"
637
+ );
638
+ }
639
+ continue;
640
+ }
641
+ if (opts.preserveXMeta) {
642
+ const metaMatch = fileName.match(/^x_meta\/(\d+)\.json$/);
643
+ if (metaMatch) {
644
+ const index = parseInt(metaMatch[1], 10);
645
+ try {
646
+ const content = toString(fileData);
647
+ const meta = JSON.parse(content);
648
+ metadata.set(index, meta);
649
+ } catch {
650
+ }
651
+ continue;
652
+ }
653
+ }
654
+ if (fileName === "module.risum" && opts.preserveModuleRisum) {
655
+ moduleRisum = fileData;
656
+ continue;
657
+ }
658
+ if (fileName.startsWith("assets/")) {
659
+ const name = fileName.split("/").pop() || "unknown";
660
+ const ext = name.split(".").pop() || "bin";
661
+ assets.push({
662
+ path: fileName,
663
+ descriptor: {
664
+ type: "custom",
665
+ name: name.replace(/\.[^.]+$/, ""),
666
+ // Remove extension
667
+ uri: `embeded://${fileName}`,
668
+ ext
669
+ },
670
+ buffer: fileData
671
+ });
672
+ continue;
673
+ }
674
+ }
675
+ if (!cardJson) {
676
+ throw new ParseError("CharX file does not contain card.json", "charx");
677
+ }
678
+ if (cardJson.spec !== "chara_card_v3") {
679
+ throw new ParseError(
680
+ `Invalid card spec: expected "chara_card_v3", got "${cardJson.spec}"`,
681
+ "charx"
682
+ );
683
+ }
684
+ const matchedAssets = matchAssetsToDescriptors(assets, cardJson.data.assets || []);
685
+ const isRisuFormat = !!moduleRisum || hasRisuExtensions(cardJson.data.extensions);
686
+ return {
687
+ card: cardJson,
688
+ assets: matchedAssets,
689
+ metadata: metadata.size > 0 ? metadata : void 0,
690
+ moduleRisum,
691
+ isRisuFormat
692
+ };
693
+ }
694
+ function matchAssetsToDescriptors(extractedAssets, descriptors) {
695
+ const assetsByPath = /* @__PURE__ */ new Map();
696
+ for (const asset of extractedAssets) {
697
+ assetsByPath.set(asset.path, asset);
698
+ }
699
+ const matched = [];
700
+ for (const descriptor of descriptors) {
701
+ const parsed = parseURI(descriptor.uri);
702
+ if (parsed.scheme === "embeded" && parsed.path) {
703
+ const asset = assetsByPath.get(parsed.path);
704
+ if (asset) {
705
+ matched.push({
706
+ ...asset,
707
+ descriptor
708
+ });
709
+ } else {
710
+ matched.push({
711
+ path: parsed.path,
712
+ descriptor,
713
+ buffer: void 0
714
+ });
715
+ }
716
+ } else if (parsed.scheme === "ccdefault") {
717
+ matched.push({
718
+ path: "ccdefault:",
719
+ descriptor,
720
+ buffer: void 0
721
+ });
722
+ } else if (parsed.scheme === "https" || parsed.scheme === "http") {
723
+ matched.push({
724
+ path: descriptor.uri,
725
+ descriptor,
726
+ buffer: void 0
727
+ });
728
+ } else if (parsed.scheme === "data") {
729
+ if (parsed.data && parsed.encoding === "base64") {
730
+ const buffer = decode(parsed.data);
731
+ matched.push({
732
+ path: "data:",
733
+ descriptor,
734
+ buffer
735
+ });
736
+ } else {
737
+ matched.push({
738
+ path: "data:",
739
+ descriptor,
740
+ buffer: void 0
741
+ });
742
+ }
743
+ }
744
+ }
745
+ return matched;
746
+ }
747
+ function readCardJsonOnly(data) {
748
+ let unzipped;
749
+ try {
750
+ unzipped = streamingUnzipSync(data, {
751
+ maxFileSize: DEFAULT_OPTIONS.maxFileSize,
752
+ // 10MB for card.json
753
+ maxTotalSize: DEFAULT_OPTIONS.maxTotalSize,
754
+ // 200MB total
755
+ maxFiles: 1e4
756
+ });
757
+ } catch (err) {
758
+ throw new ParseError(
759
+ `Failed to unzip CharX: ${err instanceof Error ? err.message : String(err)}`,
760
+ "charx"
761
+ );
762
+ }
763
+ const cardData = unzipped["card.json"];
764
+ if (!cardData) {
765
+ throw new ParseError("card.json not found in CharX file", "charx");
766
+ }
767
+ try {
768
+ const content = toString(cardData);
769
+ return JSON.parse(content);
770
+ } catch (err) {
771
+ throw new ParseError(
772
+ `Failed to parse card.json: ${err instanceof Error ? err.message : String(err)}`,
773
+ "charx"
774
+ );
775
+ }
776
+ }
777
+ async function readCharXAsync(data, options = {}) {
778
+ const result = readCharX(data, options);
779
+ if (!options.fetchRemoteAssets || !options.assetFetcher) {
780
+ return result;
781
+ }
782
+ const fetchedAssets = await Promise.all(
783
+ result.assets.map(async (asset) => {
784
+ if (asset.buffer) {
785
+ return asset;
786
+ }
787
+ const parsed = parseURI(asset.descriptor.uri);
788
+ if ((parsed.scheme === "https" || parsed.scheme === "http") && parsed.url) {
789
+ try {
790
+ const buffer = await options.assetFetcher(parsed.url);
791
+ if (buffer) {
792
+ return { ...asset, buffer };
793
+ }
794
+ } catch {
795
+ }
796
+ }
797
+ return asset;
798
+ })
799
+ );
800
+ return {
801
+ ...result,
802
+ assets: fetchedAssets
803
+ };
804
+ }
805
+ function getCharxCategory(mimetype) {
806
+ if (mimetype.startsWith("image/")) return "images";
807
+ if (mimetype.startsWith("audio/")) return "audio";
808
+ if (mimetype.startsWith("video/")) return "video";
809
+ return "other";
810
+ }
811
+ function sanitizeName(name, ext) {
812
+ let safeName = name;
813
+ if (safeName.toLowerCase().endsWith(`.${ext.toLowerCase()}`)) {
814
+ safeName = safeName.substring(0, safeName.length - (ext.length + 1));
815
+ }
816
+ safeName = safeName.replace(/[._]/g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
817
+ if (!safeName) safeName = "asset";
818
+ return safeName;
819
+ }
820
+ function writeCharX(card, assets, options = {}) {
821
+ const {
822
+ spec = "v3",
823
+ compressionLevel = 6,
824
+ emitXMeta = spec === "risu",
825
+ emitReadme = false,
826
+ moduleRisum
827
+ } = options;
828
+ const transformedCard = transformAssetUris(card, assets);
829
+ const zipEntries = {};
830
+ const cardJson = JSON.stringify(transformedCard, null, 2);
831
+ zipEntries["card.json"] = [fromString(cardJson), { level: compressionLevel }];
832
+ if (emitReadme) {
833
+ const readme = `Character: ${card.data.name}
834
+ Created with Character Foundry
835
+
836
+ This is a CharX character card package.
837
+ Import this file into SillyTavern, RisuAI, or other compatible applications.
838
+ `;
839
+ zipEntries["readme.txt"] = [fromString(readme), { level: compressionLevel }];
840
+ }
841
+ let assetCount = 0;
842
+ for (let i = 0; i < assets.length; i++) {
843
+ const asset = assets[i];
844
+ const mimetype = getMimeTypeFromExt(asset.ext);
845
+ const category = getCharxCategory(mimetype);
846
+ const safeName = sanitizeName(asset.name, asset.ext);
847
+ const assetPath = `assets/${asset.type}/${category}/${safeName}.${asset.ext}`;
848
+ zipEntries[assetPath] = [asset.data, { level: compressionLevel }];
849
+ assetCount++;
850
+ if (emitXMeta && mimetype.startsWith("image/")) {
851
+ const metaJson = JSON.stringify({
852
+ type: mimetype.split("/")[1]?.toUpperCase() || "PNG"
853
+ });
854
+ zipEntries[`x_meta/${i}.json`] = [fromString(metaJson), { level: compressionLevel }];
855
+ }
856
+ }
857
+ if (moduleRisum) {
858
+ zipEntries["module.risum"] = [moduleRisum, { level: compressionLevel }];
859
+ }
860
+ const buffer = zipSync(zipEntries);
861
+ return {
862
+ buffer,
863
+ assetCount,
864
+ totalSize: buffer.length
865
+ };
866
+ }
867
+ function transformAssetUris(card, assets) {
868
+ const transformed = JSON.parse(JSON.stringify(card));
869
+ transformed.data.assets = assets.map((asset) => {
870
+ const mimetype = getMimeTypeFromExt(asset.ext);
871
+ const category = getCharxCategory(mimetype);
872
+ const safeName = sanitizeName(asset.name, asset.ext);
873
+ return {
874
+ type: asset.type,
875
+ uri: `embeded://assets/${asset.type}/${category}/${safeName}.${asset.ext}`,
876
+ name: safeName,
877
+ ext: asset.ext
878
+ };
879
+ });
880
+ return transformed;
881
+ }
882
+ async function writeCharXAsync(card, assets, options = {}) {
883
+ return writeCharX(card, assets, options);
884
+ }
885
+ export {
886
+ isCharX,
887
+ isJpegCharX,
888
+ readCardJsonOnly,
889
+ readCharX,
890
+ readCharXAsync,
891
+ writeCharX,
892
+ writeCharXAsync
893
+ };
2
894
  //# sourceMappingURL=charx.js.map