@ema.co/mcp-toolkit 0.2.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +321 -0
  3. package/config.example.yaml +32 -0
  4. package/dist/cli/index.js +333 -0
  5. package/dist/config.js +136 -0
  6. package/dist/emaClient.js +398 -0
  7. package/dist/index.js +109 -0
  8. package/dist/mcp/handlers-consolidated.js +851 -0
  9. package/dist/mcp/index.js +15 -0
  10. package/dist/mcp/prompts.js +1753 -0
  11. package/dist/mcp/resources.js +624 -0
  12. package/dist/mcp/server.js +4723 -0
  13. package/dist/mcp/tools-consolidated.js +590 -0
  14. package/dist/mcp/tools-legacy.js +736 -0
  15. package/dist/models.js +8 -0
  16. package/dist/scheduler.js +21 -0
  17. package/dist/sdk/client.js +788 -0
  18. package/dist/sdk/config.js +136 -0
  19. package/dist/sdk/contracts.js +429 -0
  20. package/dist/sdk/generation-schema.js +189 -0
  21. package/dist/sdk/index.js +39 -0
  22. package/dist/sdk/knowledge.js +2780 -0
  23. package/dist/sdk/models.js +8 -0
  24. package/dist/sdk/state.js +88 -0
  25. package/dist/sdk/sync-options.js +216 -0
  26. package/dist/sdk/sync.js +220 -0
  27. package/dist/sdk/validation-rules.js +355 -0
  28. package/dist/sdk/workflow-generator.js +291 -0
  29. package/dist/sdk/workflow-intent.js +1585 -0
  30. package/dist/state.js +88 -0
  31. package/dist/sync.js +416 -0
  32. package/dist/syncOptions.js +216 -0
  33. package/dist/ui.js +334 -0
  34. package/docs/advisor-comms-assistant-fixes.md +175 -0
  35. package/docs/api-contracts.md +216 -0
  36. package/docs/auto-builder-analysis.md +271 -0
  37. package/docs/data-architecture.md +166 -0
  38. package/docs/ema-auto-builder-guide.html +394 -0
  39. package/docs/ema-user-guide.md +1121 -0
  40. package/docs/mcp-tools-guide.md +149 -0
  41. package/docs/naming-conventions.md +218 -0
  42. package/docs/tool-consolidation-proposal.md +427 -0
  43. package/package.json +98 -0
  44. package/resources/templates/chat-ai/README.md +119 -0
  45. package/resources/templates/chat-ai/persona-config.json +111 -0
  46. package/resources/templates/dashboard-ai/README.md +156 -0
  47. package/resources/templates/dashboard-ai/persona-config.json +180 -0
  48. package/resources/templates/voice-ai/README.md +123 -0
  49. package/resources/templates/voice-ai/persona-config.json +74 -0
  50. package/resources/templates/voice-ai/workflow-prompt.md +120 -0
package/dist/state.js ADDED
@@ -0,0 +1,88 @@
1
+ import Database from "better-sqlite3";
2
+ export class StateStore {
3
+ db;
4
+ mapTable = "persona_map_v2";
5
+ fpTable = "persona_fingerprint_v2";
6
+ runsTable = "sync_runs_v2";
7
+ constructor(path) {
8
+ this.db = new Database(path);
9
+ this.db.pragma("journal_mode = WAL");
10
+ this.migrate();
11
+ }
12
+ close() {
13
+ this.db.close();
14
+ }
15
+ migrate() {
16
+ this.db.exec(`
17
+ CREATE TABLE IF NOT EXISTS ${this.mapTable} (
18
+ master_env TEXT NOT NULL,
19
+ master_user_id TEXT NOT NULL,
20
+ master_persona_id TEXT NOT NULL,
21
+ target_env TEXT NOT NULL,
22
+ target_user_id TEXT NOT NULL,
23
+ target_persona_id TEXT NOT NULL,
24
+ PRIMARY KEY (master_env, master_user_id, master_persona_id, target_env, target_user_id)
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS ${this.fpTable} (
28
+ env TEXT NOT NULL,
29
+ env_user_id TEXT NOT NULL,
30
+ persona_id TEXT NOT NULL,
31
+ fingerprint TEXT NOT NULL,
32
+ last_seen_at TEXT NOT NULL,
33
+ PRIMARY KEY (env, env_user_id, persona_id)
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS ${this.runsTable} (
37
+ id TEXT PRIMARY KEY,
38
+ master_env TEXT NOT NULL,
39
+ master_env_user_id TEXT NOT NULL,
40
+ started_at TEXT NOT NULL,
41
+ finished_at TEXT,
42
+ status TEXT NOT NULL,
43
+ error TEXT
44
+ );
45
+ `);
46
+ }
47
+ getMapping(args) {
48
+ const row = this.db
49
+ .prepare(`SELECT target_persona_id
50
+ FROM ${this.mapTable}
51
+ WHERE master_env = ? AND master_user_id = ? AND master_persona_id = ? AND target_env = ? AND target_user_id = ?`)
52
+ .get(args.masterEnv, args.masterUserId, args.masterPersonaId, args.targetEnv, args.targetUserId);
53
+ return row?.target_persona_id ?? null;
54
+ }
55
+ upsertMapping(row) {
56
+ this.db
57
+ .prepare(`INSERT INTO ${this.mapTable} (master_env, master_user_id, master_persona_id, target_env, target_user_id, target_persona_id)
58
+ VALUES (?, ?, ?, ?, ?, ?)
59
+ ON CONFLICT(master_env, master_user_id, master_persona_id, target_env, target_user_id)
60
+ DO UPDATE SET target_persona_id = excluded.target_persona_id`)
61
+ .run(row.master_env, row.master_user_id, row.master_persona_id, row.target_env, row.target_user_id, row.target_persona_id);
62
+ }
63
+ getFingerprint(args) {
64
+ const row = this.db
65
+ .prepare(`SELECT fingerprint FROM ${this.fpTable} WHERE env = ? AND env_user_id = ? AND persona_id = ?`)
66
+ .get(args.env, args.envUserId, args.personaId);
67
+ return row?.fingerprint ?? null;
68
+ }
69
+ upsertFingerprint(row) {
70
+ this.db
71
+ .prepare(`INSERT INTO ${this.fpTable} (env, env_user_id, persona_id, fingerprint, last_seen_at)
72
+ VALUES (?, ?, ?, ?, ?)
73
+ ON CONFLICT(env, env_user_id, persona_id)
74
+ DO UPDATE SET fingerprint = excluded.fingerprint, last_seen_at = excluded.last_seen_at`)
75
+ .run(row.env, row.env_user_id, row.persona_id, row.fingerprint, row.last_seen_at);
76
+ }
77
+ beginRun(row) {
78
+ this.db
79
+ .prepare(`INSERT INTO ${this.runsTable} (id, master_env, master_env_user_id, started_at, status)
80
+ VALUES (?, ?, ?, ?, ?)`)
81
+ .run(row.id, row.master_env, row.master_env_user_id, row.started_at, row.status);
82
+ }
83
+ finishRun(args) {
84
+ this.db
85
+ .prepare(`UPDATE ${this.runsTable} SET finished_at = ?, status = ?, error = ? WHERE id = ?`)
86
+ .run(args.finishedAt, args.status, args.error ?? null, args.id);
87
+ }
88
+ }
package/dist/sync.js ADDED
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Sync Engine
3
+ *
4
+ * Core synchronization logic for replicating AI Employees across environments.
5
+ * Uses fingerprinting for change detection and SQLite for state tracking.
6
+ */
7
+ import crypto from "node:crypto";
8
+ import { getMasterEnv, resolveBearerToken } from "./sdk/config.js";
9
+ import { EmaApiError, EmaClient } from "./sdk/client.js";
10
+ import { SYNC_METADATA_KEY } from "./sdk/models.js";
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ // Constants
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+ // Sync tag format: <!-- synced_from:env/id -->
15
+ const SYNC_TAG_REGEX = /<!-- synced_from:([^/]+)\/([a-f0-9-]+) -->/;
16
+ const SYNC_TAG_CLEANUP_REGEX = /<!-- (?:synced_from|sync|_ema_sync):[^\n]*-->/g;
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // Utilities
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ function nowIso() {
21
+ return new Date().toISOString();
22
+ }
23
+ function getPersonaTemplateId(p) {
24
+ return (p.template_id ?? p.templateId);
25
+ }
26
+ function escapeRegex(s) {
27
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
29
+ function stableStringify(v) {
30
+ if (v === null || v === undefined)
31
+ return JSON.stringify(v);
32
+ if (Array.isArray(v))
33
+ return `[${v.map(stableStringify).join(",")}]`;
34
+ if (typeof v === "object") {
35
+ const o = v;
36
+ const keys = Object.keys(o).sort();
37
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(",")}}`;
38
+ }
39
+ return JSON.stringify(v);
40
+ }
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+ // Sync Tag Helpers
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ /**
45
+ * Extract clean description (without sync marker).
46
+ */
47
+ export function getCleanDescription(description) {
48
+ if (!description)
49
+ return "";
50
+ return description.replace(SYNC_TAG_CLEANUP_REGEX, "").trim();
51
+ }
52
+ /**
53
+ * Build compact sync tag.
54
+ */
55
+ export function buildSyncTag(masterEnv, masterId) {
56
+ return `<!-- synced_from:${masterEnv}/${masterId} -->`;
57
+ }
58
+ /**
59
+ * Build description with sync tag appended.
60
+ */
61
+ export function buildDescriptionWithSyncTag(cleanDescription, masterEnv, masterId) {
62
+ const marker = buildSyncTag(masterEnv, masterId);
63
+ return cleanDescription ? `${cleanDescription}\n\n${marker}` : marker;
64
+ }
65
+ /**
66
+ * Parse sync tag from description.
67
+ */
68
+ export function parseSyncTag(description) {
69
+ if (!description)
70
+ return null;
71
+ const match = description.match(SYNC_TAG_REGEX);
72
+ if (!match)
73
+ return null;
74
+ return { masterEnv: match[1], masterId: match[2] };
75
+ }
76
+ /**
77
+ * Clean proto_config by removing any legacy sync tags.
78
+ */
79
+ function cleanProtoConfig(sourceProtoConfig) {
80
+ const base = { ...(sourceProtoConfig ?? {}) };
81
+ delete base[SYNC_METADATA_KEY];
82
+ return base;
83
+ }
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+ // Fingerprinting
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+ /**
88
+ * Compute a deterministic fingerprint (SHA256) of a persona's configuration.
89
+ * Used for change detection - only syncs when fingerprint differs.
90
+ */
91
+ export function fingerprintPersona(p) {
92
+ // Clean proto_config (remove sync metadata)
93
+ let protoConfigForFingerprint = p.proto_config ?? null;
94
+ if (protoConfigForFingerprint && typeof protoConfigForFingerprint === "object") {
95
+ protoConfigForFingerprint = { ...protoConfigForFingerprint };
96
+ delete protoConfigForFingerprint[SYNC_METADATA_KEY];
97
+ if (Object.keys(protoConfigForFingerprint).length === 0) {
98
+ protoConfigForFingerprint = null;
99
+ }
100
+ }
101
+ // Clean description (remove sync tag)
102
+ const cleanDesc = getCleanDescription(p.description);
103
+ const canonical = {
104
+ name: p.name ?? null,
105
+ description: cleanDesc || null,
106
+ proto_config: protoConfigForFingerprint,
107
+ welcome_messages: p.welcome_messages ?? null,
108
+ trigger_type: p.trigger_type ?? null,
109
+ workflow_def: p.workflow_def ?? null,
110
+ workflow_interface: p.workflow_interface ?? null,
111
+ embedding_enabled: p.embedding_enabled ?? null,
112
+ // Excluded: id, status, access_level, status_log (computed/user-specific)
113
+ };
114
+ const bytes = Buffer.from(stableStringify(canonical), "utf8");
115
+ return crypto.createHash("sha256").update(bytes).digest("hex");
116
+ }
117
+ // ─────────────────────────────────────────────────────────────────────────────
118
+ // Workflow Transformation
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ /**
121
+ * Transform workflow_def by replacing source persona ID with target persona ID.
122
+ * Required because workflow_def contains embedded persona ID references.
123
+ */
124
+ export function transformWorkflowForTarget(workflowDef, sourcePersonaId, targetPersonaId) {
125
+ const transformed = JSON.parse(JSON.stringify(workflowDef));
126
+ // Transform workflowName.name.namespaces[2]
127
+ const workflowName = transformed.workflowName;
128
+ if (workflowName?.name?.namespaces && Array.isArray(workflowName.name.namespaces)) {
129
+ workflowName.name.namespaces = workflowName.name.namespaces.map((ns) => ns === sourcePersonaId ? targetPersonaId : ns);
130
+ }
131
+ // Replace all other occurrences
132
+ const jsonStr = JSON.stringify(transformed);
133
+ const replaced = jsonStr.replace(new RegExp(escapeRegex(sourcePersonaId), "g"), targetPersonaId);
134
+ return JSON.parse(replaced);
135
+ }
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+ // Routing
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+ function globToRegex(glob) {
140
+ const re = "^" + glob.split("").map((c) => (c === "*" ? ".*" : c === "?" ? "." : escapeRegex(c))).join("") + "$";
141
+ return new RegExp(re);
142
+ }
143
+ function personaMatchesRule(p, r) {
144
+ if ((r.personaIds ?? []).includes(p.id))
145
+ return true;
146
+ const name = (p.name ?? "").trim();
147
+ if (!name)
148
+ return false;
149
+ for (const pref of r.namePrefixes ?? []) {
150
+ if (name.startsWith(pref))
151
+ return true;
152
+ }
153
+ for (const g of r.nameGlobs ?? []) {
154
+ if (globToRegex(g).test(name))
155
+ return true;
156
+ }
157
+ for (const rx of r.nameRegexes ?? []) {
158
+ if (new RegExp(rx).test(name))
159
+ return true;
160
+ }
161
+ return false;
162
+ }
163
+ function pickTargets(cfg, persona) {
164
+ const routing = cfg.routing ?? [];
165
+ if (!routing || !Array.isArray(routing))
166
+ return { targetEnvs: [] };
167
+ const rules = routing.filter((r) => personaMatchesRule(persona, r));
168
+ const envs = new Set();
169
+ for (const r of rules) {
170
+ for (const t of r.targetEnvs ?? [])
171
+ envs.add(t);
172
+ }
173
+ return { targetEnvs: [...envs] };
174
+ }
175
+ // ─────────────────────────────────────────────────────────────────────────────
176
+ // Environment Helpers
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+ function envByName(cfg, name) {
179
+ const e = cfg.environments.find((x) => x.name === name);
180
+ if (!e)
181
+ throw new Error(`Unknown environment: ${name}`);
182
+ return e;
183
+ }
184
+ function buildEnv(e) {
185
+ return { name: e.name, baseUrl: e.baseUrl, bearerToken: resolveBearerToken(e.bearerTokenEnv) };
186
+ }
187
+ /**
188
+ * Run a single sync pass - scans master personas and replicates to targets.
189
+ */
190
+ export async function syncOnce(cfg, state) {
191
+ const masterEnvCfg = getMasterEnv(cfg);
192
+ if (!masterEnvCfg) {
193
+ throw new Error("No master environment configured (isMaster: true)");
194
+ }
195
+ const masterEnvName = masterEnvCfg.name;
196
+ const runId = crypto.randomUUID();
197
+ state.beginRun({
198
+ id: runId,
199
+ master_env: masterEnvName,
200
+ master_env_user_id: masterEnvName, // Use env name as user ID
201
+ started_at: nowIso(),
202
+ status: "running",
203
+ });
204
+ const masterClient = new EmaClient(buildEnv(masterEnvCfg));
205
+ try {
206
+ const personas = await masterClient.getPersonasForTenant();
207
+ let scanned = 0;
208
+ let changed = 0;
209
+ let synced = 0;
210
+ let skipped = 0;
211
+ const errors = [];
212
+ for (let p of personas) {
213
+ scanned++;
214
+ const routing = pickTargets(cfg, p);
215
+ if (routing.targetEnvs.length === 0) {
216
+ skipped++;
217
+ continue;
218
+ }
219
+ // Fetch full persona if workflow_def missing
220
+ if (!p.workflow_def) {
221
+ const fullPersona = await masterClient.getPersonaById(p.id);
222
+ if (fullPersona) {
223
+ p = { ...p, ...fullPersona };
224
+ }
225
+ }
226
+ const fp = fingerprintPersona(p);
227
+ const prevMasterFp = state.getFingerprint({ env: masterEnvName, envUserId: masterEnvName, personaId: p.id });
228
+ const masterChanged = prevMasterFp !== fp;
229
+ // Check which targets need sync
230
+ const targetsNeedingSync = [];
231
+ for (const targetEnvName of routing.targetEnvs) {
232
+ if (targetEnvName === masterEnvName)
233
+ continue;
234
+ const mappedId = state.getMapping({
235
+ masterEnv: masterEnvName,
236
+ masterUserId: masterEnvName,
237
+ masterPersonaId: p.id,
238
+ targetEnv: targetEnvName,
239
+ targetUserId: targetEnvName,
240
+ });
241
+ if (!mappedId) {
242
+ targetsNeedingSync.push(targetEnvName);
243
+ continue;
244
+ }
245
+ const targetFp = state.getFingerprint({ env: targetEnvName, envUserId: targetEnvName, personaId: mappedId });
246
+ if (targetFp !== fp)
247
+ targetsNeedingSync.push(targetEnvName);
248
+ }
249
+ if (!masterChanged && targetsNeedingSync.length === 0) {
250
+ skipped++;
251
+ continue;
252
+ }
253
+ if (masterChanged)
254
+ changed++;
255
+ // Record master fingerprint
256
+ if (!cfg.dryRun) {
257
+ state.upsertFingerprint({
258
+ env: masterEnvName,
259
+ env_user_id: masterEnvName,
260
+ persona_id: p.id,
261
+ fingerprint: fp,
262
+ last_seen_at: nowIso(),
263
+ });
264
+ }
265
+ // Sync to each target
266
+ for (const targetEnvName of targetsNeedingSync) {
267
+ const targetCfg = envByName(cfg, targetEnvName);
268
+ const targetClient = new EmaClient(buildEnv(targetCfg));
269
+ try {
270
+ await upsertReplicaPersona({
271
+ cfg,
272
+ state,
273
+ masterEnvName,
274
+ masterPersona: p,
275
+ masterFingerprint: fp,
276
+ targetEnv: targetEnvName,
277
+ targetClient,
278
+ runId,
279
+ });
280
+ synced++;
281
+ }
282
+ catch (e) {
283
+ errors.push(e instanceof Error ? e.message : String(e));
284
+ }
285
+ }
286
+ }
287
+ state.finishRun({
288
+ id: runId,
289
+ finishedAt: nowIso(),
290
+ status: errors.length ? "error" : "success",
291
+ error: errors.length ? errors.slice(0, 10).join(" | ") : undefined,
292
+ });
293
+ return { runId, scanned, changed, synced, skipped };
294
+ }
295
+ catch (e) {
296
+ state.finishRun({
297
+ id: runId,
298
+ finishedAt: nowIso(),
299
+ status: "error",
300
+ error: e instanceof Error ? e.message : String(e),
301
+ });
302
+ throw e;
303
+ }
304
+ }
305
+ // ─────────────────────────────────────────────────────────────────────────────
306
+ // Replica Upsert
307
+ // ─────────────────────────────────────────────────────────────────────────────
308
+ async function upsertReplicaPersona(args) {
309
+ const { cfg, state, masterEnvName, masterPersona, masterFingerprint, targetEnv, targetClient, runId } = args;
310
+ let mappedId = state.getMapping({
311
+ masterEnv: masterEnvName,
312
+ masterUserId: masterEnvName,
313
+ masterPersonaId: masterPersona.id,
314
+ targetEnv,
315
+ targetUserId: targetEnv,
316
+ });
317
+ // Search by name if no mapping
318
+ if (!mappedId && masterPersona.name) {
319
+ const targetPersonas = await targetClient.getPersonasForTenant();
320
+ const existingByName = targetPersonas.find((p) => p.name === masterPersona.name);
321
+ if (existingByName) {
322
+ mappedId = existingByName.id;
323
+ if (!cfg.dryRun) {
324
+ state.upsertMapping({
325
+ master_env: masterEnvName,
326
+ master_user_id: masterEnvName,
327
+ master_persona_id: masterPersona.id,
328
+ target_env: targetEnv,
329
+ target_user_id: targetEnv,
330
+ target_persona_id: mappedId,
331
+ });
332
+ }
333
+ }
334
+ }
335
+ if (cfg.dryRun)
336
+ return;
337
+ const cleanedProtoConfig = cleanProtoConfig(masterPersona.proto_config);
338
+ const cleanMasterDesc = getCleanDescription(masterPersona.description);
339
+ const descriptionWithTag = buildDescriptionWithSyncTag(cleanMasterDesc, masterEnvName, masterPersona.id);
340
+ if (mappedId) {
341
+ // Update existing
342
+ let transformedWorkflow;
343
+ if (masterPersona.workflow_def) {
344
+ transformedWorkflow = transformWorkflowForTarget(masterPersona.workflow_def, masterPersona.id, mappedId);
345
+ }
346
+ const upd = {
347
+ persona_id: mappedId,
348
+ name: masterPersona.name,
349
+ description: descriptionWithTag,
350
+ proto_config: cleanedProtoConfig,
351
+ welcome_messages: masterPersona.welcome_messages,
352
+ embedding_enabled: masterPersona.embedding_enabled,
353
+ workflow: transformedWorkflow,
354
+ };
355
+ try {
356
+ await targetClient.updateAiEmployee(upd, { verbose: cfg.verbose });
357
+ state.upsertFingerprint({
358
+ env: targetEnv,
359
+ env_user_id: targetEnv,
360
+ persona_id: mappedId,
361
+ fingerprint: masterFingerprint,
362
+ last_seen_at: nowIso(),
363
+ });
364
+ return;
365
+ }
366
+ catch (e) {
367
+ if (e instanceof EmaApiError && e.statusCode === 404) {
368
+ // Fall through to create
369
+ }
370
+ else {
371
+ throw e;
372
+ }
373
+ }
374
+ }
375
+ // Create new persona
376
+ const sourceTemplateId = getPersonaTemplateId(masterPersona);
377
+ if (!sourceTemplateId) {
378
+ throw new Error(`Cannot create persona in ${targetEnv}: missing template_id`);
379
+ }
380
+ const createReq = {
381
+ name: masterPersona.name ?? "Unnamed Persona",
382
+ description: descriptionWithTag,
383
+ template_id: sourceTemplateId,
384
+ proto_config: cleanedProtoConfig,
385
+ welcome_messages: masterPersona.welcome_messages,
386
+ trigger_type: masterPersona.trigger_type,
387
+ };
388
+ const created = await targetClient.createAiEmployee(createReq, { verbose: cfg.verbose });
389
+ const newId = created.persona_id ?? created.id;
390
+ if (!newId)
391
+ throw new Error(`Create in ${targetEnv} succeeded but no persona_id returned`);
392
+ // Follow-up: sync workflow now that we have target ID
393
+ if (masterPersona.workflow_def) {
394
+ const transformedWorkflow = transformWorkflowForTarget(masterPersona.workflow_def, masterPersona.id, newId);
395
+ await targetClient.updateAiEmployee({
396
+ persona_id: newId,
397
+ proto_config: cleanedProtoConfig,
398
+ workflow: transformedWorkflow,
399
+ });
400
+ }
401
+ state.upsertMapping({
402
+ master_env: masterEnvName,
403
+ master_user_id: masterEnvName,
404
+ master_persona_id: masterPersona.id,
405
+ target_env: targetEnv,
406
+ target_user_id: targetEnv,
407
+ target_persona_id: newId,
408
+ });
409
+ state.upsertFingerprint({
410
+ env: targetEnv,
411
+ env_user_id: targetEnv,
412
+ persona_id: newId,
413
+ fingerprint: masterFingerprint,
414
+ last_seen_at: nowIso(),
415
+ });
416
+ }