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,238 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase } from "../db/connection.js";
3
+ import { safeJsonParse } from "../utils/json.js";
4
+ import { validateGameExists } from "./game.js";
5
+ import { getCharacter } from "./character.js";
6
+ export function createAbility(params) {
7
+ // Validate game exists to prevent orphaned records
8
+ validateGameExists(params.gameId);
9
+ const db = getDatabase();
10
+ const id = uuidv4();
11
+ const now = new Date().toISOString();
12
+ const ownerId = params.ownerType === "template" ? null : (params.ownerId || null);
13
+ db.prepare(`
14
+ INSERT INTO abilities (id, game_id, owner_id, owner_type, name, description, category, cost, cooldown, effects, requirements, tags, created_at)
15
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
16
+ `).run(id, params.gameId, ownerId, params.ownerType, params.name, params.description || "", params.category || null, JSON.stringify(params.cost || {}), params.cooldown ?? null, JSON.stringify(params.effects || []), JSON.stringify(params.requirements || {}), JSON.stringify(params.tags || []), now);
17
+ return {
18
+ id,
19
+ gameId: params.gameId,
20
+ ownerId,
21
+ ownerType: params.ownerType,
22
+ name: params.name,
23
+ description: params.description || "",
24
+ category: params.category || null,
25
+ cost: params.cost || {},
26
+ cooldown: params.cooldown ?? null,
27
+ currentCooldown: 0,
28
+ effects: params.effects || [],
29
+ requirements: params.requirements || {},
30
+ tags: params.tags || [],
31
+ createdAt: now,
32
+ };
33
+ }
34
+ export function getAbility(id) {
35
+ const db = getDatabase();
36
+ const row = db.prepare(`SELECT * FROM abilities WHERE id = ?`).get(id);
37
+ if (!row)
38
+ return null;
39
+ return {
40
+ id: row.id,
41
+ gameId: row.game_id,
42
+ ownerId: row.owner_id,
43
+ ownerType: row.owner_type,
44
+ name: row.name,
45
+ description: row.description,
46
+ category: row.category,
47
+ cost: safeJsonParse(row.cost || "{}", {}),
48
+ cooldown: row.cooldown,
49
+ currentCooldown: row.current_cooldown,
50
+ effects: safeJsonParse(row.effects || "[]", []),
51
+ requirements: safeJsonParse(row.requirements || "{}", {}),
52
+ tags: safeJsonParse(row.tags || "[]", []),
53
+ createdAt: row.created_at,
54
+ };
55
+ }
56
+ export function updateAbility(id, updates) {
57
+ const db = getDatabase();
58
+ const current = getAbility(id);
59
+ if (!current)
60
+ return null;
61
+ const newName = updates.name ?? current.name;
62
+ const newDescription = updates.description ?? current.description;
63
+ const newCategory = updates.category !== undefined ? updates.category : current.category;
64
+ const newCost = updates.cost ?? current.cost;
65
+ const newCooldown = updates.cooldown !== undefined ? updates.cooldown : current.cooldown;
66
+ const newEffects = updates.effects ?? current.effects;
67
+ const newRequirements = updates.requirements ?? current.requirements;
68
+ const newTags = updates.tags ?? current.tags;
69
+ db.prepare(`
70
+ UPDATE abilities
71
+ SET name = ?, description = ?, category = ?, cost = ?, cooldown = ?, effects = ?, requirements = ?, tags = ?
72
+ WHERE id = ?
73
+ `).run(newName, newDescription, newCategory, JSON.stringify(newCost), newCooldown, JSON.stringify(newEffects), JSON.stringify(newRequirements), JSON.stringify(newTags), id);
74
+ return {
75
+ ...current,
76
+ name: newName,
77
+ description: newDescription,
78
+ category: newCategory,
79
+ cost: newCost,
80
+ cooldown: newCooldown,
81
+ effects: newEffects,
82
+ requirements: newRequirements,
83
+ tags: newTags,
84
+ };
85
+ }
86
+ export function deleteAbility(id) {
87
+ const db = getDatabase();
88
+ const result = db.prepare(`DELETE FROM abilities WHERE id = ?`).run(id);
89
+ return result.changes > 0;
90
+ }
91
+ export function listAbilities(gameId, filter) {
92
+ const db = getDatabase();
93
+ let query = `SELECT * FROM abilities WHERE game_id = ?`;
94
+ const params = [gameId];
95
+ if (filter?.ownerType) {
96
+ query += ` AND owner_type = ?`;
97
+ params.push(filter.ownerType);
98
+ }
99
+ if (filter?.ownerId) {
100
+ query += ` AND owner_id = ?`;
101
+ params.push(filter.ownerId);
102
+ }
103
+ if (filter?.category) {
104
+ query += ` AND category = ?`;
105
+ params.push(filter.category);
106
+ }
107
+ query += ` ORDER BY name`;
108
+ const rows = db.prepare(query).all(...params);
109
+ return rows.map(row => ({
110
+ id: row.id,
111
+ gameId: row.game_id,
112
+ ownerId: row.owner_id,
113
+ ownerType: row.owner_type,
114
+ name: row.name,
115
+ description: row.description,
116
+ category: row.category,
117
+ cost: safeJsonParse(row.cost || "{}", {}),
118
+ cooldown: row.cooldown,
119
+ currentCooldown: row.current_cooldown,
120
+ effects: safeJsonParse(row.effects || "[]", []),
121
+ requirements: safeJsonParse(row.requirements || "{}", {}),
122
+ tags: safeJsonParse(row.tags || "[]", []),
123
+ createdAt: row.created_at,
124
+ }));
125
+ }
126
+ export function learnAbility(templateId, characterId) {
127
+ const db = getDatabase();
128
+ const template = getAbility(templateId);
129
+ if (!template || template.ownerType !== "template")
130
+ return null;
131
+ // Validate character exists before creating ability
132
+ const character = getCharacter(characterId);
133
+ if (!character) {
134
+ throw new Error(`Character '${characterId}' not found. Cannot learn ability without a valid character.`);
135
+ }
136
+ const id = uuidv4();
137
+ const now = new Date().toISOString();
138
+ db.prepare(`
139
+ INSERT INTO abilities (id, game_id, owner_id, owner_type, name, description, category, cost, cooldown, effects, requirements, tags, created_at)
140
+ VALUES (?, ?, ?, 'character', ?, ?, ?, ?, ?, ?, ?, ?, ?)
141
+ `).run(id, template.gameId, characterId, template.name, template.description, template.category, JSON.stringify(template.cost), template.cooldown, JSON.stringify(template.effects), JSON.stringify(template.requirements), JSON.stringify(template.tags), now);
142
+ return {
143
+ id,
144
+ gameId: template.gameId,
145
+ ownerId: characterId,
146
+ ownerType: "character",
147
+ name: template.name,
148
+ description: template.description,
149
+ category: template.category,
150
+ cost: template.cost,
151
+ cooldown: template.cooldown,
152
+ currentCooldown: 0,
153
+ effects: template.effects,
154
+ requirements: template.requirements,
155
+ tags: template.tags,
156
+ createdAt: now,
157
+ };
158
+ }
159
+ export function useAbility(abilityId, _characterId) {
160
+ const db = getDatabase();
161
+ const ability = getAbility(abilityId);
162
+ if (!ability) {
163
+ return { success: false, ability: null, reason: "Ability not found" };
164
+ }
165
+ // Check cooldown
166
+ if (ability.currentCooldown > 0) {
167
+ return {
168
+ success: false,
169
+ ability,
170
+ reason: `Ability on cooldown (${ability.currentCooldown} rounds remaining)`,
171
+ };
172
+ }
173
+ // Check costs (would need character resources to validate)
174
+ // For now, we just set the cooldown and return success
175
+ // In a full implementation, we'd deduct from character resources
176
+ if (ability.cooldown && ability.cooldown > 0) {
177
+ db.prepare(`UPDATE abilities SET current_cooldown = ? WHERE id = ?`)
178
+ .run(ability.cooldown, abilityId);
179
+ }
180
+ return {
181
+ success: true,
182
+ ability: { ...ability, currentCooldown: ability.cooldown || 0 },
183
+ costsPaid: ability.cost,
184
+ };
185
+ }
186
+ export function tickCooldowns(gameId, amount = 1) {
187
+ const db = getDatabase();
188
+ // Get all abilities on cooldown
189
+ const rows = db.prepare(`
190
+ SELECT * FROM abilities WHERE game_id = ? AND current_cooldown > 0
191
+ `).all(gameId);
192
+ const updated = [];
193
+ for (const row of rows) {
194
+ const currentCooldown = row.current_cooldown;
195
+ const newCooldown = Math.max(0, currentCooldown - amount);
196
+ db.prepare(`UPDATE abilities SET current_cooldown = ? WHERE id = ?`)
197
+ .run(newCooldown, row.id);
198
+ updated.push({
199
+ id: row.id,
200
+ gameId: row.game_id,
201
+ ownerId: row.owner_id,
202
+ ownerType: row.owner_type,
203
+ name: row.name,
204
+ description: row.description,
205
+ category: row.category,
206
+ cost: safeJsonParse(row.cost || "{}", {}),
207
+ cooldown: row.cooldown,
208
+ currentCooldown: newCooldown,
209
+ effects: safeJsonParse(row.effects || "[]", []),
210
+ requirements: safeJsonParse(row.requirements || "{}", {}),
211
+ tags: safeJsonParse(row.tags || "[]", []),
212
+ createdAt: row.created_at,
213
+ });
214
+ }
215
+ return updated;
216
+ }
217
+ export function checkRequirements(abilityId, characterId) {
218
+ const ability = getAbility(abilityId);
219
+ if (!ability)
220
+ return null;
221
+ const character = getCharacter(characterId);
222
+ if (!character)
223
+ return null;
224
+ const missing = {};
225
+ for (const [key, required] of Object.entries(ability.requirements)) {
226
+ // Check attributes first, then status
227
+ const actual = character.attributes[key]
228
+ ?? character.status[key]
229
+ ?? 0;
230
+ if (actual < required) {
231
+ missing[key] = { required, actual };
232
+ }
233
+ }
234
+ return {
235
+ meetsRequirements: Object.keys(missing).length === 0,
236
+ missing,
237
+ };
238
+ }
@@ -0,0 +1,24 @@
1
+ import type { StoredAudio, StoreAudioParams, AudioListResult, VoiceReferenceResult } from "../types/index.js";
2
+ export declare function storeAudio(params: StoreAudioParams): Promise<StoredAudio>;
3
+ export declare function getAudio(audioId: string): StoredAudio | null;
4
+ export declare function getAudioFilePath(audioId: string): string | null;
5
+ export declare function getAudioData(audioId: string): {
6
+ audio: StoredAudio;
7
+ base64: string;
8
+ } | null;
9
+ export declare function listEntityAudio(entityId: string, entityType: string): AudioListResult;
10
+ export declare function listGameAudio(gameId: string): StoredAudio[];
11
+ export declare function listVoiceReferences(gameId: string, characterId?: string): StoredAudio[];
12
+ export declare function getCharacterVoiceReferences(gameId: string, characterId: string): VoiceReferenceResult | null;
13
+ export declare function deleteAudio(audioId: string): boolean;
14
+ export declare function setPrimaryAudio(audioId: string): StoredAudio | null;
15
+ export declare function updateAudioMetadata(audioId: string, updates: {
16
+ label?: string;
17
+ description?: string;
18
+ ttsText?: string;
19
+ voiceName?: string;
20
+ voiceDescription?: string;
21
+ isVoiceReference?: boolean;
22
+ }): StoredAudio | null;
23
+ export declare function deleteGameAudio(gameId: string): number;
24
+ export declare function getPrimaryAudio(entityId: string, entityType: string): StoredAudio | null;
@@ -0,0 +1,365 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase, getDataDir, withTransaction } from "../db/connection.js";
3
+ import { writeFileSync, readFileSync, mkdirSync, existsSync, unlinkSync, rmSync, } from "fs";
4
+ import { dirname, join, extname } from "path";
5
+ import { getCharacter } from "./character.js";
6
+ import { getLocation } from "./world.js";
7
+ import { getFaction } from "./faction.js";
8
+ // Use the same data directory as the database, but in an 'audio' subdirectory
9
+ function getAudioDir() {
10
+ return join(getDataDir(), "audio");
11
+ }
12
+ // Helper: Validate entity exists and return its name
13
+ function validateEntityAndGetName(entityId, entityType) {
14
+ let entity = null;
15
+ switch (entityType) {
16
+ case "character":
17
+ entity = getCharacter(entityId);
18
+ break;
19
+ case "location":
20
+ entity = getLocation(entityId);
21
+ break;
22
+ case "faction":
23
+ entity = getFaction(entityId);
24
+ break;
25
+ default:
26
+ // For other entity types (game, scene, narration, etc.), skip validation
27
+ return entityType.charAt(0).toUpperCase() + entityType.slice(1);
28
+ }
29
+ if (!entity) {
30
+ throw new Error(`${entityType} not found: ${entityId}`);
31
+ }
32
+ return entity.name;
33
+ }
34
+ // Helper: Ensure directory exists
35
+ function ensureDir(dirPath) {
36
+ if (!existsSync(dirPath)) {
37
+ mkdirSync(dirPath, { recursive: true });
38
+ }
39
+ }
40
+ // Helper: Get extension from mime type
41
+ function getExtension(mimeType) {
42
+ const map = {
43
+ "audio/mpeg": "mp3",
44
+ "audio/mp3": "mp3",
45
+ "audio/wav": "wav",
46
+ "audio/wave": "wav",
47
+ "audio/x-wav": "wav",
48
+ "audio/ogg": "ogg",
49
+ "audio/flac": "flac",
50
+ "audio/aac": "aac",
51
+ "audio/m4a": "m4a",
52
+ "audio/mp4": "m4a",
53
+ "audio/webm": "webm",
54
+ };
55
+ return map[mimeType] || "mp3";
56
+ }
57
+ // Helper: Get mime type from extension
58
+ function getMimeType(ext) {
59
+ const map = {
60
+ mp3: "audio/mpeg",
61
+ wav: "audio/wav",
62
+ ogg: "audio/ogg",
63
+ flac: "audio/flac",
64
+ aac: "audio/aac",
65
+ m4a: "audio/m4a",
66
+ webm: "audio/webm",
67
+ };
68
+ return map[ext.toLowerCase().replace(".", "")] || "audio/mpeg";
69
+ }
70
+ // Helper: Map database row to StoredAudio
71
+ function mapRowToStoredAudio(row) {
72
+ return {
73
+ id: row.id,
74
+ gameId: row.game_id,
75
+ entityId: row.entity_id,
76
+ entityType: row.entity_type,
77
+ filePath: row.file_path,
78
+ fileSize: row.file_size,
79
+ mimeType: row.mime_type,
80
+ durationMs: row.duration_ms,
81
+ sampleRate: row.sample_rate,
82
+ label: row.label,
83
+ description: row.description,
84
+ source: row.source,
85
+ sourceUrl: row.source_url,
86
+ ttsEngine: row.tts_engine,
87
+ ttsVoice: row.tts_voice,
88
+ ttsText: row.tts_text,
89
+ ttsSettings: row.tts_settings ? JSON.parse(row.tts_settings) : null,
90
+ isVoiceReference: row.is_voice_reference === 1,
91
+ voiceName: row.voice_name,
92
+ voiceDescription: row.voice_description,
93
+ isPrimary: row.is_primary === 1,
94
+ createdAt: row.created_at,
95
+ };
96
+ }
97
+ export async function storeAudio(params) {
98
+ // Validate entity exists and get its name for confirmation
99
+ const entityName = validateEntityAndGetName(params.entityId, params.entityType);
100
+ const db = getDatabase();
101
+ const id = uuidv4();
102
+ const now = new Date().toISOString();
103
+ let audioBuffer;
104
+ let mimeType;
105
+ let source;
106
+ let sourceUrl = null;
107
+ if (params.url) {
108
+ // Fetch audio from URL
109
+ const response = await fetch(params.url);
110
+ if (!response.ok) {
111
+ throw new Error(`Failed to fetch audio: ${response.status}`);
112
+ }
113
+ audioBuffer = Buffer.from(await response.arrayBuffer());
114
+ mimeType = response.headers.get("content-type") || params.mimeType || "audio/mpeg";
115
+ source = params.ttsEngine ? "generated" : "url";
116
+ sourceUrl = params.url;
117
+ }
118
+ else if (params.filePath) {
119
+ // Copy from local file
120
+ if (!existsSync(params.filePath)) {
121
+ throw new Error(`File not found: ${params.filePath}`);
122
+ }
123
+ audioBuffer = readFileSync(params.filePath);
124
+ const ext = extname(params.filePath).slice(1);
125
+ mimeType = params.mimeType || getMimeType(ext);
126
+ source = params.ttsEngine ? "generated" : "uploaded";
127
+ }
128
+ else {
129
+ throw new Error("Either url or filePath must be provided");
130
+ }
131
+ // Build file path
132
+ const ext = getExtension(mimeType);
133
+ const relativePath = join(params.gameId, `${params.entityType}s`, params.entityId, `${id}.${ext}`);
134
+ const fullPath = join(getAudioDir(), relativePath);
135
+ // Ensure directory exists and write file
136
+ ensureDir(dirname(fullPath));
137
+ writeFileSync(fullPath, audioBuffer);
138
+ // If setting as primary, unset current primary first
139
+ if (params.setAsPrimary) {
140
+ db.prepare(`
141
+ UPDATE stored_audio SET is_primary = 0
142
+ WHERE entity_id = ? AND entity_type = ?
143
+ `).run(params.entityId, params.entityType);
144
+ }
145
+ // Insert database record
146
+ const stmt = db.prepare(`
147
+ INSERT INTO stored_audio (
148
+ id, game_id, entity_id, entity_type, file_path, file_size, mime_type,
149
+ duration_ms, sample_rate, label, description, source, source_url,
150
+ tts_engine, tts_voice, tts_text, tts_settings,
151
+ is_voice_reference, voice_name, voice_description,
152
+ is_primary, created_at
153
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
154
+ `);
155
+ stmt.run(id, params.gameId, params.entityId, params.entityType, relativePath, audioBuffer.length, mimeType, null, // duration_ms - would need audio parsing library
156
+ null, // sample_rate - would need audio parsing library
157
+ params.label || null, params.description || null, source, sourceUrl, params.ttsEngine || null, params.ttsVoice || null, params.ttsText || null, params.ttsSettings ? JSON.stringify(params.ttsSettings) : null, params.isVoiceReference ? 1 : 0, params.voiceName || null, params.voiceDescription || null, params.setAsPrimary ? 1 : 0, now);
158
+ return {
159
+ id,
160
+ gameId: params.gameId,
161
+ entityId: params.entityId,
162
+ entityType: params.entityType,
163
+ entityName,
164
+ filePath: relativePath,
165
+ fileSize: audioBuffer.length,
166
+ mimeType,
167
+ durationMs: null,
168
+ sampleRate: null,
169
+ label: params.label || null,
170
+ description: params.description || null,
171
+ source,
172
+ sourceUrl,
173
+ ttsEngine: params.ttsEngine || null,
174
+ ttsVoice: params.ttsVoice || null,
175
+ ttsText: params.ttsText || null,
176
+ ttsSettings: params.ttsSettings || null,
177
+ isVoiceReference: params.isVoiceReference || false,
178
+ voiceName: params.voiceName || null,
179
+ voiceDescription: params.voiceDescription || null,
180
+ isPrimary: params.setAsPrimary || false,
181
+ createdAt: now,
182
+ };
183
+ }
184
+ export function getAudio(audioId) {
185
+ const db = getDatabase();
186
+ const row = db.prepare(`SELECT * FROM stored_audio WHERE id = ?`).get(audioId);
187
+ if (!row)
188
+ return null;
189
+ return mapRowToStoredAudio(row);
190
+ }
191
+ export function getAudioFilePath(audioId) {
192
+ const audio = getAudio(audioId);
193
+ if (!audio)
194
+ return null;
195
+ const fullPath = join(getAudioDir(), audio.filePath);
196
+ if (!existsSync(fullPath))
197
+ return null;
198
+ return fullPath;
199
+ }
200
+ export function getAudioData(audioId) {
201
+ const audio = getAudio(audioId);
202
+ if (!audio)
203
+ return null;
204
+ const fullPath = join(getAudioDir(), audio.filePath);
205
+ if (!existsSync(fullPath))
206
+ return null;
207
+ const buffer = readFileSync(fullPath);
208
+ const base64 = `data:${audio.mimeType};base64,${buffer.toString("base64")}`;
209
+ return { audio, base64 };
210
+ }
211
+ export function listEntityAudio(entityId, entityType) {
212
+ const db = getDatabase();
213
+ const rows = db
214
+ .prepare(`
215
+ SELECT * FROM stored_audio
216
+ WHERE entity_id = ? AND entity_type = ?
217
+ ORDER BY is_primary DESC, created_at DESC
218
+ `)
219
+ .all(entityId, entityType);
220
+ const audioFiles = rows.map(mapRowToStoredAudio);
221
+ const primaryAudio = audioFiles.find((a) => a.isPrimary) || null;
222
+ return { entityId, entityType, audioFiles, primaryAudio };
223
+ }
224
+ export function listGameAudio(gameId) {
225
+ const db = getDatabase();
226
+ const rows = db
227
+ .prepare(`
228
+ SELECT * FROM stored_audio
229
+ WHERE game_id = ?
230
+ ORDER BY created_at DESC
231
+ `)
232
+ .all(gameId);
233
+ return rows.map(mapRowToStoredAudio);
234
+ }
235
+ export function listVoiceReferences(gameId, characterId) {
236
+ const db = getDatabase();
237
+ if (characterId) {
238
+ const rows = db
239
+ .prepare(`
240
+ SELECT * FROM stored_audio
241
+ WHERE game_id = ? AND entity_id = ? AND is_voice_reference = 1
242
+ ORDER BY created_at DESC
243
+ `)
244
+ .all(gameId, characterId);
245
+ return rows.map(mapRowToStoredAudio);
246
+ }
247
+ else {
248
+ const rows = db
249
+ .prepare(`
250
+ SELECT * FROM stored_audio
251
+ WHERE game_id = ? AND is_voice_reference = 1
252
+ ORDER BY entity_id, created_at DESC
253
+ `)
254
+ .all(gameId);
255
+ return rows.map(mapRowToStoredAudio);
256
+ }
257
+ }
258
+ export function getCharacterVoiceReferences(gameId, characterId) {
259
+ const character = getCharacter(characterId);
260
+ if (!character)
261
+ return null;
262
+ const voiceRefs = listVoiceReferences(gameId, characterId);
263
+ // Build full file paths for convenience (ready to pass to TTS tools)
264
+ const audioDir = getAudioDir();
265
+ const filePaths = voiceRefs
266
+ .map((ref) => {
267
+ const fullPath = join(audioDir, ref.filePath);
268
+ return existsSync(fullPath) ? fullPath : null;
269
+ })
270
+ .filter((p) => p !== null);
271
+ return {
272
+ gameId,
273
+ characterId,
274
+ characterName: character.name,
275
+ voiceReferences: voiceRefs,
276
+ filePaths,
277
+ totalCount: voiceRefs.length,
278
+ };
279
+ }
280
+ export function deleteAudio(audioId) {
281
+ const db = getDatabase();
282
+ const audio = getAudio(audioId);
283
+ if (!audio)
284
+ return false;
285
+ // Delete file
286
+ const fullPath = join(getAudioDir(), audio.filePath);
287
+ if (existsSync(fullPath)) {
288
+ unlinkSync(fullPath);
289
+ }
290
+ // Delete database record
291
+ const result = db.prepare(`DELETE FROM stored_audio WHERE id = ?`).run(audioId);
292
+ return result.changes > 0;
293
+ }
294
+ export function setPrimaryAudio(audioId) {
295
+ const audio = getAudio(audioId);
296
+ if (!audio)
297
+ return null;
298
+ // Unsetting the old primary and setting the new one must happen
299
+ // together -- a failure between the two could otherwise leave the entity
300
+ // with zero primaries, or briefly with two.
301
+ withTransaction(() => {
302
+ const db = getDatabase();
303
+ // Unset current primary
304
+ db.prepare(`
305
+ UPDATE stored_audio SET is_primary = 0
306
+ WHERE entity_id = ? AND entity_type = ?
307
+ `).run(audio.entityId, audio.entityType);
308
+ // Set new primary
309
+ db.prepare(`UPDATE stored_audio SET is_primary = 1 WHERE id = ?`).run(audioId);
310
+ });
311
+ return { ...audio, isPrimary: true };
312
+ }
313
+ export function updateAudioMetadata(audioId, updates) {
314
+ const db = getDatabase();
315
+ const current = getAudio(audioId);
316
+ if (!current)
317
+ return null;
318
+ const newLabel = updates.label !== undefined ? updates.label : current.label;
319
+ const newDescription = updates.description !== undefined ? updates.description : current.description;
320
+ const newTtsText = updates.ttsText !== undefined ? updates.ttsText : current.ttsText;
321
+ const newVoiceName = updates.voiceName !== undefined ? updates.voiceName : current.voiceName;
322
+ const newVoiceDescription = updates.voiceDescription !== undefined ? updates.voiceDescription : current.voiceDescription;
323
+ const newIsVoiceReference = updates.isVoiceReference !== undefined ? updates.isVoiceReference : current.isVoiceReference;
324
+ db.prepare(`
325
+ UPDATE stored_audio
326
+ SET label = ?, description = ?, tts_text = ?, voice_name = ?, voice_description = ?, is_voice_reference = ?
327
+ WHERE id = ?
328
+ `).run(newLabel, newDescription, newTtsText, newVoiceName, newVoiceDescription, newIsVoiceReference ? 1 : 0, audioId);
329
+ return {
330
+ ...current,
331
+ label: newLabel,
332
+ description: newDescription,
333
+ ttsText: newTtsText,
334
+ voiceName: newVoiceName,
335
+ voiceDescription: newVoiceDescription,
336
+ isVoiceReference: newIsVoiceReference,
337
+ };
338
+ }
339
+ // Cleanup helper - delete all audio for a game
340
+ export function deleteGameAudio(gameId) {
341
+ const db = getDatabase();
342
+ // Delete files
343
+ const gameDir = join(getAudioDir(), gameId);
344
+ if (existsSync(gameDir)) {
345
+ rmSync(gameDir, { recursive: true, force: true });
346
+ }
347
+ // Delete records (cascade should handle this, but be explicit)
348
+ const result = db
349
+ .prepare(`DELETE FROM stored_audio WHERE game_id = ?`)
350
+ .run(gameId);
351
+ return result.changes;
352
+ }
353
+ // Get primary audio for an entity
354
+ export function getPrimaryAudio(entityId, entityType) {
355
+ const db = getDatabase();
356
+ const row = db
357
+ .prepare(`
358
+ SELECT * FROM stored_audio
359
+ WHERE entity_id = ? AND entity_type = ? AND is_primary = 1
360
+ `)
361
+ .get(entityId, entityType);
362
+ if (!row)
363
+ return null;
364
+ return mapRowToStoredAudio(row);
365
+ }
@@ -0,0 +1,70 @@
1
+ import type { Character, CharacterStatus, VoiceDescription, ImageGeneration } from "../types/index.js";
2
+ export declare function createCharacter(params: {
3
+ gameId: string;
4
+ name: string;
5
+ isPlayer: boolean;
6
+ attributes?: Record<string, number>;
7
+ skills?: Record<string, number>;
8
+ status?: Partial<CharacterStatus>;
9
+ locationId?: string;
10
+ notes?: string;
11
+ voice?: VoiceDescription;
12
+ imageGen?: ImageGeneration;
13
+ }): Character;
14
+ export declare function getCharacter(id: string): Character | null;
15
+ export declare function updateCharacter(id: string, updates: {
16
+ name?: string;
17
+ attributes?: Record<string, number>;
18
+ skills?: Record<string, number>;
19
+ status?: Partial<CharacterStatus>;
20
+ locationId?: string | null;
21
+ notes?: string;
22
+ voice?: VoiceDescription | null;
23
+ imageGen?: ImageGeneration | null;
24
+ }): Character | null;
25
+ export declare function deleteCharacter(id: string): boolean;
26
+ export declare function listCharacters(gameId: string, filter?: {
27
+ isPlayer?: boolean;
28
+ locationId?: string;
29
+ }): Character[];
30
+ /**
31
+ * Find a character by name (case-insensitive, fuzzy match).
32
+ * Returns the best match or null if no reasonable match found.
33
+ */
34
+ export declare function getCharacterByName(gameId: string, name: string): Character | null;
35
+ export declare function moveCharacter(characterId: string, locationId: string | null): boolean;
36
+ /**
37
+ * Modify a character's health - damage or heal in a single call.
38
+ * Use mode: "damage" to reduce health, mode: "heal" to restore health.
39
+ */
40
+ export declare function modifyHealth(characterId: string, params: {
41
+ mode: "damage" | "heal";
42
+ amount: number;
43
+ }): {
44
+ character: Character;
45
+ previousHealth: number;
46
+ newHealth: number;
47
+ } | null;
48
+ export declare function addCondition(characterId: string, condition: string): Character | null;
49
+ export declare function removeCondition(characterId: string, condition: string): Character | null;
50
+ /**
51
+ * Modify conditions on a character - add or remove in a single call.
52
+ * This consolidated function reduces the number of tool calls needed.
53
+ */
54
+ export declare function modifyConditions(characterId: string, params: {
55
+ add?: string[];
56
+ remove?: string[];
57
+ }): {
58
+ character: Character;
59
+ added: string[];
60
+ removed: string[];
61
+ } | null;
62
+ export interface CharacterSheetData {
63
+ character: Character;
64
+ locationName: string | null;
65
+ inventory: Array<{
66
+ name: string;
67
+ type?: string;
68
+ }>;
69
+ }
70
+ export declare function renderCharacterSheet(characterId: string): CharacterSheetData | null;