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,725 @@
1
+ import { getDatabase } from "./connection.js";
2
+ export function initializeSchema() {
3
+ const db = getDatabase();
4
+ // ============================================================================
5
+ // MIGRATION: Rename 'sessions' to 'games' and 'session_id' to 'game_id'
6
+ // This handles existing databases that use the old 'session' terminology
7
+ // ============================================================================
8
+ // Check if the old 'sessions' table exists and migrate to 'games'
9
+ const sessionsTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get();
10
+ if (sessionsTableExists) {
11
+ // Rename the main sessions table to games
12
+ db.exec(`ALTER TABLE sessions RENAME TO games`);
13
+ // Rename session_id columns to game_id in all tables that have them
14
+ const tablesWithSessionId = [
15
+ 'characters', 'locations', 'items', 'quests', 'narrative_events',
16
+ 'combats', 'resources', 'game_time', 'scheduled_events', 'timers',
17
+ 'random_tables', 'secrets', 'relationships', 'factions', 'abilities',
18
+ 'status_effects', 'tags', 'notes', 'external_updates', 'pause_states',
19
+ 'stored_images', 'stored_audio'
20
+ ];
21
+ for (const table of tablesWithSessionId) {
22
+ try {
23
+ db.exec(`ALTER TABLE ${table} RENAME COLUMN session_id TO game_id`);
24
+ }
25
+ catch {
26
+ // Column doesn't exist or already renamed
27
+ }
28
+ }
29
+ // Rename session_themes table to game_themes if it exists
30
+ try {
31
+ // First rename the column, then rename the table
32
+ db.exec(`ALTER TABLE session_themes RENAME COLUMN session_id TO game_id`);
33
+ db.exec(`ALTER TABLE session_themes RENAME TO game_themes`);
34
+ }
35
+ catch {
36
+ // Table doesn't exist or already renamed
37
+ }
38
+ }
39
+ // Fix for edge case where game_themes table exists but has session_id column
40
+ // This can happen if the above RENAME COLUMN failed but RENAME TO succeeded
41
+ const gameThemesHasSessionId = db.prepare("SELECT name FROM pragma_table_info('game_themes') WHERE name = 'session_id'").get();
42
+ if (gameThemesHasSessionId) {
43
+ // Recreate the table with correct column name
44
+ db.exec(`
45
+ CREATE TABLE game_themes_fixed (
46
+ game_id TEXT PRIMARY KEY,
47
+ config TEXT NOT NULL DEFAULT '{}',
48
+ updated_at TEXT NOT NULL,
49
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
50
+ );
51
+ INSERT INTO game_themes_fixed (game_id, config, updated_at)
52
+ SELECT session_id, config, updated_at FROM game_themes;
53
+ DROP TABLE game_themes;
54
+ ALTER TABLE game_themes_fixed RENAME TO game_themes;
55
+ `);
56
+ }
57
+ // Games table (formerly sessions)
58
+ db.exec(`
59
+ CREATE TABLE IF NOT EXISTS games (
60
+ id TEXT PRIMARY KEY,
61
+ name TEXT NOT NULL,
62
+ setting TEXT NOT NULL,
63
+ style TEXT NOT NULL,
64
+ rules TEXT,
65
+ preferences TEXT,
66
+ current_location_id TEXT,
67
+ created_at TEXT NOT NULL,
68
+ updated_at TEXT NOT NULL
69
+ )
70
+ `);
71
+ // Add preferences column if it doesn't exist (migration)
72
+ try {
73
+ db.exec(`ALTER TABLE games ADD COLUMN preferences TEXT`);
74
+ }
75
+ catch {
76
+ // Column already exists
77
+ }
78
+ // Add title_image_id column if it doesn't exist (migration)
79
+ try {
80
+ db.exec(`ALTER TABLE games ADD COLUMN title_image_id TEXT`);
81
+ }
82
+ catch {
83
+ // Column already exists
84
+ }
85
+ // Add favicon_image_id column if it doesn't exist (migration)
86
+ try {
87
+ db.exec(`ALTER TABLE games ADD COLUMN favicon_image_id TEXT`);
88
+ }
89
+ catch {
90
+ // Column already exists
91
+ }
92
+ // Characters table
93
+ db.exec(`
94
+ CREATE TABLE IF NOT EXISTS characters (
95
+ id TEXT PRIMARY KEY,
96
+ game_id TEXT NOT NULL,
97
+ name TEXT NOT NULL,
98
+ is_player INTEGER NOT NULL DEFAULT 0,
99
+ attributes TEXT NOT NULL DEFAULT '{}',
100
+ skills TEXT NOT NULL DEFAULT '{}',
101
+ status TEXT NOT NULL DEFAULT '{}',
102
+ location_id TEXT,
103
+ notes TEXT DEFAULT '',
104
+ voice TEXT,
105
+ created_at TEXT NOT NULL,
106
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
107
+ )
108
+ `);
109
+ // Add voice column if it doesn't exist (migration for existing databases)
110
+ try {
111
+ db.exec(`ALTER TABLE characters ADD COLUMN voice TEXT`);
112
+ }
113
+ catch {
114
+ // Column already exists
115
+ }
116
+ // Add image_gen column to characters
117
+ try {
118
+ db.exec(`ALTER TABLE characters ADD COLUMN image_gen TEXT`);
119
+ }
120
+ catch {
121
+ // Column already exists
122
+ }
123
+ // Locations table
124
+ db.exec(`
125
+ CREATE TABLE IF NOT EXISTS locations (
126
+ id TEXT PRIMARY KEY,
127
+ game_id TEXT NOT NULL,
128
+ name TEXT NOT NULL,
129
+ description TEXT NOT NULL,
130
+ properties TEXT NOT NULL DEFAULT '{}',
131
+ image_gen TEXT,
132
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
133
+ )
134
+ `);
135
+ // Add image_gen column to locations
136
+ try {
137
+ db.exec(`ALTER TABLE locations ADD COLUMN image_gen TEXT`);
138
+ }
139
+ catch {
140
+ // Column already exists
141
+ }
142
+ // Items table
143
+ db.exec(`
144
+ CREATE TABLE IF NOT EXISTS items (
145
+ id TEXT PRIMARY KEY,
146
+ game_id TEXT NOT NULL,
147
+ owner_id TEXT NOT NULL,
148
+ owner_type TEXT NOT NULL CHECK (owner_type IN ('character', 'location')),
149
+ name TEXT NOT NULL,
150
+ properties TEXT NOT NULL DEFAULT '{}',
151
+ image_gen TEXT,
152
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
153
+ )
154
+ `);
155
+ // Add image_gen column to items
156
+ try {
157
+ db.exec(`ALTER TABLE items ADD COLUMN image_gen TEXT`);
158
+ }
159
+ catch {
160
+ // Column already exists
161
+ }
162
+ // Quests table
163
+ db.exec(`
164
+ CREATE TABLE IF NOT EXISTS quests (
165
+ id TEXT PRIMARY KEY,
166
+ game_id TEXT NOT NULL,
167
+ name TEXT NOT NULL,
168
+ description TEXT NOT NULL,
169
+ objectives TEXT NOT NULL DEFAULT '[]',
170
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'completed', 'failed', 'abandoned')),
171
+ rewards TEXT,
172
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
173
+ )
174
+ `);
175
+ // Narrative events table
176
+ db.exec(`
177
+ CREATE TABLE IF NOT EXISTS narrative_events (
178
+ id TEXT PRIMARY KEY,
179
+ game_id TEXT NOT NULL,
180
+ event_type TEXT NOT NULL,
181
+ content TEXT NOT NULL,
182
+ metadata TEXT NOT NULL DEFAULT '{}',
183
+ timestamp TEXT NOT NULL,
184
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
185
+ )
186
+ `);
187
+ // Combat table
188
+ db.exec(`
189
+ CREATE TABLE IF NOT EXISTS combats (
190
+ id TEXT PRIMARY KEY,
191
+ game_id TEXT NOT NULL,
192
+ location_id TEXT NOT NULL,
193
+ participants TEXT NOT NULL DEFAULT '[]',
194
+ current_turn INTEGER NOT NULL DEFAULT 0,
195
+ round INTEGER NOT NULL DEFAULT 1,
196
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'resolved')),
197
+ log TEXT NOT NULL DEFAULT '[]',
198
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
199
+ )
200
+ `);
201
+ // Resources table (for tracking currency, reputation, counters, etc.)
202
+ db.exec(`
203
+ CREATE TABLE IF NOT EXISTS resources (
204
+ id TEXT PRIMARY KEY,
205
+ game_id TEXT NOT NULL,
206
+ owner_id TEXT,
207
+ owner_type TEXT NOT NULL CHECK (owner_type IN ('game', 'character')),
208
+ name TEXT NOT NULL,
209
+ description TEXT,
210
+ category TEXT,
211
+ value REAL NOT NULL DEFAULT 0,
212
+ min_value REAL,
213
+ max_value REAL,
214
+ created_at TEXT NOT NULL,
215
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
216
+ )
217
+ `);
218
+ // Resource history table (tracks all changes)
219
+ db.exec(`
220
+ CREATE TABLE IF NOT EXISTS resource_history (
221
+ id TEXT PRIMARY KEY,
222
+ resource_id TEXT NOT NULL,
223
+ previous_value REAL NOT NULL,
224
+ new_value REAL NOT NULL,
225
+ delta REAL NOT NULL,
226
+ reason TEXT,
227
+ timestamp TEXT NOT NULL,
228
+ FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
229
+ )
230
+ `);
231
+ // Resource constraints table -- optional, server-enforced invariants on
232
+ // resource values (see src/tools/constraint.ts). Opt-in: a resource with
233
+ // no row here (directly or via resource_constraint_members) behaves
234
+ // exactly as before this feature existed.
235
+ db.exec(`
236
+ CREATE TABLE IF NOT EXISTS resource_constraints (
237
+ id TEXT PRIMARY KEY,
238
+ game_id TEXT NOT NULL,
239
+ kind TEXT NOT NULL CHECK (kind IN ('bounded', 'monotonic', 'conserved')),
240
+ direction TEXT CHECK (direction IN ('increasing', 'decreasing')),
241
+ total REAL,
242
+ created_at TEXT NOT NULL,
243
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
244
+ )
245
+ `);
246
+ // Members of a resource constraint. 'bounded' and 'monotonic' constraints
247
+ // have exactly one member (the resource they govern); 'conserved'
248
+ // constraints have two or more (the set that must sum to a fixed total).
249
+ db.exec(`
250
+ CREATE TABLE IF NOT EXISTS resource_constraint_members (
251
+ constraint_id TEXT NOT NULL,
252
+ resource_id TEXT NOT NULL,
253
+ PRIMARY KEY (constraint_id, resource_id),
254
+ FOREIGN KEY (constraint_id) REFERENCES resource_constraints(id) ON DELETE CASCADE,
255
+ FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
256
+ )
257
+ `);
258
+ // Game time table (one per game)
259
+ db.exec(`
260
+ CREATE TABLE IF NOT EXISTS game_time (
261
+ game_id TEXT PRIMARY KEY,
262
+ current_time TEXT NOT NULL,
263
+ calendar_config TEXT NOT NULL,
264
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
265
+ )
266
+ `);
267
+ // Scheduled events table
268
+ db.exec(`
269
+ CREATE TABLE IF NOT EXISTS scheduled_events (
270
+ id TEXT PRIMARY KEY,
271
+ game_id TEXT NOT NULL,
272
+ name TEXT NOT NULL,
273
+ description TEXT,
274
+ trigger_time TEXT NOT NULL,
275
+ recurring TEXT,
276
+ triggered INTEGER DEFAULT 0,
277
+ metadata TEXT DEFAULT '{}',
278
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
279
+ )
280
+ `);
281
+ // Add consequence column to scheduled_events (migration). Holds an
282
+ // optional JSON-encoded ExpiryConsequence ({resourceId, delta}) that
283
+ // advanceTime() applies itself, atomically with marking the event
284
+ // triggered, when its trigger time is crossed. Null/absent means "no
285
+ // consequence" -- behavior is unchanged from before this feature existed.
286
+ try {
287
+ db.exec(`ALTER TABLE scheduled_events ADD COLUMN consequence TEXT`);
288
+ }
289
+ catch {
290
+ // Column already exists
291
+ }
292
+ // Timers table (countdowns, stopwatches, clocks)
293
+ db.exec(`
294
+ CREATE TABLE IF NOT EXISTS timers (
295
+ id TEXT PRIMARY KEY,
296
+ game_id TEXT NOT NULL,
297
+ name TEXT NOT NULL,
298
+ description TEXT,
299
+ timer_type TEXT NOT NULL CHECK (timer_type IN ('countdown', 'stopwatch', 'clock')),
300
+ current_value INTEGER NOT NULL,
301
+ max_value INTEGER,
302
+ direction TEXT DEFAULT 'down' CHECK (direction IN ('up', 'down')),
303
+ trigger_at INTEGER,
304
+ triggered INTEGER DEFAULT 0,
305
+ unit TEXT DEFAULT 'tick',
306
+ visible_to_players INTEGER DEFAULT 1,
307
+ created_at TEXT NOT NULL,
308
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
309
+ )
310
+ `);
311
+ // Add consequence column to timers (migration). Same shape and semantics
312
+ // as scheduled_events.consequence, applied by tickTimer() the moment the
313
+ // timer crosses trigger_at.
314
+ try {
315
+ db.exec(`ALTER TABLE timers ADD COLUMN consequence TEXT`);
316
+ }
317
+ catch {
318
+ // Column already exists
319
+ }
320
+ // Random tables
321
+ db.exec(`
322
+ CREATE TABLE IF NOT EXISTS random_tables (
323
+ id TEXT PRIMARY KEY,
324
+ game_id TEXT NOT NULL,
325
+ name TEXT NOT NULL,
326
+ description TEXT,
327
+ category TEXT,
328
+ entries TEXT NOT NULL DEFAULT '[]',
329
+ roll_expression TEXT DEFAULT '1d100',
330
+ created_at TEXT NOT NULL,
331
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
332
+ )
333
+ `);
334
+ // Secrets table
335
+ db.exec(`
336
+ CREATE TABLE IF NOT EXISTS secrets (
337
+ id TEXT PRIMARY KEY,
338
+ game_id TEXT NOT NULL,
339
+ name TEXT NOT NULL,
340
+ description TEXT NOT NULL,
341
+ category TEXT,
342
+ related_entity_id TEXT,
343
+ related_entity_type TEXT,
344
+ revealed_to TEXT DEFAULT '[]',
345
+ is_public INTEGER DEFAULT 0,
346
+ clues TEXT DEFAULT '[]',
347
+ created_at TEXT NOT NULL,
348
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
349
+ )
350
+ `);
351
+ // Relationships table
352
+ db.exec(`
353
+ CREATE TABLE IF NOT EXISTS relationships (
354
+ id TEXT PRIMARY KEY,
355
+ game_id TEXT NOT NULL,
356
+ source_id TEXT NOT NULL,
357
+ source_type TEXT NOT NULL,
358
+ target_id TEXT NOT NULL,
359
+ target_type TEXT NOT NULL,
360
+ relationship_type TEXT NOT NULL,
361
+ value INTEGER DEFAULT 0,
362
+ label TEXT,
363
+ notes TEXT,
364
+ created_at TEXT NOT NULL,
365
+ updated_at TEXT NOT NULL,
366
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
367
+ )
368
+ `);
369
+ // Relationship history table
370
+ db.exec(`
371
+ CREATE TABLE IF NOT EXISTS relationship_history (
372
+ id TEXT PRIMARY KEY,
373
+ relationship_id TEXT NOT NULL,
374
+ previous_value INTEGER,
375
+ new_value INTEGER,
376
+ reason TEXT,
377
+ timestamp TEXT NOT NULL,
378
+ FOREIGN KEY (relationship_id) REFERENCES relationships(id) ON DELETE CASCADE
379
+ )
380
+ `);
381
+ // Factions table
382
+ db.exec(`
383
+ CREATE TABLE IF NOT EXISTS factions (
384
+ id TEXT PRIMARY KEY,
385
+ game_id TEXT NOT NULL,
386
+ name TEXT NOT NULL,
387
+ description TEXT,
388
+ leader_id TEXT,
389
+ headquarters_id TEXT,
390
+ resources TEXT DEFAULT '{}',
391
+ goals TEXT DEFAULT '[]',
392
+ traits TEXT DEFAULT '[]',
393
+ status TEXT DEFAULT 'active' CHECK (status IN ('active', 'disbanded', 'hidden')),
394
+ created_at TEXT NOT NULL,
395
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
396
+ )
397
+ `);
398
+ // Abilities table
399
+ db.exec(`
400
+ CREATE TABLE IF NOT EXISTS abilities (
401
+ id TEXT PRIMARY KEY,
402
+ game_id TEXT NOT NULL,
403
+ owner_id TEXT,
404
+ owner_type TEXT CHECK (owner_type IN ('template', 'character')),
405
+ name TEXT NOT NULL,
406
+ description TEXT,
407
+ category TEXT,
408
+ cost TEXT DEFAULT '{}',
409
+ cooldown INTEGER,
410
+ current_cooldown INTEGER DEFAULT 0,
411
+ effects TEXT DEFAULT '[]',
412
+ requirements TEXT DEFAULT '{}',
413
+ tags TEXT DEFAULT '[]',
414
+ created_at TEXT NOT NULL,
415
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
416
+ )
417
+ `);
418
+ // Status effects table
419
+ db.exec(`
420
+ CREATE TABLE IF NOT EXISTS status_effects (
421
+ id TEXT PRIMARY KEY,
422
+ game_id TEXT NOT NULL,
423
+ target_id TEXT NOT NULL,
424
+ name TEXT NOT NULL,
425
+ description TEXT,
426
+ effect_type TEXT CHECK (effect_type IN ('buff', 'debuff', 'neutral')),
427
+ duration INTEGER,
428
+ stacks INTEGER DEFAULT 1,
429
+ max_stacks INTEGER,
430
+ effects TEXT DEFAULT '{}',
431
+ source_id TEXT,
432
+ source_type TEXT,
433
+ expires_at TEXT,
434
+ created_at TEXT NOT NULL,
435
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
436
+ )
437
+ `);
438
+ // Tags table
439
+ db.exec(`
440
+ CREATE TABLE IF NOT EXISTS tags (
441
+ id TEXT PRIMARY KEY,
442
+ game_id TEXT NOT NULL,
443
+ entity_id TEXT NOT NULL,
444
+ entity_type TEXT NOT NULL,
445
+ tag TEXT NOT NULL,
446
+ color TEXT,
447
+ notes TEXT,
448
+ created_at TEXT NOT NULL,
449
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE,
450
+ UNIQUE (game_id, entity_id, entity_type, tag)
451
+ )
452
+ `);
453
+ // Notes table
454
+ db.exec(`
455
+ CREATE TABLE IF NOT EXISTS notes (
456
+ id TEXT PRIMARY KEY,
457
+ game_id TEXT NOT NULL,
458
+ title TEXT NOT NULL,
459
+ content TEXT NOT NULL,
460
+ category TEXT,
461
+ pinned INTEGER DEFAULT 0,
462
+ related_entity_id TEXT,
463
+ related_entity_type TEXT,
464
+ tags TEXT DEFAULT '[]',
465
+ created_at TEXT NOT NULL,
466
+ updated_at TEXT NOT NULL,
467
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
468
+ )
469
+ `);
470
+ // External updates table - enables multi-agent collaboration
471
+ // External agents can push updates that the primary DM agent receives
472
+ db.exec(`
473
+ CREATE TABLE IF NOT EXISTS external_updates (
474
+ id TEXT PRIMARY KEY,
475
+ game_id TEXT NOT NULL,
476
+
477
+ -- Source identification
478
+ source_agent TEXT NOT NULL,
479
+ source_description TEXT,
480
+
481
+ -- Update content
482
+ update_type TEXT NOT NULL,
483
+ category TEXT,
484
+ title TEXT NOT NULL,
485
+ content TEXT NOT NULL,
486
+ structured_data TEXT,
487
+
488
+ -- Targeting (what entities this relates to)
489
+ target_entity_id TEXT,
490
+ target_entity_type TEXT,
491
+
492
+ -- Priority and status
493
+ priority TEXT DEFAULT 'normal' CHECK (priority IN ('low', 'normal', 'high', 'urgent')),
494
+ status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'acknowledged', 'applied', 'rejected')),
495
+
496
+ -- Timestamps
497
+ created_at TEXT NOT NULL,
498
+ acknowledged_at TEXT,
499
+ applied_at TEXT,
500
+
501
+ -- DM notes on how update was used
502
+ dm_notes TEXT,
503
+
504
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
505
+ )
506
+ `);
507
+ // Pause states table - captures agent context for seamless resume
508
+ db.exec(`
509
+ CREATE TABLE IF NOT EXISTS pause_states (
510
+ id TEXT PRIMARY KEY,
511
+ game_id TEXT NOT NULL UNIQUE,
512
+
513
+ -- Current scene/moment context
514
+ current_scene TEXT NOT NULL,
515
+ scene_atmosphere TEXT,
516
+ immediate_situation TEXT NOT NULL,
517
+
518
+ -- Pending player interaction
519
+ pending_player_action TEXT,
520
+ awaiting_response_to TEXT,
521
+ presented_choices TEXT,
522
+
523
+ -- Active narrative threads
524
+ active_threads TEXT DEFAULT '[]',
525
+
526
+ -- DM's plans and notes
527
+ dm_short_term_plans TEXT,
528
+ dm_long_term_plans TEXT,
529
+ upcoming_reveals TEXT DEFAULT '[]',
530
+
531
+ -- NPC states (not persisted elsewhere)
532
+ npc_attitudes TEXT DEFAULT '{}',
533
+ active_conversations TEXT DEFAULT '[]',
534
+
535
+ -- Important context that might be lost
536
+ recent_tone TEXT,
537
+ player_apparent_goals TEXT,
538
+ unresolved_hooks TEXT DEFAULT '[]',
539
+
540
+ -- Metadata
541
+ pause_reason TEXT,
542
+ created_at TEXT NOT NULL,
543
+ model_used TEXT,
544
+
545
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
546
+ )
547
+ `);
548
+ // Stored images table - file-based image storage
549
+ // entity_type is flexible to support any entity (character, location, item, scene, faction, quest, ability, etc.)
550
+ db.exec(`
551
+ CREATE TABLE IF NOT EXISTS stored_images (
552
+ id TEXT PRIMARY KEY,
553
+ game_id TEXT NOT NULL,
554
+ entity_id TEXT NOT NULL,
555
+ entity_type TEXT NOT NULL,
556
+
557
+ -- File information
558
+ file_path TEXT NOT NULL,
559
+ file_size INTEGER NOT NULL,
560
+ mime_type TEXT NOT NULL,
561
+ width INTEGER,
562
+ height INTEGER,
563
+
564
+ -- Metadata
565
+ label TEXT,
566
+ description TEXT,
567
+ source TEXT NOT NULL CHECK (source IN ('generated', 'uploaded', 'url')),
568
+ source_url TEXT,
569
+ generation_tool TEXT,
570
+ generation_prompt TEXT,
571
+
572
+ -- Flags
573
+ is_primary INTEGER DEFAULT 0,
574
+
575
+ -- Timestamps
576
+ created_at TEXT NOT NULL,
577
+
578
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
579
+ )
580
+ `);
581
+ // Migration: Remove entity_type CHECK constraint to allow any entity type
582
+ // SQLite doesn't support ALTER CHECK, so we need to recreate the table
583
+ const storedImagesInfo = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='stored_images'").get();
584
+ if (storedImagesInfo && storedImagesInfo.sql.includes("entity_type IN")) {
585
+ db.exec(`
586
+ CREATE TABLE stored_images_new (
587
+ id TEXT PRIMARY KEY,
588
+ game_id TEXT NOT NULL,
589
+ entity_id TEXT NOT NULL,
590
+ entity_type TEXT NOT NULL,
591
+ file_path TEXT NOT NULL,
592
+ file_size INTEGER NOT NULL,
593
+ mime_type TEXT NOT NULL,
594
+ width INTEGER,
595
+ height INTEGER,
596
+ label TEXT,
597
+ description TEXT,
598
+ source TEXT NOT NULL CHECK (source IN ('generated', 'uploaded', 'url')),
599
+ source_url TEXT,
600
+ generation_tool TEXT,
601
+ generation_prompt TEXT,
602
+ is_primary INTEGER DEFAULT 0,
603
+ created_at TEXT NOT NULL,
604
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
605
+ );
606
+ INSERT INTO stored_images_new SELECT * FROM stored_images;
607
+ DROP TABLE stored_images;
608
+ ALTER TABLE stored_images_new RENAME TO stored_images;
609
+ `);
610
+ }
611
+ // Display configuration table (global, one row)
612
+ db.exec(`
613
+ CREATE TABLE IF NOT EXISTS display_config (
614
+ id INTEGER PRIMARY KEY CHECK (id = 1),
615
+ config TEXT NOT NULL DEFAULT '{}',
616
+ updated_at TEXT NOT NULL
617
+ )
618
+ `);
619
+ // Per-game theme configuration
620
+ db.exec(`
621
+ CREATE TABLE IF NOT EXISTS game_themes (
622
+ game_id TEXT PRIMARY KEY,
623
+ config TEXT NOT NULL DEFAULT '{}',
624
+ updated_at TEXT NOT NULL,
625
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
626
+ )
627
+ `);
628
+ // Stored audio table - file-based audio storage for TTS/voice clips
629
+ // entity_type is flexible to support any entity (character, location, game, scene, etc.)
630
+ db.exec(`
631
+ CREATE TABLE IF NOT EXISTS stored_audio (
632
+ id TEXT PRIMARY KEY,
633
+ game_id TEXT NOT NULL,
634
+ entity_id TEXT NOT NULL,
635
+ entity_type TEXT NOT NULL,
636
+
637
+ -- File information
638
+ file_path TEXT NOT NULL,
639
+ file_size INTEGER NOT NULL,
640
+ mime_type TEXT NOT NULL,
641
+ duration_ms INTEGER,
642
+ sample_rate INTEGER,
643
+
644
+ -- Metadata
645
+ label TEXT,
646
+ description TEXT,
647
+ source TEXT NOT NULL CHECK (source IN ('generated', 'uploaded', 'url')),
648
+ source_url TEXT,
649
+
650
+ -- TTS-specific metadata
651
+ tts_engine TEXT,
652
+ tts_voice TEXT,
653
+ tts_text TEXT,
654
+ tts_settings TEXT,
655
+
656
+ -- Voice reference metadata (for voice cloning)
657
+ is_voice_reference INTEGER DEFAULT 0,
658
+ voice_name TEXT,
659
+ voice_description TEXT,
660
+
661
+ -- Flags
662
+ is_primary INTEGER DEFAULT 0,
663
+
664
+ -- Timestamps
665
+ created_at TEXT NOT NULL,
666
+
667
+ FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
668
+ )
669
+ `);
670
+ // Create indexes for common queries
671
+ db.exec(`
672
+ CREATE INDEX IF NOT EXISTS idx_characters_game ON characters(game_id);
673
+ CREATE INDEX IF NOT EXISTS idx_characters_location ON characters(location_id);
674
+ CREATE INDEX IF NOT EXISTS idx_locations_game ON locations(game_id);
675
+ CREATE INDEX IF NOT EXISTS idx_items_game ON items(game_id);
676
+ CREATE INDEX IF NOT EXISTS idx_items_owner ON items(owner_id, owner_type);
677
+ CREATE INDEX IF NOT EXISTS idx_quests_game ON quests(game_id);
678
+ CREATE INDEX IF NOT EXISTS idx_quests_status ON quests(status);
679
+ CREATE INDEX IF NOT EXISTS idx_narrative_game ON narrative_events(game_id);
680
+ CREATE INDEX IF NOT EXISTS idx_narrative_timestamp ON narrative_events(timestamp);
681
+ CREATE INDEX IF NOT EXISTS idx_combats_game ON combats(game_id);
682
+ CREATE INDEX IF NOT EXISTS idx_combats_status ON combats(status);
683
+ CREATE INDEX IF NOT EXISTS idx_resources_game ON resources(game_id);
684
+ CREATE INDEX IF NOT EXISTS idx_resources_owner ON resources(owner_id, owner_type);
685
+ CREATE INDEX IF NOT EXISTS idx_resources_category ON resources(category);
686
+ CREATE INDEX IF NOT EXISTS idx_resource_history_resource ON resource_history(resource_id);
687
+ CREATE INDEX IF NOT EXISTS idx_resource_constraints_game ON resource_constraints(game_id);
688
+ CREATE INDEX IF NOT EXISTS idx_resource_constraint_members_resource ON resource_constraint_members(resource_id);
689
+ CREATE INDEX IF NOT EXISTS idx_scheduled_events_game ON scheduled_events(game_id);
690
+ CREATE INDEX IF NOT EXISTS idx_scheduled_events_trigger ON scheduled_events(trigger_time);
691
+ CREATE INDEX IF NOT EXISTS idx_timers_game ON timers(game_id);
692
+ CREATE INDEX IF NOT EXISTS idx_random_tables_game ON random_tables(game_id);
693
+ CREATE INDEX IF NOT EXISTS idx_random_tables_category ON random_tables(category);
694
+ CREATE INDEX IF NOT EXISTS idx_secrets_game ON secrets(game_id);
695
+ CREATE INDEX IF NOT EXISTS idx_secrets_category ON secrets(category);
696
+ CREATE INDEX IF NOT EXISTS idx_relationships_game ON relationships(game_id);
697
+ CREATE INDEX IF NOT EXISTS idx_relationships_source ON relationships(source_id, source_type);
698
+ CREATE INDEX IF NOT EXISTS idx_relationships_target ON relationships(target_id, target_type);
699
+ CREATE INDEX IF NOT EXISTS idx_relationship_history_rel ON relationship_history(relationship_id);
700
+ CREATE INDEX IF NOT EXISTS idx_factions_game ON factions(game_id);
701
+ CREATE INDEX IF NOT EXISTS idx_factions_status ON factions(status);
702
+ CREATE INDEX IF NOT EXISTS idx_abilities_game ON abilities(game_id);
703
+ CREATE INDEX IF NOT EXISTS idx_abilities_owner ON abilities(owner_id, owner_type);
704
+ CREATE INDEX IF NOT EXISTS idx_abilities_category ON abilities(category);
705
+ CREATE INDEX IF NOT EXISTS idx_status_effects_game ON status_effects(game_id);
706
+ CREATE INDEX IF NOT EXISTS idx_status_effects_target ON status_effects(target_id);
707
+ CREATE INDEX IF NOT EXISTS idx_tags_game ON tags(game_id);
708
+ CREATE INDEX IF NOT EXISTS idx_tags_entity ON tags(entity_id, entity_type);
709
+ CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
710
+ CREATE INDEX IF NOT EXISTS idx_notes_game ON notes(game_id);
711
+ CREATE INDEX IF NOT EXISTS idx_notes_category ON notes(category);
712
+ CREATE INDEX IF NOT EXISTS idx_notes_pinned ON notes(pinned);
713
+ CREATE INDEX IF NOT EXISTS idx_pause_states_game ON pause_states(game_id);
714
+ CREATE INDEX IF NOT EXISTS idx_external_updates_game ON external_updates(game_id);
715
+ CREATE INDEX IF NOT EXISTS idx_external_updates_status ON external_updates(status);
716
+ CREATE INDEX IF NOT EXISTS idx_external_updates_priority ON external_updates(priority);
717
+ CREATE INDEX IF NOT EXISTS idx_stored_images_game ON stored_images(game_id);
718
+ CREATE INDEX IF NOT EXISTS idx_stored_images_entity ON stored_images(entity_id, entity_type);
719
+ CREATE INDEX IF NOT EXISTS idx_stored_images_primary ON stored_images(entity_id, entity_type, is_primary);
720
+ CREATE INDEX IF NOT EXISTS idx_stored_audio_game ON stored_audio(game_id);
721
+ CREATE INDEX IF NOT EXISTS idx_stored_audio_entity ON stored_audio(entity_id, entity_type);
722
+ CREATE INDEX IF NOT EXISTS idx_stored_audio_primary ON stored_audio(entity_id, entity_type, is_primary);
723
+ CREATE INDEX IF NOT EXISTS idx_stored_audio_voice_ref ON stored_audio(game_id, is_voice_reference);
724
+ `);
725
+ }