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.
- package/LICENSE +22 -0
- package/README.md +79 -0
- package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
- package/dist/__tests__/engineVocabulary.test.js +147 -0
- package/dist/db/__tests__/connection.test.d.ts +1 -0
- package/dist/db/__tests__/connection.test.js +72 -0
- package/dist/db/__tests__/testDb.d.ts +33 -0
- package/dist/db/__tests__/testDb.js +41 -0
- package/dist/db/connection.d.ts +22 -0
- package/dist/db/connection.js +107 -0
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.js +725 -0
- package/dist/events/emitter.d.ts +22 -0
- package/dist/events/emitter.js +71 -0
- package/dist/http/server.d.ts +3 -0
- package/dist/http/server.js +649 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -0
- package/dist/register/abilities.d.ts +2 -0
- package/dist/register/abilities.js +165 -0
- package/dist/register/audio.d.ts +2 -0
- package/dist/register/audio.js +326 -0
- package/dist/register/batch.d.ts +2 -0
- package/dist/register/batch.js +343 -0
- package/dist/register/character.d.ts +2 -0
- package/dist/register/character.js +324 -0
- package/dist/register/combat.d.ts +2 -0
- package/dist/register/combat.js +207 -0
- package/dist/register/core.d.ts +2 -0
- package/dist/register/core.js +1040 -0
- package/dist/register/display.d.ts +2 -0
- package/dist/register/display.js +263 -0
- package/dist/register/factions.d.ts +2 -0
- package/dist/register/factions.js +186 -0
- package/dist/register/images.d.ts +2 -0
- package/dist/register/images.js +400 -0
- package/dist/register/inventory.d.ts +2 -0
- package/dist/register/inventory.js +115 -0
- package/dist/register/mcp-prompts.d.ts +2 -0
- package/dist/register/mcp-prompts.js +684 -0
- package/dist/register/mcp-resources.d.ts +2 -0
- package/dist/register/mcp-resources.js +335 -0
- package/dist/register/narrative.d.ts +2 -0
- package/dist/register/narrative.js +242 -0
- package/dist/register/notes.d.ts +2 -0
- package/dist/register/notes.js +170 -0
- package/dist/register/pause.d.ts +2 -0
- package/dist/register/pause.js +580 -0
- package/dist/register/quests.d.ts +2 -0
- package/dist/register/quests.js +118 -0
- package/dist/register/relationships.d.ts +2 -0
- package/dist/register/relationships.js +147 -0
- package/dist/register/resources.d.ts +2 -0
- package/dist/register/resources.js +277 -0
- package/dist/register/secrets.d.ts +2 -0
- package/dist/register/secrets.js +192 -0
- package/dist/register/status.d.ts +2 -0
- package/dist/register/status.js +130 -0
- package/dist/register/tables.d.ts +2 -0
- package/dist/register/tables.js +146 -0
- package/dist/register/tags.d.ts +2 -0
- package/dist/register/tags.js +114 -0
- package/dist/register/time.d.ts +2 -0
- package/dist/register/time.js +281 -0
- package/dist/register/world.d.ts +2 -0
- package/dist/register/world.js +127 -0
- package/dist/schemas/index.d.ts +921 -0
- package/dist/schemas/index.js +121 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/tools/__tests__/audio.test.d.ts +1 -0
- package/dist/tools/__tests__/audio.test.js +59 -0
- package/dist/tools/__tests__/conserved.test.d.ts +1 -0
- package/dist/tools/__tests__/conserved.test.js +488 -0
- package/dist/tools/__tests__/constraint.test.d.ts +1 -0
- package/dist/tools/__tests__/constraint.test.js +212 -0
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
- package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
- package/dist/tools/__tests__/images.test.d.ts +1 -0
- package/dist/tools/__tests__/images.test.js +59 -0
- package/dist/tools/__tests__/relationship.test.d.ts +1 -0
- package/dist/tools/__tests__/relationship.test.js +132 -0
- package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
- package/dist/tools/__tests__/resource-constraints.test.js +131 -0
- package/dist/tools/__tests__/resource.test.d.ts +1 -0
- package/dist/tools/__tests__/resource.test.js +190 -0
- package/dist/tools/__tests__/time.test.d.ts +1 -0
- package/dist/tools/__tests__/time.test.js +404 -0
- package/dist/tools/__tests__/timers.test.d.ts +1 -0
- package/dist/tools/__tests__/timers.test.js +426 -0
- package/dist/tools/__tests__/world.test.d.ts +1 -0
- package/dist/tools/__tests__/world.test.js +70 -0
- package/dist/tools/ability.d.ts +48 -0
- package/dist/tools/ability.js +238 -0
- package/dist/tools/audio.d.ts +24 -0
- package/dist/tools/audio.js +365 -0
- package/dist/tools/character.d.ts +70 -0
- package/dist/tools/character.js +309 -0
- package/dist/tools/combat.d.ts +13 -0
- package/dist/tools/combat.js +195 -0
- package/dist/tools/constraint.d.ts +132 -0
- package/dist/tools/constraint.js +269 -0
- package/dist/tools/dice.d.ts +23 -0
- package/dist/tools/dice.js +111 -0
- package/dist/tools/display.d.ts +120 -0
- package/dist/tools/display.js +528 -0
- package/dist/tools/faction.d.ts +61 -0
- package/dist/tools/faction.js +269 -0
- package/dist/tools/game.d.ts +96 -0
- package/dist/tools/game.js +526 -0
- package/dist/tools/image-prompt.d.ts +49 -0
- package/dist/tools/image-prompt.js +479 -0
- package/dist/tools/images.d.ts +47 -0
- package/dist/tools/images.js +449 -0
- package/dist/tools/inventory.d.ts +20 -0
- package/dist/tools/inventory.js +145 -0
- package/dist/tools/narrative.d.ts +58 -0
- package/dist/tools/narrative.js +237 -0
- package/dist/tools/notes.d.ts +41 -0
- package/dist/tools/notes.js +220 -0
- package/dist/tools/pause.d.ts +110 -0
- package/dist/tools/pause.js +1254 -0
- package/dist/tools/quest.d.ts +34 -0
- package/dist/tools/quest.js +164 -0
- package/dist/tools/relationship.d.ts +74 -0
- package/dist/tools/relationship.js +324 -0
- package/dist/tools/resource.d.ts +93 -0
- package/dist/tools/resource.js +374 -0
- package/dist/tools/rules.d.ts +4 -0
- package/dist/tools/rules.js +30 -0
- package/dist/tools/secrets.d.ts +49 -0
- package/dist/tools/secrets.js +195 -0
- package/dist/tools/status.d.ts +36 -0
- package/dist/tools/status.js +218 -0
- package/dist/tools/tables.d.ts +33 -0
- package/dist/tools/tables.js +209 -0
- package/dist/tools/tags.d.ts +52 -0
- package/dist/tools/tags.js +176 -0
- package/dist/tools/time.d.ts +33 -0
- package/dist/tools/time.js +276 -0
- package/dist/tools/timers.d.ts +41 -0
- package/dist/tools/timers.js +215 -0
- package/dist/tools/world.d.ts +78 -0
- package/dist/tools/world.js +331 -0
- package/dist/types/index.d.ts +969 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/__tests__/json.test.d.ts +1 -0
- package/dist/utils/__tests__/json.test.js +55 -0
- package/dist/utils/__tests__/validation.test.d.ts +1 -0
- package/dist/utils/__tests__/validation.test.js +90 -0
- package/dist/utils/errors.d.ts +44 -0
- package/dist/utils/errors.js +121 -0
- package/dist/utils/json.d.ts +9 -0
- package/dist/utils/json.js +23 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +50 -0
- package/dist/utils/output-schemas.d.ts +594 -0
- package/dist/utils/output-schemas.js +331 -0
- package/dist/utils/tool-annotations.d.ts +147 -0
- package/dist/utils/tool-annotations.js +98 -0
- package/dist/utils/validation.d.ts +34 -0
- package/dist/utils/validation.js +52 -0
- package/dist/utils/verbosity.d.ts +57 -0
- package/dist/utils/verbosity.js +67 -0
- package/dist/utils/webui.d.ts +20 -0
- package/dist/utils/webui.js +35 -0
- 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;
|