@ema.co/mcp-toolkit 0.2.3 → 0.3.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/dist/mcp/handlers-consolidated.js +248 -1
- package/dist/mcp/server.js +44 -3
- package/dist/mcp/tools-consolidated.js +19 -3
- package/dist/sdk/index.js +8 -0
- package/dist/sdk/version-policy.js +328 -0
- package/dist/sdk/version-storage.js +465 -0
- package/dist/sdk/version-tracking.js +346 -0
- package/package.json +1 -1
- package/docs/advisor-comms-assistant-fixes.md +0 -175
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona Version Policy Engine
|
|
3
|
+
*
|
|
4
|
+
* Handles automatic versioning decisions based on policies.
|
|
5
|
+
* Determines when to create snapshots, manages retention, and
|
|
6
|
+
* enforces versioning rules.
|
|
7
|
+
*/
|
|
8
|
+
import { createVersionSnapshot, hashSnapshot, createSnapshotFromPersona, generateChangesSummary, compareVersions, } from "./version-tracking.js";
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Policy Engine Class
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
export class VersionPolicyEngine {
|
|
13
|
+
storage;
|
|
14
|
+
constructor(storage) {
|
|
15
|
+
this.storage = storage;
|
|
16
|
+
}
|
|
17
|
+
// ─────────────── Policy Checks ───────────────
|
|
18
|
+
/**
|
|
19
|
+
* Check if a version should be created for a persona.
|
|
20
|
+
*/
|
|
21
|
+
checkPolicy(persona, trigger) {
|
|
22
|
+
const policy = this.storage.getPolicy(persona.id);
|
|
23
|
+
const snapshot = createSnapshotFromPersona(persona);
|
|
24
|
+
const contentHash = hashSnapshot(snapshot);
|
|
25
|
+
// Check if this would be a duplicate
|
|
26
|
+
const isDuplicate = this.storage.hasContentHash(persona.id, contentHash);
|
|
27
|
+
// Determine if we should create based on trigger and policy
|
|
28
|
+
let shouldCreate = false;
|
|
29
|
+
let reason = "";
|
|
30
|
+
let skipReason;
|
|
31
|
+
switch (trigger) {
|
|
32
|
+
case "manual":
|
|
33
|
+
// Always allow manual creation
|
|
34
|
+
shouldCreate = true;
|
|
35
|
+
reason = "Manual version creation requested";
|
|
36
|
+
break;
|
|
37
|
+
case "deploy":
|
|
38
|
+
if (policy.auto_version_on_deploy) {
|
|
39
|
+
if (isDuplicate) {
|
|
40
|
+
shouldCreate = false;
|
|
41
|
+
skipReason = "Content unchanged from existing version";
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
shouldCreate = true;
|
|
45
|
+
reason = "Auto-versioning on deploy enabled";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
shouldCreate = false;
|
|
50
|
+
skipReason = "Auto-versioning on deploy disabled by policy";
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case "sync":
|
|
54
|
+
if (policy.auto_version_on_sync) {
|
|
55
|
+
if (isDuplicate) {
|
|
56
|
+
shouldCreate = false;
|
|
57
|
+
skipReason = "Content unchanged from existing version";
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
shouldCreate = true;
|
|
61
|
+
reason = "Auto-versioning on sync enabled";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
shouldCreate = false;
|
|
66
|
+
skipReason = "Auto-versioning on sync disabled by policy";
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case "auto":
|
|
70
|
+
// Check interval if specified
|
|
71
|
+
if (policy.min_interval) {
|
|
72
|
+
const latest = this.storage.getLatestVersion(persona.id);
|
|
73
|
+
if (latest && !this.hasIntervalPassed(latest.created_at, policy.min_interval)) {
|
|
74
|
+
shouldCreate = false;
|
|
75
|
+
skipReason = `Minimum interval (${policy.min_interval}) not yet passed`;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (isDuplicate) {
|
|
80
|
+
shouldCreate = false;
|
|
81
|
+
skipReason = "Content unchanged from existing version";
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
shouldCreate = true;
|
|
85
|
+
reason = "Auto-snapshot triggered";
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case "import":
|
|
89
|
+
// Always allow imports (but check dedup)
|
|
90
|
+
if (isDuplicate) {
|
|
91
|
+
shouldCreate = false;
|
|
92
|
+
skipReason = "Imported content matches existing version";
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
shouldCreate = true;
|
|
96
|
+
reason = "Importing version from external source";
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
should_create_version: shouldCreate,
|
|
102
|
+
reason: shouldCreate ? reason : skipReason ?? "Unknown reason",
|
|
103
|
+
skip_reason: shouldCreate ? undefined : skipReason,
|
|
104
|
+
content_hash: contentHash,
|
|
105
|
+
is_duplicate: isDuplicate,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if the minimum interval has passed since the last version.
|
|
110
|
+
*/
|
|
111
|
+
hasIntervalPassed(lastCreatedAt, interval) {
|
|
112
|
+
const last = new Date(lastCreatedAt);
|
|
113
|
+
const now = new Date();
|
|
114
|
+
// Parse ISO duration (simplified - supports PT{n}H, PT{n}M, P{n}D)
|
|
115
|
+
const hours = interval.match(/PT(\d+)H/)?.[1];
|
|
116
|
+
const minutes = interval.match(/PT(\d+)M/)?.[1];
|
|
117
|
+
const days = interval.match(/P(\d+)D/)?.[1];
|
|
118
|
+
let intervalMs = 0;
|
|
119
|
+
if (hours)
|
|
120
|
+
intervalMs += parseInt(hours, 10) * 60 * 60 * 1000;
|
|
121
|
+
if (minutes)
|
|
122
|
+
intervalMs += parseInt(minutes, 10) * 60 * 1000;
|
|
123
|
+
if (days)
|
|
124
|
+
intervalMs += parseInt(days, 10) * 24 * 60 * 60 * 1000;
|
|
125
|
+
return now.getTime() - last.getTime() >= intervalMs;
|
|
126
|
+
}
|
|
127
|
+
// ─────────────── Version Creation ───────────────
|
|
128
|
+
/**
|
|
129
|
+
* Create a version if policy allows.
|
|
130
|
+
* This is the main entry point for automatic versioning.
|
|
131
|
+
*/
|
|
132
|
+
createVersionIfAllowed(persona, trigger, options) {
|
|
133
|
+
// Check policy (unless forced)
|
|
134
|
+
const check = this.checkPolicy(persona, trigger);
|
|
135
|
+
if (!options.force && !check.should_create_version) {
|
|
136
|
+
return {
|
|
137
|
+
created: false,
|
|
138
|
+
reason: check.reason,
|
|
139
|
+
versions_pruned: 0,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Get parent version info
|
|
143
|
+
const latest = this.storage.getLatestVersion(persona.id);
|
|
144
|
+
const previousVersionNumber = latest?.version_number;
|
|
145
|
+
const parentVersionId = latest?.id;
|
|
146
|
+
// Create the version
|
|
147
|
+
const version = createVersionSnapshot({
|
|
148
|
+
persona,
|
|
149
|
+
environment: options.environment,
|
|
150
|
+
tenant_id: options.tenant_id,
|
|
151
|
+
trigger,
|
|
152
|
+
message: options.message,
|
|
153
|
+
created_by: options.created_by,
|
|
154
|
+
previous_version_number: previousVersionNumber,
|
|
155
|
+
parent_version_id: parentVersionId,
|
|
156
|
+
});
|
|
157
|
+
// Compute changes from parent
|
|
158
|
+
let changesFromParent;
|
|
159
|
+
if (latest) {
|
|
160
|
+
const diff = compareVersions(latest, version);
|
|
161
|
+
changesFromParent = generateChangesSummary(diff);
|
|
162
|
+
version.changes_summary = changesFromParent;
|
|
163
|
+
}
|
|
164
|
+
// Save the version
|
|
165
|
+
this.storage.saveVersion(version);
|
|
166
|
+
// Prune if necessary
|
|
167
|
+
const versionsPruned = this.storage.pruneVersions(persona.id);
|
|
168
|
+
return {
|
|
169
|
+
created: true,
|
|
170
|
+
version,
|
|
171
|
+
reason: `Created ${version.version_name}${options.force ? " (forced)" : ""}`,
|
|
172
|
+
changes_from_parent: changesFromParent,
|
|
173
|
+
versions_pruned: versionsPruned,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Force create a version regardless of policy.
|
|
178
|
+
*/
|
|
179
|
+
forceCreateVersion(persona, options) {
|
|
180
|
+
return this.createVersionIfAllowed(persona, "manual", {
|
|
181
|
+
...options,
|
|
182
|
+
force: true,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// ─────────────── Version Restoration ───────────────
|
|
186
|
+
/**
|
|
187
|
+
* Get the snapshot data needed to restore a version.
|
|
188
|
+
* Returns the persona configuration that can be applied via updateAiEmployee.
|
|
189
|
+
*/
|
|
190
|
+
getRestoreData(personaId, versionIdentifier) {
|
|
191
|
+
// Get the version
|
|
192
|
+
let version = null;
|
|
193
|
+
if (typeof versionIdentifier === "number") {
|
|
194
|
+
version = this.storage.getVersion(personaId, versionIdentifier);
|
|
195
|
+
}
|
|
196
|
+
else if (versionIdentifier.match(/^v\d+$/)) {
|
|
197
|
+
version = this.storage.getVersionByName(personaId, versionIdentifier);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Assume it's a version ID
|
|
201
|
+
version = this.storage.getVersionById(personaId, versionIdentifier);
|
|
202
|
+
}
|
|
203
|
+
if (!version) {
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
error: `Version not found: ${versionIdentifier}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// Build restore payload
|
|
210
|
+
const snapshot = version.snapshot;
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
version,
|
|
214
|
+
restore_payload: {
|
|
215
|
+
persona_id: personaId,
|
|
216
|
+
name: snapshot.display_name,
|
|
217
|
+
description: snapshot.description,
|
|
218
|
+
proto_config: snapshot.proto_config ?? {},
|
|
219
|
+
workflow: snapshot.workflow_definition,
|
|
220
|
+
welcome_messages: snapshot.welcome_messages ?? null,
|
|
221
|
+
embedding_enabled: snapshot.embedding_enabled ?? null,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// ─────────────── Policy Management ───────────────
|
|
226
|
+
/**
|
|
227
|
+
* Get policy for a persona.
|
|
228
|
+
*/
|
|
229
|
+
getPolicy(personaId) {
|
|
230
|
+
return this.storage.getPolicy(personaId);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Update policy for a persona.
|
|
234
|
+
*/
|
|
235
|
+
updatePolicy(personaId, updates) {
|
|
236
|
+
const current = this.storage.getPolicy(personaId);
|
|
237
|
+
const updated = {
|
|
238
|
+
...current,
|
|
239
|
+
...updates,
|
|
240
|
+
persona_id: personaId, // Ensure this is always set
|
|
241
|
+
};
|
|
242
|
+
this.storage.savePolicy(updated);
|
|
243
|
+
return updated;
|
|
244
|
+
}
|
|
245
|
+
// ─────────────── Query Methods ───────────────
|
|
246
|
+
/**
|
|
247
|
+
* List versions for a persona with optional filtering.
|
|
248
|
+
*/
|
|
249
|
+
listVersions(personaId, options) {
|
|
250
|
+
let versions = this.storage.listVersions(personaId);
|
|
251
|
+
// Apply filters
|
|
252
|
+
if (options?.trigger) {
|
|
253
|
+
versions = versions.filter((v) => v.trigger === options.trigger);
|
|
254
|
+
}
|
|
255
|
+
if (options?.since) {
|
|
256
|
+
const sinceDate = new Date(options.since);
|
|
257
|
+
versions = versions.filter((v) => new Date(v.created_at) >= sinceDate);
|
|
258
|
+
}
|
|
259
|
+
// Sort by version number descending (newest first)
|
|
260
|
+
versions = versions.sort((a, b) => b.version_number - a.version_number);
|
|
261
|
+
// Apply limit
|
|
262
|
+
if (options?.limit) {
|
|
263
|
+
versions = versions.slice(0, options.limit);
|
|
264
|
+
}
|
|
265
|
+
return versions.map((v) => ({
|
|
266
|
+
id: v.id,
|
|
267
|
+
version_number: v.version_number,
|
|
268
|
+
version_name: v.version_name,
|
|
269
|
+
created_at: v.created_at,
|
|
270
|
+
trigger: v.trigger,
|
|
271
|
+
message: v.message,
|
|
272
|
+
content_hash: v.content_hash,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Get a specific version with full details.
|
|
277
|
+
*/
|
|
278
|
+
getVersion(personaId, versionIdentifier) {
|
|
279
|
+
if (versionIdentifier === "latest") {
|
|
280
|
+
return this.storage.getLatestVersion(personaId);
|
|
281
|
+
}
|
|
282
|
+
if (typeof versionIdentifier === "number") {
|
|
283
|
+
return this.storage.getVersion(personaId, versionIdentifier);
|
|
284
|
+
}
|
|
285
|
+
// Try as version name first (v1, v2, etc.)
|
|
286
|
+
if (versionIdentifier.match(/^v\d+$/)) {
|
|
287
|
+
return this.storage.getVersionByName(personaId, versionIdentifier);
|
|
288
|
+
}
|
|
289
|
+
// Try as version ID
|
|
290
|
+
return this.storage.getVersionById(personaId, versionIdentifier);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Compare two versions.
|
|
294
|
+
*/
|
|
295
|
+
compareVersions(personaId, v1Identifier, v2Identifier) {
|
|
296
|
+
const v1 = this.getVersion(personaId, v1Identifier);
|
|
297
|
+
const v2 = this.getVersion(personaId, v2Identifier);
|
|
298
|
+
if (!v1) {
|
|
299
|
+
return { success: false, error: `Version not found: ${v1Identifier}` };
|
|
300
|
+
}
|
|
301
|
+
if (!v2) {
|
|
302
|
+
return { success: false, error: `Version not found: ${v2Identifier}` };
|
|
303
|
+
}
|
|
304
|
+
const diff = compareVersions(v1, v2);
|
|
305
|
+
const summary = generateChangesSummary(diff);
|
|
306
|
+
return {
|
|
307
|
+
success: true,
|
|
308
|
+
diff,
|
|
309
|
+
summary,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// ─────────────── Storage Access ───────────────
|
|
313
|
+
/**
|
|
314
|
+
* Get the underlying storage instance.
|
|
315
|
+
*/
|
|
316
|
+
getStorage() {
|
|
317
|
+
return this.storage;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
321
|
+
// Factory Function
|
|
322
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
323
|
+
/**
|
|
324
|
+
* Create a VersionPolicyEngine instance.
|
|
325
|
+
*/
|
|
326
|
+
export function createVersionPolicyEngine(storage) {
|
|
327
|
+
return new VersionPolicyEngine(storage);
|
|
328
|
+
}
|