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,649 @@
1
+ import express from "express";
2
+ import { join } from "path";
3
+ import { existsSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import { dirname } from "path";
6
+ // Import tool functions
7
+ import { listGames, loadGame, listImageGenerationPresets, getImageGenerationPreset, getDefaultImagePreset } from "../tools/game.js";
8
+ import { getCharacter, listCharacters, renderCharacterSheet, } from "../tools/character.js";
9
+ import { getLocation, listLocations, renderMap } from "../tools/world.js";
10
+ import { getImage, getImageData, listEntityImages, listGameImages, } from "../tools/images.js";
11
+ import { getInventory, getItem, listGameItems } from "../tools/inventory.js";
12
+ import { listQuests, getQuest } from "../tools/quest.js";
13
+ import { getHistory } from "../tools/narrative.js";
14
+ import { getDisplayConfig, getGameDisplayConfig, hasGameTheme, } from "../tools/display.js";
15
+ import { listFactions, getFaction } from "../tools/faction.js";
16
+ import { listResources, getResource } from "../tools/resource.js";
17
+ import { listNotes, getNote } from "../tools/notes.js";
18
+ import { listRelationships } from "../tools/relationship.js";
19
+ import { listAbilities, getAbility } from "../tools/ability.js";
20
+ import { listTimers, getTimer } from "../tools/timers.js";
21
+ import { listSecrets } from "../tools/secrets.js";
22
+ import { getTime } from "../tools/time.js";
23
+ import { getActiveCombat } from "../tools/combat.js";
24
+ import { gameEvents } from "../events/emitter.js";
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = dirname(__filename);
27
+ // Path to Vue app build
28
+ const CLIENT_DIST = join(__dirname, "..", "..", "client", "dist");
29
+ export function createHttpServer(_port = 3456) {
30
+ const app = express();
31
+ app.use(express.json());
32
+ // ============================================================================
33
+ // API ROUTES (JSON)
34
+ // ============================================================================
35
+ // Theme/Display config endpoint (global)
36
+ app.get("/api/theme", (_req, res) => {
37
+ res.json(getDisplayConfig());
38
+ });
39
+ // Per-game theme endpoint
40
+ app.get("/api/games/:gameId/theme", (req, res) => {
41
+ res.json(getGameDisplayConfig(req.params.gameId));
42
+ });
43
+ // Image generation presets
44
+ app.get("/api/games/:gameId/image-presets", (req, res) => {
45
+ const presets = listImageGenerationPresets(req.params.gameId);
46
+ const defaultPreset = getDefaultImagePreset(req.params.gameId);
47
+ res.json({
48
+ presets,
49
+ defaultPresetId: defaultPreset?.id || null,
50
+ });
51
+ });
52
+ app.get("/api/games/:gameId/image-presets/:presetId", (req, res) => {
53
+ const preset = getImageGenerationPreset(req.params.gameId, req.params.presetId);
54
+ if (!preset) {
55
+ res.status(404).json({ error: "Preset not found" });
56
+ return;
57
+ }
58
+ res.json(preset);
59
+ });
60
+ // Games - include theme data for each game
61
+ app.get("/api/games", (_req, res) => {
62
+ const games = listGames();
63
+ const gamesWithThemes = games.map(game => ({
64
+ ...game,
65
+ theme: hasGameTheme(game.id) ? getGameDisplayConfig(game.id) : null,
66
+ }));
67
+ res.json(gamesWithThemes);
68
+ });
69
+ app.get("/api/games/:gameId", (req, res) => {
70
+ const gameId = req.params.gameId;
71
+ const game = loadGame(gameId);
72
+ if (!game) {
73
+ res.status(404).json({ error: "Game not found" });
74
+ return;
75
+ }
76
+ const characters = listCharacters(gameId);
77
+ const locations = listLocations(gameId);
78
+ const quests = listQuests(gameId);
79
+ // Add primary image IDs to characters and locations
80
+ const charactersWithImages = characters.map((c) => {
81
+ const result = listEntityImages(c.id, "character");
82
+ const primary = result.primaryImage || result.images[0];
83
+ return { ...c, primaryImageId: primary?.id || null };
84
+ });
85
+ const locationsWithImages = locations.map((l) => {
86
+ const result = listEntityImages(l.id, "location");
87
+ const primary = result.primaryImage || result.images[0];
88
+ return { ...l, primaryImageId: primary?.id || null };
89
+ });
90
+ // Get counts for all entity types (for conditional UI display)
91
+ const factions = listFactions(gameId);
92
+ // Add primary image IDs to factions
93
+ const factionsWithImages = factions.map((f) => {
94
+ const result = listEntityImages(f.id, "faction");
95
+ const primary = result.primaryImage || result.images[0];
96
+ return { ...f, primaryImageId: primary?.id || null };
97
+ });
98
+ // Add primary image IDs to quests
99
+ const questsWithImages = quests.map((q) => {
100
+ const result = listEntityImages(q.id, "quest");
101
+ const primary = result.primaryImage || result.images[0];
102
+ return { ...q, primaryImageId: primary?.id || null };
103
+ });
104
+ const resources = listResources(gameId);
105
+ // Add primary image IDs to resources
106
+ const resourcesWithImages = resources.map((r) => {
107
+ const result = listEntityImages(r.id, "resource");
108
+ const primary = result.primaryImage || result.images[0];
109
+ return { ...r, primaryImageId: primary?.id || null };
110
+ });
111
+ const notes = listNotes(gameId);
112
+ // Add primary image IDs to notes
113
+ const notesWithImages = notes.map((n) => {
114
+ const result = listEntityImages(n.id, "note");
115
+ const primary = result.primaryImage || result.images[0];
116
+ return { ...n, primaryImageId: primary?.id || null };
117
+ });
118
+ const relationships = listRelationships(gameId);
119
+ const abilities = listAbilities(gameId);
120
+ const timers = listTimers(gameId);
121
+ const secrets = listSecrets(gameId);
122
+ const images = listGameImages(gameId);
123
+ const items = listGameItems(gameId);
124
+ // Add primary image IDs to items
125
+ const itemsWithImages = items.map((i) => {
126
+ const result = listEntityImages(i.id, "item");
127
+ const primary = result.primaryImage || result.images[0];
128
+ return { ...i, primaryImageId: primary?.id || null };
129
+ });
130
+ const events = getHistory(gameId, { limit: 1 });
131
+ const activeCombat = getActiveCombat(gameId);
132
+ const gameTime = getTime(gameId);
133
+ res.json({
134
+ game,
135
+ characters: charactersWithImages,
136
+ locations: locationsWithImages,
137
+ quests: questsWithImages,
138
+ // Include full data for populated entity types
139
+ factions: factionsWithImages,
140
+ resources: resourcesWithImages,
141
+ notes: notesWithImages,
142
+ items: itemsWithImages,
143
+ // Include counts for UI tab visibility
144
+ counts: {
145
+ characters: characters.length,
146
+ locations: locations.length,
147
+ quests: quests.length,
148
+ factions: factions.length,
149
+ resources: resources.length,
150
+ notes: notes.length,
151
+ relationships: relationships.length,
152
+ abilities: abilities.length,
153
+ timers: timers.length,
154
+ secrets: secrets.length,
155
+ images: images.length,
156
+ items: items.length,
157
+ events: events.length > 0 ? 1 : 0, // Just indicate if there's history
158
+ },
159
+ // Include active state info
160
+ activeCombat: activeCombat || null,
161
+ gameTime: gameTime || null,
162
+ });
163
+ });
164
+ app.get("/api/games/:gameId/map", (req, res) => {
165
+ const characters = listCharacters(req.params.gameId);
166
+ const playerChar = characters.find((c) => c.isPlayer);
167
+ const mapData = renderMap(req.params.gameId, {
168
+ playerLocationId: playerChar?.locationId || undefined,
169
+ });
170
+ if (!mapData) {
171
+ res.status(404).json({ error: "No map data" });
172
+ return;
173
+ }
174
+ res.json(mapData);
175
+ });
176
+ app.get("/api/games/:gameId/history", (req, res) => {
177
+ const limit = req.query.limit ? parseInt(req.query.limit) : 50;
178
+ const history = getHistory(req.params.gameId, { limit });
179
+ res.json(history);
180
+ });
181
+ app.get("/api/games/:gameId/images", (req, res) => {
182
+ const images = listGameImages(req.params.gameId);
183
+ res.json(images);
184
+ });
185
+ app.get("/api/games/:gameId/characters", (req, res) => {
186
+ const locationId = req.query.locationId;
187
+ const characters = listCharacters(req.params.gameId, { locationId });
188
+ res.json(characters);
189
+ });
190
+ // Characters
191
+ app.get("/api/characters/:characterId", (req, res) => {
192
+ const char = getCharacter(req.params.characterId);
193
+ if (!char) {
194
+ res.status(404).json({ error: "Character not found" });
195
+ return;
196
+ }
197
+ res.json(char);
198
+ });
199
+ app.get("/api/characters/:characterId/sheet", (req, res) => {
200
+ const sheet = renderCharacterSheet(req.params.characterId);
201
+ if (!sheet) {
202
+ res.status(404).json({ error: "Character not found" });
203
+ return;
204
+ }
205
+ res.json(sheet);
206
+ });
207
+ // Locations
208
+ app.get("/api/locations/:locationId", (req, res) => {
209
+ const loc = getLocation(req.params.locationId);
210
+ if (!loc) {
211
+ res.status(404).json({ error: "Location not found" });
212
+ return;
213
+ }
214
+ res.json(loc);
215
+ });
216
+ // Quests
217
+ app.get("/api/quests/:questId", (req, res) => {
218
+ const quest = getQuest(req.params.questId);
219
+ if (!quest) {
220
+ res.status(404).json({ error: "Quest not found" });
221
+ return;
222
+ }
223
+ res.json(quest);
224
+ });
225
+ // Factions
226
+ app.get("/api/factions/:factionId", (req, res) => {
227
+ const faction = getFaction(req.params.factionId);
228
+ if (!faction) {
229
+ res.status(404).json({ error: "Faction not found" });
230
+ return;
231
+ }
232
+ res.json(faction);
233
+ });
234
+ // Resources
235
+ app.get("/api/resources/:resourceId", (req, res) => {
236
+ const resource = getResource(req.params.resourceId);
237
+ if (!resource) {
238
+ res.status(404).json({ error: "Resource not found" });
239
+ return;
240
+ }
241
+ res.json(resource);
242
+ });
243
+ // Notes
244
+ app.get("/api/notes/:noteId", (req, res) => {
245
+ const note = getNote(req.params.noteId);
246
+ if (!note) {
247
+ res.status(404).json({ error: "Note not found" });
248
+ return;
249
+ }
250
+ res.json(note);
251
+ });
252
+ // Abilities
253
+ app.get("/api/abilities/:abilityId", (req, res) => {
254
+ const ability = getAbility(req.params.abilityId);
255
+ if (!ability) {
256
+ res.status(404).json({ error: "Ability not found" });
257
+ return;
258
+ }
259
+ res.json(ability);
260
+ });
261
+ // Timers
262
+ app.get("/api/timers/:timerId", (req, res) => {
263
+ const timer = getTimer(req.params.timerId);
264
+ if (!timer) {
265
+ res.status(404).json({ error: "Timer not found" });
266
+ return;
267
+ }
268
+ res.json(timer);
269
+ });
270
+ // Relationships (for graph visualization)
271
+ app.get("/api/games/:gameId/relationships", (req, res) => {
272
+ const { gameId } = req.params;
273
+ const relationships = listRelationships(gameId);
274
+ const characters = listCharacters(gameId);
275
+ const factions = listFactions(gameId);
276
+ // Build lookup maps for entity names and images
277
+ const entityInfo = {};
278
+ characters.forEach((c) => {
279
+ const result = listEntityImages(c.id, "character");
280
+ const primary = result.primaryImage || result.images[0];
281
+ entityInfo[c.id] = { name: c.name, type: "character", imageId: primary?.id || null };
282
+ });
283
+ factions.forEach((f) => {
284
+ const result = listEntityImages(f.id, "faction");
285
+ const primary = result.primaryImage || result.images[0];
286
+ entityInfo[f.id] = { name: f.name, type: "faction", imageId: primary?.id || null };
287
+ });
288
+ // Enrich relationships with entity names and images
289
+ const enrichedRelationships = relationships.map((r) => ({
290
+ ...r,
291
+ sourceName: entityInfo[r.sourceId]?.name || r.sourceId,
292
+ targetName: entityInfo[r.targetId]?.name || r.targetId,
293
+ sourceImageId: entityInfo[r.sourceId]?.imageId || null,
294
+ targetImageId: entityInfo[r.targetId]?.imageId || null,
295
+ }));
296
+ res.json(enrichedRelationships);
297
+ });
298
+ // Images
299
+ app.get("/api/images/:imageId", (req, res) => {
300
+ const image = getImage(req.params.imageId);
301
+ if (!image) {
302
+ res.status(404).json({ error: "Image not found" });
303
+ return;
304
+ }
305
+ res.json(image);
306
+ });
307
+ // Entity images
308
+ app.get("/api/entities/:entityType/:entityId/images", (req, res) => {
309
+ const images = listEntityImages(req.params.entityId, req.params.entityType);
310
+ res.json(images);
311
+ });
312
+ // Items
313
+ app.get("/api/items/:itemId", (req, res) => {
314
+ const item = getItem(req.params.itemId);
315
+ if (!item) {
316
+ res.status(404).json({ error: "Item not found" });
317
+ return;
318
+ }
319
+ res.json(item);
320
+ });
321
+ // Inventory
322
+ app.get("/api/inventory/:ownerType/:ownerId", (req, res) => {
323
+ const items = getInventory(req.params.ownerId, req.params.ownerType);
324
+ res.json(items);
325
+ });
326
+ // Search within a game - full-text search across all entity types
327
+ app.get("/api/games/:gameId/search", (req, res) => {
328
+ const { gameId } = req.params;
329
+ const query = (req.query.q || "").toLowerCase().trim();
330
+ if (!query || query.length < 2) {
331
+ res.json({ characters: [], locations: [], quests: [], items: [], factions: [], notes: [], events: [] });
332
+ return;
333
+ }
334
+ const game = loadGame(gameId);
335
+ if (!game) {
336
+ res.status(404).json({ error: "Game not found" });
337
+ return;
338
+ }
339
+ // Helper to extract a snippet around the match
340
+ const getSnippet = (text, maxLen = 80) => {
341
+ if (!text)
342
+ return "";
343
+ const lowerText = text.toLowerCase();
344
+ const idx = lowerText.indexOf(query);
345
+ if (idx === -1)
346
+ return text.slice(0, maxLen) + (text.length > maxLen ? "..." : "");
347
+ const start = Math.max(0, idx - 20);
348
+ const end = Math.min(text.length, idx + query.length + 60);
349
+ let snippet = text.slice(start, end);
350
+ if (start > 0)
351
+ snippet = "..." + snippet;
352
+ if (end < text.length)
353
+ snippet = snippet + "...";
354
+ return snippet;
355
+ };
356
+ // Helper to check if text matches query
357
+ const matches = (text) => {
358
+ return !!text && text.toLowerCase().includes(query);
359
+ };
360
+ const characters = listCharacters(gameId);
361
+ const locations = listLocations(gameId);
362
+ const quests = listQuests(gameId);
363
+ const items = listGameItems(gameId);
364
+ const factions = listFactions(gameId);
365
+ const notes = listNotes(gameId);
366
+ const events = getHistory(gameId, { limit: 100 }); // Search recent events
367
+ // Search characters by name and notes
368
+ const matchingCharacters = characters
369
+ .filter((c) => matches(c.name) || matches(c.notes))
370
+ .slice(0, 5)
371
+ .map((c) => {
372
+ const result = listEntityImages(c.id, "character");
373
+ const primary = result.primaryImage || result.images[0];
374
+ const matchField = matches(c.name) ? "name" : "notes";
375
+ return {
376
+ id: c.id,
377
+ name: c.name,
378
+ type: "character",
379
+ isPlayer: c.isPlayer,
380
+ primaryImageId: primary?.id || null,
381
+ matchField,
382
+ snippet: matchField === "notes" ? getSnippet(c.notes) : null,
383
+ };
384
+ });
385
+ // Search locations by name and description
386
+ const matchingLocations = locations
387
+ .filter((l) => matches(l.name) || matches(l.description))
388
+ .slice(0, 5)
389
+ .map((l) => {
390
+ const result = listEntityImages(l.id, "location");
391
+ const primary = result.primaryImage || result.images[0];
392
+ const matchField = matches(l.name) ? "name" : "description";
393
+ return {
394
+ id: l.id,
395
+ name: l.name,
396
+ type: "location",
397
+ primaryImageId: primary?.id || null,
398
+ matchField,
399
+ snippet: matchField === "description" ? getSnippet(l.description) : null,
400
+ };
401
+ });
402
+ const matchingQuests = quests
403
+ .filter((q) => matches(q.name) || matches(q.description))
404
+ .slice(0, 5)
405
+ .map((q) => {
406
+ const result = listEntityImages(q.id, "quest");
407
+ const primary = result.primaryImage || result.images[0];
408
+ const matchField = matches(q.name) ? "name" : "description";
409
+ return {
410
+ id: q.id,
411
+ name: q.name,
412
+ type: "quest",
413
+ status: q.status,
414
+ primaryImageId: primary?.id || null,
415
+ matchField,
416
+ snippet: matchField === "description" ? getSnippet(q.description || "") : null,
417
+ };
418
+ });
419
+ const matchingItems = items
420
+ .filter((i) => matches(i.name) || matches(i.properties?.description))
421
+ .slice(0, 5)
422
+ .map((i) => {
423
+ const result = listEntityImages(i.id, "item");
424
+ const primary = result.primaryImage || result.images[0];
425
+ const matchField = matches(i.name) ? "name" : "description";
426
+ return {
427
+ id: i.id,
428
+ name: i.name,
429
+ type: "item",
430
+ primaryImageId: primary?.id || null,
431
+ matchField,
432
+ snippet: matchField === "description" ? getSnippet(i.properties?.description || "") : null,
433
+ };
434
+ });
435
+ // Search factions by name and description
436
+ const matchingFactions = factions
437
+ .filter((f) => matches(f.name) || matches(f.description))
438
+ .slice(0, 5)
439
+ .map((f) => {
440
+ const result = listEntityImages(f.id, "faction");
441
+ const primary = result.primaryImage || result.images[0];
442
+ const matchField = matches(f.name) ? "name" : "description";
443
+ return {
444
+ id: f.id,
445
+ name: f.name,
446
+ type: "faction",
447
+ status: f.status,
448
+ primaryImageId: primary?.id || null,
449
+ matchField,
450
+ snippet: matchField === "description" ? getSnippet(f.description || "") : null,
451
+ };
452
+ });
453
+ const matchingNotes = notes
454
+ .filter((n) => matches(n.title) || matches(n.content))
455
+ .slice(0, 5)
456
+ .map((n) => {
457
+ const result = listEntityImages(n.id, "note");
458
+ const primary = result.primaryImage || result.images[0];
459
+ const matchField = matches(n.title) ? "title" : "content";
460
+ return {
461
+ id: n.id,
462
+ name: n.title,
463
+ type: "note",
464
+ category: n.category || undefined,
465
+ primaryImageId: primary?.id || null,
466
+ matchField,
467
+ snippet: matchField === "content" ? getSnippet(n.content) : null,
468
+ };
469
+ });
470
+ const matchingEvents = events
471
+ .filter((e) => matches(e.content) || matches(e.eventType))
472
+ .slice(0, 5)
473
+ .map((e) => ({
474
+ id: e.id,
475
+ name: e.eventType,
476
+ type: "event",
477
+ eventType: e.eventType,
478
+ timestamp: e.timestamp,
479
+ matchField: "content",
480
+ snippet: getSnippet(e.content),
481
+ }));
482
+ res.json({
483
+ characters: matchingCharacters,
484
+ locations: matchingLocations,
485
+ quests: matchingQuests,
486
+ items: matchingItems,
487
+ factions: matchingFactions,
488
+ notes: matchingNotes,
489
+ events: matchingEvents,
490
+ });
491
+ });
492
+ // ============================================================================
493
+ // SERVER-SENT EVENTS (SSE) - Realtime Updates
494
+ // ============================================================================
495
+ app.get("/api/games/:gameId/subscribe", (req, res) => {
496
+ const { gameId } = req.params;
497
+ // Verify game exists
498
+ const game = loadGame(gameId);
499
+ if (!game) {
500
+ res.status(404).json({ error: "Game not found" });
501
+ return;
502
+ }
503
+ // Set SSE headers
504
+ res.setHeader("Content-Type", "text/event-stream");
505
+ res.setHeader("Cache-Control", "no-cache");
506
+ res.setHeader("Connection", "keep-alive");
507
+ res.setHeader("X-Accel-Buffering", "no"); // Disable nginx buffering
508
+ res.flushHeaders();
509
+ // Subscribe this client to game events
510
+ gameEvents.subscribe(gameId, res);
511
+ // Handle client disconnect
512
+ req.on("close", () => {
513
+ gameEvents.unsubscribe(gameId, res);
514
+ });
515
+ });
516
+ // ============================================================================
517
+ // IMAGE FILE ROUTES
518
+ // ============================================================================
519
+ // Game favicon - serves the game's favicon image
520
+ app.get("/api/games/:gameId/favicon", async (req, res) => {
521
+ const { gameId } = req.params;
522
+ const game = loadGame(gameId);
523
+ if (!game || !game.faviconImageId) {
524
+ res.status(404).send("No favicon set for this game");
525
+ return;
526
+ }
527
+ try {
528
+ // Serve the favicon image resized (32x32 is standard, 64 for retina)
529
+ const size = req.query.size ? parseInt(req.query.size) : 32;
530
+ const result = await getImageData(game.faviconImageId, {
531
+ width: size,
532
+ height: size,
533
+ format: "png",
534
+ });
535
+ if (!result) {
536
+ res.status(404).send("Favicon image not found");
537
+ return;
538
+ }
539
+ const base64Data = result.base64.replace(/^data:[^;]+;base64,/, "");
540
+ const buffer = Buffer.from(base64Data, "base64");
541
+ res.set("Content-Type", "image/png");
542
+ res.set("Cache-Control", "public, max-age=3600"); // Cache for 1 hour
543
+ res.send(buffer);
544
+ }
545
+ catch {
546
+ res.status(500).send("Error processing favicon");
547
+ }
548
+ });
549
+ app.get("/images/:imageId/file", async (req, res) => {
550
+ const { imageId } = req.params;
551
+ const width = req.query.width ? parseInt(req.query.width) : undefined;
552
+ const height = req.query.height
553
+ ? parseInt(req.query.height)
554
+ : undefined;
555
+ const format = req.query.format;
556
+ try {
557
+ const result = await getImageData(imageId, { width, height, format });
558
+ if (!result) {
559
+ res.status(404).send("Image not found");
560
+ return;
561
+ }
562
+ const base64Data = result.base64.replace(/^data:[^;]+;base64,/, "");
563
+ const buffer = Buffer.from(base64Data, "base64");
564
+ res.set("Content-Type", result.outputFormat);
565
+ res.set("Cache-Control", "public, max-age=86400");
566
+ res.send(buffer);
567
+ }
568
+ catch {
569
+ res.status(500).send("Error processing image");
570
+ }
571
+ });
572
+ // ============================================================================
573
+ // VUE APP (SPA) - Must be after API routes
574
+ // ============================================================================
575
+ if (existsSync(CLIENT_DIST)) {
576
+ // Serve static files from Vue build
577
+ app.use(express.static(CLIENT_DIST));
578
+ // SPA fallback - serve index.html for all non-API routes
579
+ // Express 5 requires named wildcards: /{*path}
580
+ app.get("/{*path}", (req, res, next) => {
581
+ // Skip API routes and image routes
582
+ if (req.path.startsWith("/api/") || req.path.startsWith("/images/")) {
583
+ next();
584
+ return;
585
+ }
586
+ res.sendFile(join(CLIENT_DIST, "index.html"));
587
+ });
588
+ }
589
+ else {
590
+ // Development fallback - show message
591
+ app.get("/", (_req, res) => {
592
+ res.send(`
593
+ <!DOCTYPE html>
594
+ <html>
595
+ <head>
596
+ <title>DMCP - Development Mode</title>
597
+ <style>
598
+ body { font-family: system-ui; background: #1a1a2e; color: #eee; padding: 40px; }
599
+ h1 { color: #e94560; }
600
+ code { background: #16213e; padding: 4px 8px; border-radius: 4px; }
601
+ </style>
602
+ </head>
603
+ <body>
604
+ <h1>DMCP Game Viewer</h1>
605
+ <p>Vue app not built. Run the following commands:</p>
606
+ <pre><code>cd client && npm run build</code></pre>
607
+ <p>Or for development with hot reload:</p>
608
+ <pre><code>cd client && npm run dev</code></pre>
609
+ <p>API endpoints are available at <code>/api/*</code></p>
610
+ </body>
611
+ </html>
612
+ `);
613
+ });
614
+ }
615
+ // Error handling
616
+ app.use((err, _req, res, _next) => {
617
+ console.error("HTTP Error:", err);
618
+ res.status(500).json({ error: err.message });
619
+ });
620
+ return app;
621
+ }
622
+ export function startHttpServer(port = 3456) {
623
+ return new Promise((resolve, reject) => {
624
+ const app = createHttpServer(port);
625
+ const server = app.listen(port, () => {
626
+ const addr = server.address();
627
+ const actualPort = typeof addr === 'object' && addr ? addr.port : port;
628
+ console.error(`HTTP server running at http://localhost:${actualPort}`);
629
+ resolve(actualPort);
630
+ });
631
+ server.on('error', (err) => {
632
+ if (err.code === 'EADDRINUSE') {
633
+ console.error(`Port ${port} in use, trying port 0 (random available)`);
634
+ server.close();
635
+ // Try with port 0 to get a random available port
636
+ const retryServer = app.listen(0, () => {
637
+ const addr = retryServer.address();
638
+ const actualPort = typeof addr === 'object' && addr ? addr.port : 0;
639
+ console.error(`HTTP server running at http://localhost:${actualPort}`);
640
+ resolve(actualPort);
641
+ });
642
+ retryServer.on('error', reject);
643
+ }
644
+ else {
645
+ reject(err);
646
+ }
647
+ });
648
+ });
649
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};