run-dmcp 0.1.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.
Files changed (167) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +79 -0
  3. package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
  4. package/dist/__tests__/engineVocabulary.test.js +147 -0
  5. package/dist/db/__tests__/connection.test.d.ts +1 -0
  6. package/dist/db/__tests__/connection.test.js +72 -0
  7. package/dist/db/__tests__/testDb.d.ts +33 -0
  8. package/dist/db/__tests__/testDb.js +41 -0
  9. package/dist/db/connection.d.ts +22 -0
  10. package/dist/db/connection.js +107 -0
  11. package/dist/db/schema.d.ts +1 -0
  12. package/dist/db/schema.js +725 -0
  13. package/dist/events/emitter.d.ts +22 -0
  14. package/dist/events/emitter.js +71 -0
  15. package/dist/http/server.d.ts +3 -0
  16. package/dist/http/server.js +649 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +92 -0
  19. package/dist/register/abilities.d.ts +2 -0
  20. package/dist/register/abilities.js +165 -0
  21. package/dist/register/audio.d.ts +2 -0
  22. package/dist/register/audio.js +326 -0
  23. package/dist/register/batch.d.ts +2 -0
  24. package/dist/register/batch.js +343 -0
  25. package/dist/register/character.d.ts +2 -0
  26. package/dist/register/character.js +324 -0
  27. package/dist/register/combat.d.ts +2 -0
  28. package/dist/register/combat.js +207 -0
  29. package/dist/register/core.d.ts +2 -0
  30. package/dist/register/core.js +1040 -0
  31. package/dist/register/display.d.ts +2 -0
  32. package/dist/register/display.js +263 -0
  33. package/dist/register/factions.d.ts +2 -0
  34. package/dist/register/factions.js +186 -0
  35. package/dist/register/images.d.ts +2 -0
  36. package/dist/register/images.js +400 -0
  37. package/dist/register/inventory.d.ts +2 -0
  38. package/dist/register/inventory.js +115 -0
  39. package/dist/register/mcp-prompts.d.ts +2 -0
  40. package/dist/register/mcp-prompts.js +684 -0
  41. package/dist/register/mcp-resources.d.ts +2 -0
  42. package/dist/register/mcp-resources.js +335 -0
  43. package/dist/register/narrative.d.ts +2 -0
  44. package/dist/register/narrative.js +242 -0
  45. package/dist/register/notes.d.ts +2 -0
  46. package/dist/register/notes.js +170 -0
  47. package/dist/register/pause.d.ts +2 -0
  48. package/dist/register/pause.js +580 -0
  49. package/dist/register/quests.d.ts +2 -0
  50. package/dist/register/quests.js +118 -0
  51. package/dist/register/relationships.d.ts +2 -0
  52. package/dist/register/relationships.js +147 -0
  53. package/dist/register/resources.d.ts +2 -0
  54. package/dist/register/resources.js +277 -0
  55. package/dist/register/secrets.d.ts +2 -0
  56. package/dist/register/secrets.js +192 -0
  57. package/dist/register/status.d.ts +2 -0
  58. package/dist/register/status.js +130 -0
  59. package/dist/register/tables.d.ts +2 -0
  60. package/dist/register/tables.js +146 -0
  61. package/dist/register/tags.d.ts +2 -0
  62. package/dist/register/tags.js +114 -0
  63. package/dist/register/time.d.ts +2 -0
  64. package/dist/register/time.js +281 -0
  65. package/dist/register/world.d.ts +2 -0
  66. package/dist/register/world.js +127 -0
  67. package/dist/schemas/index.d.ts +921 -0
  68. package/dist/schemas/index.js +121 -0
  69. package/dist/test-setup.d.ts +1 -0
  70. package/dist/test-setup.js +13 -0
  71. package/dist/tools/__tests__/audio.test.d.ts +1 -0
  72. package/dist/tools/__tests__/audio.test.js +59 -0
  73. package/dist/tools/__tests__/conserved.test.d.ts +1 -0
  74. package/dist/tools/__tests__/conserved.test.js +488 -0
  75. package/dist/tools/__tests__/constraint.test.d.ts +1 -0
  76. package/dist/tools/__tests__/constraint.test.js +212 -0
  77. package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
  78. package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
  79. package/dist/tools/__tests__/images.test.d.ts +1 -0
  80. package/dist/tools/__tests__/images.test.js +59 -0
  81. package/dist/tools/__tests__/relationship.test.d.ts +1 -0
  82. package/dist/tools/__tests__/relationship.test.js +132 -0
  83. package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
  84. package/dist/tools/__tests__/resource-constraints.test.js +131 -0
  85. package/dist/tools/__tests__/resource.test.d.ts +1 -0
  86. package/dist/tools/__tests__/resource.test.js +190 -0
  87. package/dist/tools/__tests__/time.test.d.ts +1 -0
  88. package/dist/tools/__tests__/time.test.js +404 -0
  89. package/dist/tools/__tests__/timers.test.d.ts +1 -0
  90. package/dist/tools/__tests__/timers.test.js +426 -0
  91. package/dist/tools/__tests__/world.test.d.ts +1 -0
  92. package/dist/tools/__tests__/world.test.js +70 -0
  93. package/dist/tools/ability.d.ts +48 -0
  94. package/dist/tools/ability.js +238 -0
  95. package/dist/tools/audio.d.ts +24 -0
  96. package/dist/tools/audio.js +365 -0
  97. package/dist/tools/character.d.ts +70 -0
  98. package/dist/tools/character.js +309 -0
  99. package/dist/tools/combat.d.ts +13 -0
  100. package/dist/tools/combat.js +195 -0
  101. package/dist/tools/constraint.d.ts +132 -0
  102. package/dist/tools/constraint.js +269 -0
  103. package/dist/tools/dice.d.ts +23 -0
  104. package/dist/tools/dice.js +111 -0
  105. package/dist/tools/display.d.ts +120 -0
  106. package/dist/tools/display.js +528 -0
  107. package/dist/tools/faction.d.ts +61 -0
  108. package/dist/tools/faction.js +269 -0
  109. package/dist/tools/game.d.ts +96 -0
  110. package/dist/tools/game.js +526 -0
  111. package/dist/tools/image-prompt.d.ts +49 -0
  112. package/dist/tools/image-prompt.js +479 -0
  113. package/dist/tools/images.d.ts +47 -0
  114. package/dist/tools/images.js +449 -0
  115. package/dist/tools/inventory.d.ts +20 -0
  116. package/dist/tools/inventory.js +145 -0
  117. package/dist/tools/narrative.d.ts +58 -0
  118. package/dist/tools/narrative.js +237 -0
  119. package/dist/tools/notes.d.ts +41 -0
  120. package/dist/tools/notes.js +220 -0
  121. package/dist/tools/pause.d.ts +110 -0
  122. package/dist/tools/pause.js +1254 -0
  123. package/dist/tools/quest.d.ts +34 -0
  124. package/dist/tools/quest.js +164 -0
  125. package/dist/tools/relationship.d.ts +74 -0
  126. package/dist/tools/relationship.js +324 -0
  127. package/dist/tools/resource.d.ts +93 -0
  128. package/dist/tools/resource.js +374 -0
  129. package/dist/tools/rules.d.ts +4 -0
  130. package/dist/tools/rules.js +30 -0
  131. package/dist/tools/secrets.d.ts +49 -0
  132. package/dist/tools/secrets.js +195 -0
  133. package/dist/tools/status.d.ts +36 -0
  134. package/dist/tools/status.js +218 -0
  135. package/dist/tools/tables.d.ts +33 -0
  136. package/dist/tools/tables.js +209 -0
  137. package/dist/tools/tags.d.ts +52 -0
  138. package/dist/tools/tags.js +176 -0
  139. package/dist/tools/time.d.ts +33 -0
  140. package/dist/tools/time.js +276 -0
  141. package/dist/tools/timers.d.ts +41 -0
  142. package/dist/tools/timers.js +215 -0
  143. package/dist/tools/world.d.ts +78 -0
  144. package/dist/tools/world.js +331 -0
  145. package/dist/types/index.d.ts +969 -0
  146. package/dist/types/index.js +1 -0
  147. package/dist/utils/__tests__/json.test.d.ts +1 -0
  148. package/dist/utils/__tests__/json.test.js +55 -0
  149. package/dist/utils/__tests__/validation.test.d.ts +1 -0
  150. package/dist/utils/__tests__/validation.test.js +90 -0
  151. package/dist/utils/errors.d.ts +44 -0
  152. package/dist/utils/errors.js +121 -0
  153. package/dist/utils/json.d.ts +9 -0
  154. package/dist/utils/json.js +23 -0
  155. package/dist/utils/logger.d.ts +7 -0
  156. package/dist/utils/logger.js +50 -0
  157. package/dist/utils/output-schemas.d.ts +594 -0
  158. package/dist/utils/output-schemas.js +331 -0
  159. package/dist/utils/tool-annotations.d.ts +147 -0
  160. package/dist/utils/tool-annotations.js +98 -0
  161. package/dist/utils/validation.d.ts +34 -0
  162. package/dist/utils/validation.js +52 -0
  163. package/dist/utils/verbosity.d.ts +57 -0
  164. package/dist/utils/verbosity.js +67 -0
  165. package/dist/utils/webui.d.ts +20 -0
  166. package/dist/utils/webui.js +35 -0
  167. package/package.json +75 -0
@@ -0,0 +1,449 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase, getDataDir, withTransaction } from "../db/connection.js";
3
+ import { writeFileSync, readFileSync, mkdirSync, existsSync, unlinkSync, rmSync, } from "fs";
4
+ import { dirname, join } from "path";
5
+ import sharp from "sharp";
6
+ import { getCharacter } from "./character.js";
7
+ import { getLocation } from "./world.js";
8
+ import { getItem } from "./inventory.js";
9
+ import { getFaction } from "./faction.js";
10
+ // Use the same data directory as the database
11
+ function getImagesDir() {
12
+ return join(getDataDir(), "images");
13
+ }
14
+ // Helper: Validate entity exists and return its name
15
+ // For known entity types (character, location, item, faction), validates the entity exists
16
+ // For other types (scene, quest, ability, etc.), skips validation and returns capitalized type name
17
+ function validateEntityAndGetName(entityId, entityType) {
18
+ let entity = null;
19
+ switch (entityType) {
20
+ case "character":
21
+ entity = getCharacter(entityId);
22
+ break;
23
+ case "location":
24
+ entity = getLocation(entityId);
25
+ break;
26
+ case "item":
27
+ entity = getItem(entityId);
28
+ break;
29
+ case "faction":
30
+ entity = getFaction(entityId);
31
+ break;
32
+ default:
33
+ // For other entity types (scene, quest, ability, note, etc.), skip validation
34
+ // These may not have dedicated tables or the caller knows the entity exists
35
+ return entityType.charAt(0).toUpperCase() + entityType.slice(1);
36
+ }
37
+ if (!entity) {
38
+ throw new Error(`${entityType} not found: ${entityId}`);
39
+ }
40
+ return entity.name;
41
+ }
42
+ // Helper: Validate entity exists (legacy wrapper)
43
+ function validateEntityExists(entityId, entityType) {
44
+ validateEntityAndGetName(entityId, entityType);
45
+ }
46
+ // Helper: Ensure directory exists
47
+ function ensureDir(dirPath) {
48
+ if (!existsSync(dirPath)) {
49
+ mkdirSync(dirPath, { recursive: true });
50
+ }
51
+ }
52
+ // Helper: Get extension from mime type
53
+ function getExtension(mimeType) {
54
+ const map = {
55
+ "image/png": "png",
56
+ "image/jpeg": "jpg",
57
+ "image/jpg": "jpg",
58
+ "image/gif": "gif",
59
+ "image/webp": "webp",
60
+ "image/svg+xml": "svg",
61
+ };
62
+ return map[mimeType] || "png";
63
+ }
64
+ // Helper: Map database row to StoredImage
65
+ function mapRowToStoredImage(row) {
66
+ return {
67
+ id: row.id,
68
+ gameId: row.game_id,
69
+ entityId: row.entity_id,
70
+ entityType: row.entity_type,
71
+ filePath: row.file_path,
72
+ fileSize: row.file_size,
73
+ mimeType: row.mime_type,
74
+ width: row.width,
75
+ height: row.height,
76
+ label: row.label,
77
+ description: row.description,
78
+ source: row.source,
79
+ sourceUrl: row.source_url,
80
+ generationTool: row.generation_tool,
81
+ generationPrompt: row.generation_prompt,
82
+ isPrimary: row.is_primary === 1,
83
+ createdAt: row.created_at,
84
+ };
85
+ }
86
+ export async function storeImage(params) {
87
+ // Validate entity exists and get its name for confirmation
88
+ const entityName = validateEntityAndGetName(params.entityId, params.entityType);
89
+ const db = getDatabase();
90
+ const id = uuidv4();
91
+ const now = new Date().toISOString();
92
+ // Fetch image from URL
93
+ const response = await fetch(params.url);
94
+ if (!response.ok) {
95
+ throw new Error(`Failed to fetch image: ${response.status}`);
96
+ }
97
+ const imageBuffer = Buffer.from(await response.arrayBuffer());
98
+ let mimeType = response.headers.get("content-type") || params.mimeType || "image/png";
99
+ const source = params.generationTool ? "generated" : "url";
100
+ const sourceUrl = params.url;
101
+ // Get image metadata using sharp
102
+ let width = null;
103
+ let height = null;
104
+ try {
105
+ const metadata = await sharp(imageBuffer).metadata();
106
+ width = metadata.width || null;
107
+ height = metadata.height || null;
108
+ // Use sharp's detected format if available
109
+ if (metadata.format) {
110
+ const formatToMime = {
111
+ jpeg: "image/jpeg",
112
+ png: "image/png",
113
+ gif: "image/gif",
114
+ webp: "image/webp",
115
+ svg: "image/svg+xml",
116
+ tiff: "image/tiff",
117
+ avif: "image/avif",
118
+ };
119
+ mimeType = formatToMime[metadata.format] || mimeType;
120
+ }
121
+ }
122
+ catch {
123
+ // If sharp can't parse it, proceed without dimensions
124
+ }
125
+ // Build file path
126
+ const ext = getExtension(mimeType);
127
+ const relativePath = join(params.gameId, `${params.entityType}s`, params.entityId, `${id}.${ext}`);
128
+ const fullPath = join(getImagesDir(), relativePath);
129
+ // Ensure directory exists and write file
130
+ ensureDir(dirname(fullPath));
131
+ writeFileSync(fullPath, imageBuffer);
132
+ // If setting as primary, unset current primary first
133
+ if (params.setAsPrimary) {
134
+ db.prepare(`
135
+ UPDATE stored_images SET is_primary = 0
136
+ WHERE entity_id = ? AND entity_type = ?
137
+ `).run(params.entityId, params.entityType);
138
+ }
139
+ // Insert database record
140
+ const stmt = db.prepare(`
141
+ INSERT INTO stored_images (
142
+ id, game_id, entity_id, entity_type, file_path, file_size, mime_type,
143
+ width, height, label, description, source, source_url, generation_tool,
144
+ generation_prompt, is_primary, created_at
145
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
146
+ `);
147
+ stmt.run(id, params.gameId, params.entityId, params.entityType, relativePath, imageBuffer.length, mimeType, width, height, params.label || null, params.description || null, source, sourceUrl, params.generationTool || null, params.generationPrompt || null, params.setAsPrimary ? 1 : 0, now);
148
+ return {
149
+ id,
150
+ gameId: params.gameId,
151
+ entityId: params.entityId,
152
+ entityType: params.entityType,
153
+ entityName,
154
+ filePath: relativePath,
155
+ fileSize: imageBuffer.length,
156
+ mimeType,
157
+ width,
158
+ height,
159
+ label: params.label || null,
160
+ description: params.description || null,
161
+ source,
162
+ sourceUrl,
163
+ generationTool: params.generationTool || null,
164
+ generationPrompt: params.generationPrompt || null,
165
+ isPrimary: params.setAsPrimary || false,
166
+ createdAt: now,
167
+ };
168
+ }
169
+ export function getImage(imageId) {
170
+ const db = getDatabase();
171
+ const row = db.prepare(`SELECT * FROM stored_images WHERE id = ?`).get(imageId);
172
+ if (!row)
173
+ return null;
174
+ return mapRowToStoredImage(row);
175
+ }
176
+ export async function getImageData(imageId, options) {
177
+ const image = getImage(imageId);
178
+ if (!image)
179
+ return null;
180
+ const fullPath = join(getImagesDir(), image.filePath);
181
+ if (!existsSync(fullPath))
182
+ return null;
183
+ const originalBuffer = readFileSync(fullPath);
184
+ // If no processing options, return original
185
+ if (!options || (!options.format && !options.width && !options.height && !options.quality)) {
186
+ const base64 = `data:${image.mimeType};base64,${originalBuffer.toString("base64")}`;
187
+ return { image, base64, outputFormat: image.mimeType, outputSize: originalBuffer.length };
188
+ }
189
+ // Process with Sharp
190
+ let pipeline = sharp(originalBuffer);
191
+ // Resize if dimensions specified
192
+ if (options.width || options.height) {
193
+ pipeline = pipeline.resize({
194
+ width: options.width,
195
+ height: options.height,
196
+ fit: options.fit || "inside",
197
+ withoutEnlargement: true,
198
+ });
199
+ }
200
+ // Convert format and apply quality
201
+ let outputMimeType;
202
+ const quality = options.quality ?? 80;
203
+ switch (options.format) {
204
+ case "jpeg":
205
+ pipeline = pipeline.jpeg({ quality });
206
+ outputMimeType = "image/jpeg";
207
+ break;
208
+ case "webp":
209
+ pipeline = pipeline.webp({ quality });
210
+ outputMimeType = "image/webp";
211
+ break;
212
+ case "png":
213
+ pipeline = pipeline.png({ compressionLevel: Math.floor((100 - quality) / 10) });
214
+ outputMimeType = "image/png";
215
+ break;
216
+ default:
217
+ // Keep original format but apply quality if it's a lossy format
218
+ if (image.mimeType === "image/jpeg") {
219
+ pipeline = pipeline.jpeg({ quality });
220
+ }
221
+ else if (image.mimeType === "image/webp") {
222
+ pipeline = pipeline.webp({ quality });
223
+ }
224
+ outputMimeType = image.mimeType;
225
+ }
226
+ const processedBuffer = await pipeline.toBuffer();
227
+ const base64 = `data:${outputMimeType};base64,${processedBuffer.toString("base64")}`;
228
+ return { image, base64, outputFormat: outputMimeType, outputSize: processedBuffer.length };
229
+ }
230
+ export function listEntityImages(entityId, entityType) {
231
+ const db = getDatabase();
232
+ const rows = db
233
+ .prepare(`
234
+ SELECT * FROM stored_images
235
+ WHERE entity_id = ? AND entity_type = ?
236
+ ORDER BY is_primary DESC, created_at DESC
237
+ `)
238
+ .all(entityId, entityType);
239
+ const images = rows.map(mapRowToStoredImage);
240
+ const primaryImage = images.find((img) => img.isPrimary) || null;
241
+ return { entityId, entityType, images, primaryImage };
242
+ }
243
+ export function listEntitiesMissingImages(gameId, entityType) {
244
+ const db = getDatabase();
245
+ const result = {
246
+ gameId,
247
+ entities: [],
248
+ totalMissing: 0,
249
+ byType: { characters: 0, locations: 0, items: 0, factions: 0 },
250
+ };
251
+ // Helper to check if entity has a primary image
252
+ const hasPrimaryImage = (entityId, type) => {
253
+ const row = db.prepare(`
254
+ SELECT 1 FROM stored_images
255
+ WHERE entity_id = ? AND entity_type = ? AND is_primary = 1
256
+ LIMIT 1
257
+ `).get(entityId, type);
258
+ return !!row;
259
+ };
260
+ // Check characters
261
+ if (!entityType || entityType === "character") {
262
+ const characters = db.prepare(`
263
+ SELECT id, name, is_player, image_gen FROM characters WHERE game_id = ?
264
+ `).all(gameId);
265
+ for (const char of characters) {
266
+ if (!hasPrimaryImage(char.id, "character")) {
267
+ result.entities.push({
268
+ id: char.id,
269
+ name: char.name,
270
+ entityType: "character",
271
+ hasImageGen: !!char.image_gen,
272
+ isPlayer: char.is_player === 1,
273
+ });
274
+ result.byType.characters++;
275
+ }
276
+ }
277
+ }
278
+ // Check locations
279
+ if (!entityType || entityType === "location") {
280
+ const locations = db.prepare(`
281
+ SELECT id, name, image_gen FROM locations WHERE game_id = ?
282
+ `).all(gameId);
283
+ for (const loc of locations) {
284
+ if (!hasPrimaryImage(loc.id, "location")) {
285
+ result.entities.push({
286
+ id: loc.id,
287
+ name: loc.name,
288
+ entityType: "location",
289
+ hasImageGen: !!loc.image_gen,
290
+ });
291
+ result.byType.locations++;
292
+ }
293
+ }
294
+ }
295
+ // Check items
296
+ if (!entityType || entityType === "item") {
297
+ const items = db.prepare(`
298
+ SELECT id, name, image_gen FROM items WHERE game_id = ?
299
+ `).all(gameId);
300
+ for (const item of items) {
301
+ if (!hasPrimaryImage(item.id, "item")) {
302
+ result.entities.push({
303
+ id: item.id,
304
+ name: item.name,
305
+ entityType: "item",
306
+ hasImageGen: !!item.image_gen,
307
+ });
308
+ result.byType.items++;
309
+ }
310
+ }
311
+ }
312
+ // Check factions
313
+ if (!entityType || entityType === "faction") {
314
+ const factions = db.prepare(`
315
+ SELECT id, name FROM factions WHERE game_id = ?
316
+ `).all(gameId);
317
+ for (const faction of factions) {
318
+ if (!hasPrimaryImage(faction.id, "faction")) {
319
+ result.entities.push({
320
+ id: faction.id,
321
+ name: faction.name,
322
+ entityType: "faction",
323
+ hasImageGen: false, // Factions don't have imageGen column
324
+ });
325
+ result.byType.factions++;
326
+ }
327
+ }
328
+ }
329
+ result.totalMissing = result.entities.length;
330
+ return result;
331
+ }
332
+ export function deleteImage(imageId) {
333
+ const db = getDatabase();
334
+ const image = getImage(imageId);
335
+ if (!image)
336
+ return false;
337
+ // Delete file
338
+ const fullPath = join(getImagesDir(), image.filePath);
339
+ if (existsSync(fullPath)) {
340
+ unlinkSync(fullPath);
341
+ }
342
+ // Delete database record
343
+ const result = db.prepare(`DELETE FROM stored_images WHERE id = ?`).run(imageId);
344
+ return result.changes > 0;
345
+ }
346
+ export function setPrimaryImage(imageId) {
347
+ const image = getImage(imageId);
348
+ if (!image)
349
+ return null;
350
+ // Unsetting the old primary and setting the new one must happen
351
+ // together -- a failure between the two could otherwise leave the entity
352
+ // with zero primaries, or briefly with two.
353
+ withTransaction(() => {
354
+ const db = getDatabase();
355
+ // Unset current primary
356
+ db.prepare(`
357
+ UPDATE stored_images SET is_primary = 0
358
+ WHERE entity_id = ? AND entity_type = ?
359
+ `).run(image.entityId, image.entityType);
360
+ // Set new primary
361
+ db.prepare(`UPDATE stored_images SET is_primary = 1 WHERE id = ?`).run(imageId);
362
+ });
363
+ return { ...image, isPrimary: true };
364
+ }
365
+ export function updateImageMetadata(imageId, updates) {
366
+ const db = getDatabase();
367
+ const current = getImage(imageId);
368
+ if (!current)
369
+ return null;
370
+ const newLabel = updates.label !== undefined ? updates.label : current.label;
371
+ const newDescription = updates.description !== undefined ? updates.description : current.description;
372
+ // Handle entity re-association
373
+ let newEntityId = current.entityId;
374
+ let newEntityType = current.entityType;
375
+ let newFilePath = current.filePath;
376
+ if (updates.entityId !== undefined || updates.entityType !== undefined) {
377
+ newEntityId = updates.entityId ?? current.entityId;
378
+ newEntityType = updates.entityType ?? current.entityType;
379
+ // Validate new entity exists
380
+ validateEntityExists(newEntityId, newEntityType);
381
+ // If entity changed, move the file
382
+ if (newEntityId !== current.entityId || newEntityType !== current.entityType) {
383
+ const oldFullPath = join(getImagesDir(), current.filePath);
384
+ const ext = current.filePath.split(".").pop() || "png";
385
+ newFilePath = join(current.gameId, `${newEntityType}s`, newEntityId, `${imageId}.${ext}`);
386
+ const newFullPath = join(getImagesDir(), newFilePath);
387
+ // Ensure new directory exists
388
+ ensureDir(dirname(newFullPath));
389
+ // Move the file
390
+ if (existsSync(oldFullPath)) {
391
+ const fileData = readFileSync(oldFullPath);
392
+ writeFileSync(newFullPath, fileData);
393
+ unlinkSync(oldFullPath);
394
+ }
395
+ }
396
+ }
397
+ db.prepare(`
398
+ UPDATE stored_images
399
+ SET label = ?, description = ?, entity_id = ?, entity_type = ?, file_path = ?
400
+ WHERE id = ?
401
+ `).run(newLabel, newDescription, newEntityId, newEntityType, newFilePath, imageId);
402
+ return {
403
+ ...current,
404
+ label: newLabel,
405
+ description: newDescription,
406
+ entityId: newEntityId,
407
+ entityType: newEntityType,
408
+ filePath: newFilePath,
409
+ };
410
+ }
411
+ // Cleanup helper - delete all images for a game
412
+ export function deleteGameImages(gameId) {
413
+ const db = getDatabase();
414
+ // Delete files
415
+ const sessionDir = join(getImagesDir(), gameId);
416
+ if (existsSync(sessionDir)) {
417
+ rmSync(sessionDir, { recursive: true, force: true });
418
+ }
419
+ // Delete records (cascade should handle this, but be explicit)
420
+ const result = db
421
+ .prepare(`DELETE FROM stored_images WHERE game_id = ?`)
422
+ .run(gameId);
423
+ return result.changes;
424
+ }
425
+ // Get primary image for an entity
426
+ export function getPrimaryImage(entityId, entityType) {
427
+ const db = getDatabase();
428
+ const row = db
429
+ .prepare(`
430
+ SELECT * FROM stored_images
431
+ WHERE entity_id = ? AND entity_type = ? AND is_primary = 1
432
+ `)
433
+ .get(entityId, entityType);
434
+ if (!row)
435
+ return null;
436
+ return mapRowToStoredImage(row);
437
+ }
438
+ // List all images in a game
439
+ export function listGameImages(gameId) {
440
+ const db = getDatabase();
441
+ const rows = db
442
+ .prepare(`
443
+ SELECT * FROM stored_images
444
+ WHERE game_id = ?
445
+ ORDER BY created_at DESC
446
+ `)
447
+ .all(gameId);
448
+ return rows.map(mapRowToStoredImage);
449
+ }
@@ -0,0 +1,20 @@
1
+ import type { Item, ItemProperties, ImageGeneration } from "../types/index.js";
2
+ export declare function createItem(params: {
3
+ gameId: string;
4
+ ownerId: string;
5
+ ownerType: "character" | "location";
6
+ name: string;
7
+ properties?: Partial<ItemProperties>;
8
+ imageGen?: ImageGeneration;
9
+ }): Item;
10
+ export declare function getItem(id: string): Item | null;
11
+ export declare function updateItem(id: string, updates: {
12
+ name?: string;
13
+ properties?: Partial<ItemProperties>;
14
+ imageGen?: ImageGeneration | null;
15
+ }): Item | null;
16
+ export declare function deleteItem(id: string): boolean;
17
+ export declare function transferItem(itemId: string, newOwnerId: string, newOwnerType: "character" | "location"): Item | null;
18
+ export declare function getInventory(ownerId: string, ownerType: "character" | "location"): Item[];
19
+ export declare function findItemByName(gameId: string, name: string): Item | null;
20
+ export declare function listGameItems(gameId: string): Item[];
@@ -0,0 +1,145 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase } from "../db/connection.js";
3
+ import { safeJsonParse } from "../utils/json.js";
4
+ import { gameEvents } from "../events/emitter.js";
5
+ import { validateGameExists } from "./game.js";
6
+ export function createItem(params) {
7
+ // Validate game exists to prevent orphaned records
8
+ validateGameExists(params.gameId);
9
+ const db = getDatabase();
10
+ const id = uuidv4();
11
+ const properties = {
12
+ description: "",
13
+ type: "misc",
14
+ ...params.properties,
15
+ };
16
+ const stmt = db.prepare(`
17
+ INSERT INTO items (id, game_id, owner_id, owner_type, name, properties, image_gen)
18
+ VALUES (?, ?, ?, ?, ?, ?, ?)
19
+ `);
20
+ stmt.run(id, params.gameId, params.ownerId, params.ownerType, params.name, JSON.stringify(properties), params.imageGen ? JSON.stringify(params.imageGen) : null);
21
+ return {
22
+ id,
23
+ gameId: params.gameId,
24
+ ownerId: params.ownerId,
25
+ ownerType: params.ownerType,
26
+ name: params.name,
27
+ properties,
28
+ imageGen: params.imageGen || null,
29
+ };
30
+ }
31
+ export function getItem(id) {
32
+ const db = getDatabase();
33
+ const stmt = db.prepare(`SELECT * FROM items WHERE id = ?`);
34
+ const row = stmt.get(id);
35
+ if (!row)
36
+ return null;
37
+ return {
38
+ id: row.id,
39
+ gameId: row.game_id,
40
+ ownerId: row.owner_id,
41
+ ownerType: row.owner_type,
42
+ name: row.name,
43
+ properties: safeJsonParse(row.properties, { description: "", type: "misc" }),
44
+ imageGen: row.image_gen ? safeJsonParse(row.image_gen, null) : null,
45
+ };
46
+ }
47
+ export function updateItem(id, updates) {
48
+ const db = getDatabase();
49
+ const current = getItem(id);
50
+ if (!current)
51
+ return null;
52
+ const newName = updates.name ?? current.name;
53
+ const newProperties = updates.properties
54
+ ? { ...current.properties, ...updates.properties }
55
+ : current.properties;
56
+ const newImageGen = updates.imageGen !== undefined ? updates.imageGen : current.imageGen;
57
+ const stmt = db.prepare(`
58
+ UPDATE items SET name = ?, properties = ?, image_gen = ? WHERE id = ?
59
+ `);
60
+ stmt.run(newName, JSON.stringify(newProperties), newImageGen ? JSON.stringify(newImageGen) : null, id);
61
+ return {
62
+ ...current,
63
+ name: newName,
64
+ properties: newProperties,
65
+ imageGen: newImageGen,
66
+ };
67
+ }
68
+ export function deleteItem(id) {
69
+ const db = getDatabase();
70
+ const stmt = db.prepare(`DELETE FROM items WHERE id = ?`);
71
+ const result = stmt.run(id);
72
+ return result.changes > 0;
73
+ }
74
+ export function transferItem(itemId, newOwnerId, newOwnerType) {
75
+ const db = getDatabase();
76
+ const item = getItem(itemId);
77
+ if (!item)
78
+ return null;
79
+ const stmt = db.prepare(`
80
+ UPDATE items SET owner_id = ?, owner_type = ? WHERE id = ?
81
+ `);
82
+ stmt.run(newOwnerId, newOwnerType, itemId);
83
+ // Emit realtime event
84
+ gameEvents.emit({
85
+ type: "inventory:updated",
86
+ gameId: item.gameId,
87
+ entityId: itemId,
88
+ entityType: "item",
89
+ timestamp: new Date().toISOString(),
90
+ data: { name: item.name, newOwnerId, newOwnerType },
91
+ });
92
+ return {
93
+ ...item,
94
+ ownerId: newOwnerId,
95
+ ownerType: newOwnerType,
96
+ };
97
+ }
98
+ export function getInventory(ownerId, ownerType) {
99
+ const db = getDatabase();
100
+ const stmt = db.prepare(`
101
+ SELECT * FROM items WHERE owner_id = ? AND owner_type = ?
102
+ `);
103
+ const rows = stmt.all(ownerId, ownerType);
104
+ return rows.map((row) => ({
105
+ id: row.id,
106
+ gameId: row.game_id,
107
+ ownerId: row.owner_id,
108
+ ownerType: row.owner_type,
109
+ name: row.name,
110
+ properties: safeJsonParse(row.properties, { description: "", type: "misc" }),
111
+ imageGen: row.image_gen ? safeJsonParse(row.image_gen, null) : null,
112
+ }));
113
+ }
114
+ export function findItemByName(gameId, name) {
115
+ const db = getDatabase();
116
+ const stmt = db.prepare(`
117
+ SELECT * FROM items WHERE game_id = ? AND name = ? LIMIT 1
118
+ `);
119
+ const row = stmt.get(gameId, name);
120
+ if (!row)
121
+ return null;
122
+ return {
123
+ id: row.id,
124
+ gameId: row.game_id,
125
+ ownerId: row.owner_id,
126
+ ownerType: row.owner_type,
127
+ name: row.name,
128
+ properties: safeJsonParse(row.properties, { description: "", type: "misc" }),
129
+ imageGen: row.image_gen ? safeJsonParse(row.image_gen, null) : null,
130
+ };
131
+ }
132
+ export function listGameItems(gameId) {
133
+ const db = getDatabase();
134
+ const stmt = db.prepare(`SELECT * FROM items WHERE game_id = ?`);
135
+ const rows = stmt.all(gameId);
136
+ return rows.map((row) => ({
137
+ id: row.id,
138
+ gameId: row.game_id,
139
+ ownerId: row.owner_id,
140
+ ownerType: row.owner_type,
141
+ name: row.name,
142
+ properties: safeJsonParse(row.properties, { description: "", type: "misc" }),
143
+ imageGen: row.image_gen ? safeJsonParse(row.image_gen, null) : null,
144
+ }));
145
+ }
@@ -0,0 +1,58 @@
1
+ import type { NarrativeEvent } from "../types/index.js";
2
+ export declare function logEvent(params: {
3
+ gameId: string;
4
+ eventType: string;
5
+ content: string;
6
+ metadata?: Record<string, unknown>;
7
+ }): NarrativeEvent;
8
+ export declare function getEvent(id: string): NarrativeEvent | null;
9
+ export declare function getHistory(gameId: string, options?: {
10
+ limit?: number;
11
+ offset?: number;
12
+ eventType?: string;
13
+ since?: string;
14
+ }): NarrativeEvent[];
15
+ export declare function getRecentHistory(gameId: string, count?: number): NarrativeEvent[];
16
+ export declare function getSummary(gameId: string): {
17
+ totalEvents: number;
18
+ eventTypes: Record<string, number>;
19
+ firstEvent: string | null;
20
+ lastEvent: string | null;
21
+ recentEvents: NarrativeEvent[];
22
+ };
23
+ export declare function deleteEvent(id: string): boolean;
24
+ export declare function clearHistory(gameId: string): number;
25
+ export interface StoryExportData {
26
+ game: {
27
+ name: string;
28
+ setting: string;
29
+ style: string;
30
+ createdAt: string;
31
+ };
32
+ characters: Array<{
33
+ name: string;
34
+ isPlayer: boolean;
35
+ notes: string;
36
+ }>;
37
+ locations: Array<{
38
+ name: string;
39
+ description: string;
40
+ }>;
41
+ quests: Array<{
42
+ name: string;
43
+ description: string;
44
+ status: string;
45
+ objectives: Array<{
46
+ description: string;
47
+ completed: boolean;
48
+ }>;
49
+ }>;
50
+ chapters: Array<{
51
+ title: string;
52
+ events: NarrativeEvent[];
53
+ }>;
54
+ totalEvents: number;
55
+ exportStyle: string;
56
+ instruction: string;
57
+ }
58
+ export declare function exportStoryData(gameId: string, style: string): StoryExportData | null;