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,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;
|