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,34 @@
1
+ import type { Quest, QuestObjective } from "../types/index.js";
2
+ export declare function createQuest(params: {
3
+ gameId: string;
4
+ name: string;
5
+ description: string;
6
+ objectives: Omit<QuestObjective, "id">[];
7
+ rewards?: string;
8
+ }): Quest;
9
+ export declare function getQuest(id: string): Quest | null;
10
+ export declare function updateQuest(id: string, updates: {
11
+ name?: string;
12
+ description?: string;
13
+ status?: Quest["status"];
14
+ rewards?: string;
15
+ }): Quest | null;
16
+ /**
17
+ * Modify quest objectives - add new objectives and/or complete existing ones in a single call.
18
+ */
19
+ export declare function modifyObjectives(questId: string, params: {
20
+ add?: Array<{
21
+ description: string;
22
+ optional?: boolean;
23
+ }>;
24
+ complete?: string[];
25
+ }): {
26
+ quest: Quest;
27
+ added: QuestObjective[];
28
+ completed: string[];
29
+ } | null;
30
+ export declare function deleteQuest(id: string): boolean;
31
+ export declare function listQuests(gameId: string, filter?: {
32
+ status?: Quest["status"];
33
+ }): Quest[];
34
+ export declare function getActiveQuests(gameId: string): Quest[];
@@ -0,0 +1,164 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase } from "../db/connection.js";
3
+ import { safeJsonParse } from "../utils/json.js";
4
+ import { gameEvents } from "../events/emitter.js";
5
+ import { validateGameExists } from "./game.js";
6
+ export function createQuest(params) {
7
+ // Validate game exists to prevent orphaned records
8
+ validateGameExists(params.gameId);
9
+ const db = getDatabase();
10
+ const id = uuidv4();
11
+ const objectives = params.objectives.map((obj) => ({
12
+ id: uuidv4(),
13
+ description: obj.description,
14
+ completed: obj.completed || false,
15
+ optional: obj.optional,
16
+ }));
17
+ const stmt = db.prepare(`
18
+ INSERT INTO quests (id, game_id, name, description, objectives, status, rewards)
19
+ VALUES (?, ?, ?, ?, ?, 'active', ?)
20
+ `);
21
+ stmt.run(id, params.gameId, params.name, params.description, JSON.stringify(objectives), params.rewards || null);
22
+ return {
23
+ id,
24
+ gameId: params.gameId,
25
+ name: params.name,
26
+ description: params.description,
27
+ objectives,
28
+ status: "active",
29
+ rewards: params.rewards,
30
+ };
31
+ }
32
+ export function getQuest(id) {
33
+ const db = getDatabase();
34
+ const stmt = db.prepare(`SELECT * FROM quests WHERE id = ?`);
35
+ const row = stmt.get(id);
36
+ if (!row)
37
+ return null;
38
+ return {
39
+ id: row.id,
40
+ gameId: row.game_id,
41
+ name: row.name,
42
+ description: row.description,
43
+ objectives: safeJsonParse(row.objectives, []),
44
+ status: row.status,
45
+ rewards: row.rewards,
46
+ };
47
+ }
48
+ export function updateQuest(id, updates) {
49
+ const db = getDatabase();
50
+ const current = getQuest(id);
51
+ if (!current)
52
+ return null;
53
+ const newName = updates.name ?? current.name;
54
+ const newDescription = updates.description ?? current.description;
55
+ const newStatus = updates.status ?? current.status;
56
+ const newRewards = updates.rewards ?? current.rewards;
57
+ const stmt = db.prepare(`
58
+ UPDATE quests SET name = ?, description = ?, status = ?, rewards = ? WHERE id = ?
59
+ `);
60
+ stmt.run(newName, newDescription, newStatus, newRewards || null, id);
61
+ const updated = {
62
+ ...current,
63
+ name: newName,
64
+ description: newDescription,
65
+ status: newStatus,
66
+ rewards: newRewards,
67
+ };
68
+ // Emit realtime event
69
+ gameEvents.emit({
70
+ type: "quest:updated",
71
+ gameId: current.gameId,
72
+ entityId: id,
73
+ entityType: "quest",
74
+ timestamp: new Date().toISOString(),
75
+ data: { name: newName, status: newStatus },
76
+ });
77
+ return updated;
78
+ }
79
+ /**
80
+ * Modify quest objectives - add new objectives and/or complete existing ones in a single call.
81
+ */
82
+ export function modifyObjectives(questId, params) {
83
+ const db = getDatabase();
84
+ const quest = getQuest(questId);
85
+ if (!quest)
86
+ return null;
87
+ const objectives = [...quest.objectives];
88
+ const added = [];
89
+ const completed = [];
90
+ // Complete objectives first
91
+ if (params.complete) {
92
+ for (const objectiveId of params.complete) {
93
+ const idx = objectives.findIndex((o) => o.id === objectiveId);
94
+ if (idx !== -1 && !objectives[idx].completed) {
95
+ objectives[idx] = { ...objectives[idx], completed: true };
96
+ completed.push(objectiveId);
97
+ }
98
+ }
99
+ }
100
+ // Add new objectives
101
+ if (params.add) {
102
+ for (const obj of params.add) {
103
+ const newObjective = {
104
+ id: uuidv4(),
105
+ description: obj.description,
106
+ completed: false,
107
+ optional: obj.optional,
108
+ };
109
+ objectives.push(newObjective);
110
+ added.push(newObjective);
111
+ }
112
+ }
113
+ // Check if all required objectives are complete
114
+ const requiredComplete = objectives
115
+ .filter((o) => !o.optional)
116
+ .every((o) => o.completed);
117
+ const shouldComplete = requiredComplete && quest.status === "active";
118
+ const newStatus = shouldComplete ? "completed" : quest.status;
119
+ // Update objectives and status atomically in a single statement
120
+ const stmt = db.prepare(`UPDATE quests SET objectives = ?, status = ? WHERE id = ?`);
121
+ stmt.run(JSON.stringify(objectives), newStatus, questId);
122
+ // Emit event if quest was completed
123
+ if (shouldComplete) {
124
+ gameEvents.emit({
125
+ type: "quest:updated",
126
+ gameId: quest.gameId,
127
+ entityId: questId,
128
+ entityType: "quest",
129
+ timestamp: new Date().toISOString(),
130
+ data: { name: quest.name, status: "completed" },
131
+ });
132
+ }
133
+ const finalQuest = { ...quest, objectives, status: newStatus };
134
+ return { quest: finalQuest, added, completed };
135
+ }
136
+ export function deleteQuest(id) {
137
+ const db = getDatabase();
138
+ const stmt = db.prepare(`DELETE FROM quests WHERE id = ?`);
139
+ const result = stmt.run(id);
140
+ return result.changes > 0;
141
+ }
142
+ export function listQuests(gameId, filter) {
143
+ const db = getDatabase();
144
+ let query = `SELECT * FROM quests WHERE game_id = ?`;
145
+ const params = [gameId];
146
+ if (filter?.status) {
147
+ query += ` AND status = ?`;
148
+ params.push(filter.status);
149
+ }
150
+ const stmt = db.prepare(query);
151
+ const rows = stmt.all(...params);
152
+ return rows.map((row) => ({
153
+ id: row.id,
154
+ gameId: row.game_id,
155
+ name: row.name,
156
+ description: row.description,
157
+ objectives: safeJsonParse(row.objectives, []),
158
+ status: row.status,
159
+ rewards: row.rewards,
160
+ }));
161
+ }
162
+ export function getActiveQuests(gameId) {
163
+ return listQuests(gameId, { status: "active" });
164
+ }
@@ -0,0 +1,74 @@
1
+ import type { Relationship, RelationshipChange } from "../types/index.js";
2
+ export declare function createRelationship(params: {
3
+ gameId: string;
4
+ sourceId: string;
5
+ sourceType: string;
6
+ targetId: string;
7
+ targetType: string;
8
+ relationshipType: string;
9
+ value?: number;
10
+ label?: string;
11
+ notes?: string;
12
+ }): Relationship;
13
+ export declare function getRelationship(id: string): Relationship | null;
14
+ export declare function getRelationshipBetween(gameId: string, sourceId: string, targetId: string, relationshipType?: string): Relationship | null;
15
+ export declare function updateRelationship(id: string, updates: {
16
+ relationshipType?: string;
17
+ value?: number;
18
+ label?: string | null;
19
+ notes?: string;
20
+ }): Relationship | null;
21
+ export declare function modifyRelationship(params: {
22
+ relationshipId: string;
23
+ delta: number;
24
+ reason?: string;
25
+ minValue?: number;
26
+ maxValue?: number;
27
+ }): {
28
+ relationship: Relationship;
29
+ change: RelationshipChange;
30
+ } | null;
31
+ export declare function deleteRelationship(id: string): boolean;
32
+ export declare function listRelationships(gameId: string, filter?: {
33
+ entityId?: string;
34
+ sourceId?: string;
35
+ targetId?: string;
36
+ relationshipType?: string;
37
+ entityType?: string;
38
+ }): Relationship[];
39
+ export declare function getRelationshipHistory(relationshipId: string, limit?: number): RelationshipChange[];
40
+ /**
41
+ * Update a relationship value with optional metadata changes. Supports both direct set and delta modes.
42
+ * Logs to history when value changes.
43
+ */
44
+ export declare function updateRelationshipValue(params: {
45
+ relationshipId: string;
46
+ mode: "delta" | "set";
47
+ value: number;
48
+ reason?: string;
49
+ minValue?: number;
50
+ maxValue?: number;
51
+ relationshipType?: string;
52
+ label?: string | null;
53
+ notes?: string;
54
+ }): {
55
+ relationship: Relationship;
56
+ change: RelationshipChange | null;
57
+ previousValue: number;
58
+ } | null;
59
+ export declare function getRelationshipLabel(value: number): string;
60
+ export declare function createBidirectionalRelationship(params: {
61
+ gameId: string;
62
+ entityA: {
63
+ id: string;
64
+ type: string;
65
+ };
66
+ entityB: {
67
+ id: string;
68
+ type: string;
69
+ };
70
+ relationshipType: string;
71
+ value?: number;
72
+ label?: string;
73
+ notes?: string;
74
+ }): [Relationship, Relationship];
@@ -0,0 +1,324 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase, withTransaction } from "../db/connection.js";
3
+ import { validateGameExists } from "./game.js";
4
+ export function createRelationship(params) {
5
+ // Validate game exists to prevent orphaned records
6
+ validateGameExists(params.gameId);
7
+ const db = getDatabase();
8
+ const id = uuidv4();
9
+ const now = new Date().toISOString();
10
+ db.prepare(`
11
+ INSERT INTO relationships (id, game_id, source_id, source_type, target_id, target_type, relationship_type, value, label, notes, created_at, updated_at)
12
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
13
+ `).run(id, params.gameId, params.sourceId, params.sourceType, params.targetId, params.targetType, params.relationshipType, params.value ?? 0, params.label || null, params.notes || "", now, now);
14
+ return {
15
+ id,
16
+ gameId: params.gameId,
17
+ sourceId: params.sourceId,
18
+ sourceType: params.sourceType,
19
+ targetId: params.targetId,
20
+ targetType: params.targetType,
21
+ relationshipType: params.relationshipType,
22
+ value: params.value ?? 0,
23
+ label: params.label || null,
24
+ notes: params.notes || "",
25
+ createdAt: now,
26
+ updatedAt: now,
27
+ };
28
+ }
29
+ export function getRelationship(id) {
30
+ const db = getDatabase();
31
+ const row = db.prepare(`SELECT * FROM relationships WHERE id = ?`).get(id);
32
+ if (!row)
33
+ return null;
34
+ return {
35
+ id: row.id,
36
+ gameId: row.game_id,
37
+ sourceId: row.source_id,
38
+ sourceType: row.source_type,
39
+ targetId: row.target_id,
40
+ targetType: row.target_type,
41
+ relationshipType: row.relationship_type,
42
+ value: row.value,
43
+ label: row.label,
44
+ notes: row.notes,
45
+ createdAt: row.created_at,
46
+ updatedAt: row.updated_at,
47
+ };
48
+ }
49
+ export function getRelationshipBetween(gameId, sourceId, targetId, relationshipType) {
50
+ const db = getDatabase();
51
+ let query = `SELECT * FROM relationships WHERE game_id = ? AND source_id = ? AND target_id = ?`;
52
+ const params = [gameId, sourceId, targetId];
53
+ if (relationshipType) {
54
+ query += ` AND relationship_type = ?`;
55
+ params.push(relationshipType);
56
+ }
57
+ query += ` LIMIT 1`;
58
+ const row = db.prepare(query).get(...params);
59
+ if (!row)
60
+ return null;
61
+ return {
62
+ id: row.id,
63
+ gameId: row.game_id,
64
+ sourceId: row.source_id,
65
+ sourceType: row.source_type,
66
+ targetId: row.target_id,
67
+ targetType: row.target_type,
68
+ relationshipType: row.relationship_type,
69
+ value: row.value,
70
+ label: row.label,
71
+ notes: row.notes,
72
+ createdAt: row.created_at,
73
+ updatedAt: row.updated_at,
74
+ };
75
+ }
76
+ export function updateRelationship(id, updates) {
77
+ const db = getDatabase();
78
+ const current = getRelationship(id);
79
+ if (!current)
80
+ return null;
81
+ const now = new Date().toISOString();
82
+ const newType = updates.relationshipType ?? current.relationshipType;
83
+ const newValue = updates.value ?? current.value;
84
+ const newLabel = updates.label !== undefined ? updates.label : current.label;
85
+ const newNotes = updates.notes ?? current.notes;
86
+ db.prepare(`
87
+ UPDATE relationships
88
+ SET relationship_type = ?, value = ?, label = ?, notes = ?, updated_at = ?
89
+ WHERE id = ?
90
+ `).run(newType, newValue, newLabel, newNotes, now, id);
91
+ return {
92
+ ...current,
93
+ relationshipType: newType,
94
+ value: newValue,
95
+ label: newLabel,
96
+ notes: newNotes,
97
+ updatedAt: now,
98
+ };
99
+ }
100
+ function logRelationshipChange(relationshipId, previousValue, newValue, reason) {
101
+ const db = getDatabase();
102
+ const id = uuidv4();
103
+ const now = new Date().toISOString();
104
+ db.prepare(`
105
+ INSERT INTO relationship_history (id, relationship_id, previous_value, new_value, reason, timestamp)
106
+ VALUES (?, ?, ?, ?, ?, ?)
107
+ `).run(id, relationshipId, previousValue, newValue, reason, now);
108
+ return {
109
+ id,
110
+ relationshipId,
111
+ previousValue,
112
+ newValue,
113
+ reason,
114
+ timestamp: now,
115
+ };
116
+ }
117
+ export function modifyRelationship(params) {
118
+ const db = getDatabase();
119
+ const relationship = getRelationship(params.relationshipId);
120
+ if (!relationship)
121
+ return null;
122
+ // Validate bounds if both are provided
123
+ if (params.minValue !== undefined && params.maxValue !== undefined && params.minValue > params.maxValue) {
124
+ throw new Error(`Invalid bounds: minValue (${params.minValue}) cannot be greater than maxValue (${params.maxValue})`);
125
+ }
126
+ const previousValue = relationship.value;
127
+ let newValue = previousValue + params.delta;
128
+ // Apply bounds (clamp to min first, then max to ensure max takes precedence)
129
+ if (params.minValue !== undefined) {
130
+ newValue = Math.max(newValue, params.minValue);
131
+ }
132
+ if (params.maxValue !== undefined) {
133
+ newValue = Math.min(newValue, params.maxValue);
134
+ }
135
+ const now = new Date().toISOString();
136
+ // The value update and its history row must land together -- otherwise a
137
+ // failure between the two leaves a changed relationship value with no
138
+ // audit trail explaining why it changed.
139
+ const change = withTransaction(() => {
140
+ db.prepare(`UPDATE relationships SET value = ?, updated_at = ? WHERE id = ?`)
141
+ .run(newValue, now, params.relationshipId);
142
+ return logRelationshipChange(params.relationshipId, previousValue, newValue, params.reason || null);
143
+ });
144
+ return {
145
+ relationship: { ...relationship, value: newValue, updatedAt: now },
146
+ change,
147
+ };
148
+ }
149
+ export function deleteRelationship(id) {
150
+ const db = getDatabase();
151
+ const result = db.prepare(`DELETE FROM relationships WHERE id = ?`).run(id);
152
+ return result.changes > 0;
153
+ }
154
+ export function listRelationships(gameId, filter) {
155
+ const db = getDatabase();
156
+ let query = `SELECT * FROM relationships WHERE game_id = ?`;
157
+ const params = [gameId];
158
+ if (filter?.entityId) {
159
+ query += ` AND (source_id = ? OR target_id = ?)`;
160
+ params.push(filter.entityId, filter.entityId);
161
+ }
162
+ if (filter?.sourceId) {
163
+ query += ` AND source_id = ?`;
164
+ params.push(filter.sourceId);
165
+ }
166
+ if (filter?.targetId) {
167
+ query += ` AND target_id = ?`;
168
+ params.push(filter.targetId);
169
+ }
170
+ if (filter?.relationshipType) {
171
+ query += ` AND relationship_type = ?`;
172
+ params.push(filter.relationshipType);
173
+ }
174
+ if (filter?.entityType) {
175
+ query += ` AND (source_type = ? OR target_type = ?)`;
176
+ params.push(filter.entityType, filter.entityType);
177
+ }
178
+ query += ` ORDER BY updated_at DESC`;
179
+ const rows = db.prepare(query).all(...params);
180
+ return rows.map(row => ({
181
+ id: row.id,
182
+ gameId: row.game_id,
183
+ sourceId: row.source_id,
184
+ sourceType: row.source_type,
185
+ targetId: row.target_id,
186
+ targetType: row.target_type,
187
+ relationshipType: row.relationship_type,
188
+ value: row.value,
189
+ label: row.label,
190
+ notes: row.notes,
191
+ createdAt: row.created_at,
192
+ updatedAt: row.updated_at,
193
+ }));
194
+ }
195
+ export function getRelationshipHistory(relationshipId, limit) {
196
+ const db = getDatabase();
197
+ let query = `SELECT * FROM relationship_history WHERE relationship_id = ? ORDER BY timestamp DESC`;
198
+ const params = [relationshipId];
199
+ if (limit) {
200
+ query += ` LIMIT ?`;
201
+ params.push(limit);
202
+ }
203
+ const rows = db.prepare(query).all(...params);
204
+ return rows.map(row => ({
205
+ id: row.id,
206
+ relationshipId: row.relationship_id,
207
+ previousValue: row.previous_value,
208
+ newValue: row.new_value,
209
+ reason: row.reason,
210
+ timestamp: row.timestamp,
211
+ }));
212
+ }
213
+ /**
214
+ * Update a relationship value with optional metadata changes. Supports both direct set and delta modes.
215
+ * Logs to history when value changes.
216
+ */
217
+ export function updateRelationshipValue(params) {
218
+ const db = getDatabase();
219
+ const relationship = getRelationship(params.relationshipId);
220
+ if (!relationship)
221
+ return null;
222
+ // Validate bounds if both are provided
223
+ if (params.minValue !== undefined && params.maxValue !== undefined && params.minValue > params.maxValue) {
224
+ throw new Error(`Invalid bounds: minValue (${params.minValue}) cannot be greater than maxValue (${params.maxValue})`);
225
+ }
226
+ const previousValue = relationship.value;
227
+ let newValue;
228
+ if (params.mode === "delta") {
229
+ newValue = previousValue + params.value;
230
+ }
231
+ else {
232
+ newValue = params.value;
233
+ }
234
+ // Apply bounds (clamp to min first, then max to ensure max takes precedence)
235
+ if (params.minValue !== undefined) {
236
+ newValue = Math.max(newValue, params.minValue);
237
+ }
238
+ if (params.maxValue !== undefined) {
239
+ newValue = Math.min(newValue, params.maxValue);
240
+ }
241
+ const now = new Date().toISOString();
242
+ const newType = params.relationshipType ?? relationship.relationshipType;
243
+ const newLabel = params.label !== undefined ? params.label : relationship.label;
244
+ const newNotes = params.notes ?? relationship.notes;
245
+ // The value/metadata update and its (conditional) history row must land
246
+ // together, for the same reason as modifyRelationship() above.
247
+ const change = withTransaction(() => {
248
+ db.prepare(`
249
+ UPDATE relationships
250
+ SET relationship_type = ?, value = ?, label = ?, notes = ?, updated_at = ?
251
+ WHERE id = ?
252
+ `).run(newType, newValue, newLabel, newNotes, now, params.relationshipId);
253
+ // Log to history if value changed
254
+ if (newValue !== previousValue) {
255
+ return logRelationshipChange(params.relationshipId, previousValue, newValue, params.reason || null);
256
+ }
257
+ return null;
258
+ });
259
+ return {
260
+ relationship: {
261
+ ...relationship,
262
+ relationshipType: newType,
263
+ value: newValue,
264
+ label: newLabel,
265
+ notes: newNotes,
266
+ updatedAt: now,
267
+ },
268
+ change,
269
+ previousValue,
270
+ };
271
+ }
272
+ // Helper to get a value label based on thresholds
273
+ export function getRelationshipLabel(value) {
274
+ if (value >= 80)
275
+ return "devoted";
276
+ if (value >= 60)
277
+ return "friendly";
278
+ if (value >= 40)
279
+ return "warm";
280
+ if (value >= 20)
281
+ return "cordial";
282
+ if (value >= 0)
283
+ return "neutral";
284
+ if (value >= -20)
285
+ return "cool";
286
+ if (value >= -40)
287
+ return "unfriendly";
288
+ if (value >= -60)
289
+ return "hostile";
290
+ if (value >= -80)
291
+ return "hateful";
292
+ return "nemesis";
293
+ }
294
+ // Create a bidirectional relationship (both directions with same initial value)
295
+ export function createBidirectionalRelationship(params) {
296
+ // A "bidirectional" relationship is really two rows. If the second insert
297
+ // fails, we must not be left with a lopsided relationship where A knows
298
+ // about B but not vice versa -- so both inserts happen in one transaction.
299
+ return withTransaction(() => {
300
+ const relA = createRelationship({
301
+ gameId: params.gameId,
302
+ sourceId: params.entityA.id,
303
+ sourceType: params.entityA.type,
304
+ targetId: params.entityB.id,
305
+ targetType: params.entityB.type,
306
+ relationshipType: params.relationshipType,
307
+ value: params.value,
308
+ label: params.label,
309
+ notes: params.notes,
310
+ });
311
+ const relB = createRelationship({
312
+ gameId: params.gameId,
313
+ sourceId: params.entityB.id,
314
+ sourceType: params.entityB.type,
315
+ targetId: params.entityA.id,
316
+ targetType: params.entityA.type,
317
+ relationshipType: params.relationshipType,
318
+ value: params.value,
319
+ label: params.label,
320
+ notes: params.notes,
321
+ });
322
+ return [relA, relB];
323
+ });
324
+ }
@@ -0,0 +1,93 @@
1
+ import type { Resource, ResourceChange } from "../types/index.js";
2
+ export declare function createResource(params: {
3
+ gameId: string;
4
+ ownerType: "game" | "character";
5
+ ownerId?: string;
6
+ name: string;
7
+ description?: string;
8
+ category?: string;
9
+ value?: number;
10
+ minValue?: number;
11
+ maxValue?: number;
12
+ }): Resource;
13
+ export declare function getResource(id: string): Resource | null;
14
+ export declare function updateResource(id: string, updates: {
15
+ name?: string;
16
+ description?: string;
17
+ category?: string | null;
18
+ minValue?: number | null;
19
+ maxValue?: number | null;
20
+ }): Resource | null;
21
+ export declare function deleteResource(id: string): boolean;
22
+ export declare function listResources(gameId: string, filter?: {
23
+ ownerType?: "game" | "character";
24
+ ownerId?: string;
25
+ category?: string;
26
+ }): Resource[];
27
+ /**
28
+ * Update a resource's value - either by delta or absolute set.
29
+ * Use mode: "delta" to add/subtract, mode: "set" to set an absolute value.
30
+ */
31
+ export declare function updateResourceValue(params: {
32
+ resourceId: string;
33
+ mode: "delta" | "set";
34
+ value: number;
35
+ reason?: string;
36
+ }): {
37
+ resource: Resource;
38
+ change: ResourceChange;
39
+ } | null;
40
+ /**
41
+ * Move `amount` from one resource to another, atomically. This is the ONLY
42
+ * write path for a resource that is a member of a declared 'conserved'
43
+ * constraint -- checkResourceConstraints() (constraint.ts) rejects
44
+ * update_resource_value against such a resource specifically because a
45
+ * single-resource write can't express where the counterpart delta comes
46
+ * from. This function is that counterpart-carrying write.
47
+ *
48
+ * WHY AN EXPLICIT TRANSFER TOOL, NOT A BALANCED MULTI-RESOURCE WRITE:
49
+ * A single-resource write against a conserved member is ambiguous -- the
50
+ * server has no way to infer where the offsetting delta should come from.
51
+ * Two designs resolve that ambiguity:
52
+ * (a) an explicit transfer(from, to, amount) tool, or
53
+ * (b) a "balanced write" tool taking an arbitrary { resourceId: delta }
54
+ * map, rejecting the whole call unless every conserved set's deltas
55
+ * sum to zero.
56
+ * (b) is more general -- it can reshuffle N members in one call -- but
57
+ * offloads real bookkeeping onto the caller: an LLM has to enumerate every
58
+ * affected resourceId, get every delta's sign right, and get the
59
+ * grouping-by-constraint-set right, all in one shot with no partial-progress
60
+ * checkpoint if it gets any of that wrong. (a) covers the overwhelming
61
+ * majority of real game actions directly ("trade 10 grain for gold", "the
62
+ * treasury pays the population") and composes: an N-member reshuffle is just
63
+ * several transfers, which the caller can still wrap in one withTransaction()
64
+ * if it needs to be all-or-nothing (see the "chains two transfers" tests in
65
+ * conserved.test.ts). That composability, plus the much smaller surface for
66
+ * a caller to get wrong, is why the narrower primitive was chosen.
67
+ *
68
+ * Scope is deliberately narrow: fromResourceId and toResourceId must already
69
+ * be members of the SAME declared 'conserved' constraint. This is not a
70
+ * general "move value between any two resources" tool -- for anything not
71
+ * under a 'conserved' constraint, update_resource_value remains the right
72
+ * tool (see checkResourceConstraints()). Keeping the two write paths
73
+ * mutually exclusive per resource means which one to use is never
74
+ * ambiguous.
75
+ *
76
+ * Never clamps. Clamping one side of a transfer would apply an uneven delta
77
+ * -- the source would lose less (or the destination gain less) than the
78
+ * other side moved by, silently creating or destroying value -- so any
79
+ * bound violation on either side rejects the whole transfer instead,
80
+ * regardless of whether a 'bounded' constraint is separately declared.
81
+ */
82
+ export declare function transferResourceValue(params: {
83
+ fromResourceId: string;
84
+ toResourceId: string;
85
+ amount: number;
86
+ reason?: string;
87
+ }): {
88
+ from: Resource;
89
+ to: Resource;
90
+ fromChange: ResourceChange;
91
+ toChange: ResourceChange;
92
+ };
93
+ export declare function getResourceHistory(resourceId: string, limit?: number): ResourceChange[];