@ema.co/mcp-toolkit 0.2.2 → 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.
@@ -25,6 +25,8 @@ import { loadConfigOptional } from "../sdk/config.js";
25
25
  import { resolveSyncBehavior, loadSyncOptions } from "../sdk/sync-options.js";
26
26
  import { SyncSDK } from "../sdk/sync.js";
27
27
  import { fingerprintPersona, transformWorkflowForTarget, getCleanDescription, buildDescriptionWithSyncTag } from "../sync.js";
28
+ import { createVersionStorage } from "../sdk/version-storage.js";
29
+ import { createVersionPolicyEngine } from "../sdk/version-policy.js";
28
30
  import { SYNC_METADATA_KEY } from "../sdk/models.js";
29
31
  // Auto Builder Knowledge
30
32
  import { AGENT_CATALOG, WORKFLOW_PATTERNS, QUALIFYING_QUESTIONS, PLATFORM_CONCEPTS, WORKFLOW_EXECUTION_MODEL, COMMON_MISTAKES, DEBUG_CHECKLIST, GUIDANCE_TOPICS, VOICE_PERSONA_TEMPLATE, PROJECT_TYPES, getAgentsByCategory, getAgentByName, getWidgetsForPersonaType, checkTypeCompatibility, getQualifyingQuestionsByCategory, getConceptByTerm, suggestAgentsForUseCase, validateWorkflowPrompt,
@@ -4205,13 +4207,20 @@ const toolHandlers = {
4205
4207
  })));
4206
4208
  },
4207
4209
  persona: async (args) => {
4208
- const client = createClient(args.env);
4210
+ const targetEnv = args.env ?? getDefaultEnvName();
4211
+ const client = createClient(targetEnv);
4209
4212
  const DEFAULT_TEMPLATES = {
4210
4213
  voice: "00000000-0000-0000-0000-00000000001e",
4211
4214
  chat: "00000000-0000-0000-0000-000000000004",
4212
4215
  dashboard: "00000000-0000-0000-0000-000000000002",
4213
4216
  };
4214
- return handlePersona(args, client, (type) => DEFAULT_TEMPLATES[type], (env) => createClient(env));
4217
+ // Build version context for version management modes
4218
+ const versionContext = {
4219
+ workspaceRoot: process.cwd(),
4220
+ environment: targetEnv,
4221
+ tenant_id: targetEnv, // Use env name as tenant identifier
4222
+ };
4223
+ return handlePersona(args, client, (type) => DEFAULT_TEMPLATES[type], (env) => createClient(env), versionContext);
4215
4224
  },
4216
4225
  // Note: 'workflow' handler already exists above - consolidated version adds analyze modes
4217
4226
  // The existing 'workflow' handler is kept for backward compatibility
@@ -4358,7 +4367,34 @@ toolHandlers.workflow = async (args) => {
4358
4367
  }
4359
4368
  const validateFirst = normalizedArgs.validate !== false; // default true
4360
4369
  const autoFix = normalizedArgs.auto_fix === true; // default false
4361
- return legacyDeployWorkflow({
4370
+ const targetEnv = normalizedArgs.env ?? getDefaultEnvName();
4371
+ // ─────────────── Version tracking (pre-deploy snapshot) ───────────────
4372
+ let versionCreated;
4373
+ try {
4374
+ const client = createClient(targetEnv);
4375
+ const personaBefore = await client.getPersonaById(personaId);
4376
+ if (personaBefore) {
4377
+ const storage = createVersionStorage(process.cwd());
4378
+ const engine = createVersionPolicyEngine(storage);
4379
+ // Check policy and create version if allowed
4380
+ const result = engine.createVersionIfAllowed(personaBefore, "deploy", {
4381
+ environment: targetEnv,
4382
+ tenant_id: targetEnv,
4383
+ message: "Pre-deploy snapshot",
4384
+ created_by: "mcp-toolkit",
4385
+ });
4386
+ if (result.created && result.version) {
4387
+ versionCreated = {
4388
+ id: result.version.id,
4389
+ version_name: result.version.version_name,
4390
+ };
4391
+ }
4392
+ }
4393
+ }
4394
+ catch {
4395
+ // Version tracking is best-effort - don't fail deploy if it errors
4396
+ }
4397
+ const deployResult = await legacyDeployWorkflow({
4362
4398
  persona_id: personaId,
4363
4399
  workflow_def: workflowDef,
4364
4400
  proto_config: normalizedArgs.proto_config,
@@ -4366,6 +4402,11 @@ toolHandlers.workflow = async (args) => {
4366
4402
  auto_fix: autoFix,
4367
4403
  env: normalizedArgs.env,
4368
4404
  });
4405
+ // Add version info to result if created
4406
+ if (versionCreated && deployResult && typeof deployResult === "object") {
4407
+ deployResult.version_snapshot = versionCreated;
4408
+ }
4409
+ return deployResult;
4369
4410
  }
4370
4411
  case "optimize": {
4371
4412
  // optimize_workflow supports both:
@@ -46,7 +46,7 @@ export function generateConsolidatedTools(envNames, defaultEnv) {
46
46
  inputSchema: { type: "object", properties: {}, required: [] },
47
47
  },
48
48
  // ═══════════════════════════════════════════════════════════════════════
49
- // 2. PERSONA - AI Employee management (CRUD + compare)
49
+ // 2. PERSONA - AI Employee management (CRUD + compare + versioning)
50
50
  // ═══════════════════════════════════════════════════════════════════════
51
51
  {
52
52
  name: "persona",
@@ -71,7 +71,15 @@ export function generateConsolidatedTools(envNames, defaultEnv) {
71
71
  persona(id="abc-123", mode="compare", compare_to="def-456")
72
72
 
73
73
  **Templates** (list available templates):
74
- persona(templates=true)`,
74
+ persona(templates=true)
75
+
76
+ **Version Management** (track configuration history):
77
+ persona(id="abc-123", mode="version_create", message="Before major update")
78
+ persona(id="abc-123", mode="version_list")
79
+ persona(id="abc-123", mode="version_get", version="v3")
80
+ persona(id="abc-123", mode="version_compare", v1="v2", v2="v3")
81
+ persona(id="abc-123", mode="version_restore", version="v2")
82
+ persona(id="abc-123", mode="version_policy", auto_on_deploy=true)`,
75
83
  inputSchema: withEnv({
76
84
  // ID (or exact name) (optional - if omitted with all=true, lists all)
77
85
  id: {
@@ -87,7 +95,7 @@ export function generateConsolidatedTools(envNames, defaultEnv) {
87
95
  // Mode (defaults to "get" if id provided, "list" if all=true)
88
96
  mode: {
89
97
  type: "string",
90
- enum: ["get", "list", "create", "update", "compare"],
98
+ enum: ["get", "list", "create", "update", "compare", "version_create", "version_list", "version_get", "version_compare", "version_restore", "version_policy"],
91
99
  description: "Operation mode. Default: 'get' with id, 'list' without."
92
100
  },
93
101
  // List/Search flags
@@ -116,6 +124,14 @@ export function generateConsolidatedTools(envNames, defaultEnv) {
116
124
  compare_env: { type: "string", description: "Environment of compare_to persona" },
117
125
  // Templates flag
118
126
  templates: { type: "boolean", description: "List available templates" },
127
+ // Version management flags
128
+ version: { type: "string", description: "Version identifier (e.g., 'v3', 'latest', or UUID)" },
129
+ v1: { type: "string", description: "First version for comparison (version_compare mode)" },
130
+ v2: { type: "string", description: "Second version for comparison (version_compare mode)" },
131
+ message: { type: "string", description: "Version message/description (version_create mode)" },
132
+ auto_on_deploy: { type: "boolean", description: "Auto-create version on deploy (version_policy mode)" },
133
+ auto_on_sync: { type: "boolean", description: "Auto-create version on sync (version_policy mode)" },
134
+ max_versions: { type: "number", description: "Max versions to keep (version_policy mode)" },
119
135
  }),
120
136
  },
121
137
  // ═══════════════════════════════════════════════════════════════════════
package/dist/sdk/index.js CHANGED
@@ -37,3 +37,11 @@ buildVoiceConfig, buildChatConfig, } from "./workflow-generator.js";
37
37
  export { parseInput, validateIntent, intentToSpec, detectInputType, parseNaturalLanguage, parsePartialSpec, } from "./workflow-intent.js";
38
38
  // Generation Schema (Compact format for LLM-based generation)
39
39
  export { generateSchema, generateSchemaMarkdown, buildCompactAgents, buildTypeRules, buildConstraints, getAgentSchema, isTypeCompatible, getRecommendedInput, } from "./generation-schema.js";
40
+ // Version Tracking (Persona version history management)
41
+ export {
42
+ // Core functions
43
+ hashSnapshot, generateVersionId, generateVersionName, nowIso, createSnapshotFromPersona, createVersionSnapshot, compareVersions, generateChangesSummary, fingerprintPersona, extractDataSources, } from "./version-tracking.js";
44
+ // Version Storage (Git-backed local storage)
45
+ export { VersionStorage, createVersionStorage, } from "./version-storage.js";
46
+ // Version Policy Engine (Automatic versioning decisions)
47
+ export { VersionPolicyEngine, createVersionPolicyEngine, } from "./version-policy.js";
@@ -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
+ }