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,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
|
+
}
|
package/dist/index.d.ts
ADDED