@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.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/config.example.yaml +32 -0
- package/dist/cli/index.js +333 -0
- package/dist/config.js +136 -0
- package/dist/emaClient.js +398 -0
- package/dist/index.js +109 -0
- package/dist/mcp/handlers-consolidated.js +851 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/prompts.js +1753 -0
- package/dist/mcp/resources.js +624 -0
- package/dist/mcp/server.js +4723 -0
- package/dist/mcp/tools-consolidated.js +590 -0
- package/dist/mcp/tools-legacy.js +736 -0
- package/dist/models.js +8 -0
- package/dist/scheduler.js +21 -0
- package/dist/sdk/client.js +788 -0
- package/dist/sdk/config.js +136 -0
- package/dist/sdk/contracts.js +429 -0
- package/dist/sdk/generation-schema.js +189 -0
- package/dist/sdk/index.js +39 -0
- package/dist/sdk/knowledge.js +2780 -0
- package/dist/sdk/models.js +8 -0
- package/dist/sdk/state.js +88 -0
- package/dist/sdk/sync-options.js +216 -0
- package/dist/sdk/sync.js +220 -0
- package/dist/sdk/validation-rules.js +355 -0
- package/dist/sdk/workflow-generator.js +291 -0
- package/dist/sdk/workflow-intent.js +1585 -0
- package/dist/state.js +88 -0
- package/dist/sync.js +416 -0
- package/dist/syncOptions.js +216 -0
- package/dist/ui.js +334 -0
- package/docs/advisor-comms-assistant-fixes.md +175 -0
- package/docs/api-contracts.md +216 -0
- package/docs/auto-builder-analysis.md +271 -0
- package/docs/data-architecture.md +166 -0
- package/docs/ema-auto-builder-guide.html +394 -0
- package/docs/ema-user-guide.md +1121 -0
- package/docs/mcp-tools-guide.md +149 -0
- package/docs/naming-conventions.md +218 -0
- package/docs/tool-consolidation-proposal.md +427 -0
- package/package.json +98 -0
- package/resources/templates/chat-ai/README.md +119 -0
- package/resources/templates/chat-ai/persona-config.json +111 -0
- package/resources/templates/dashboard-ai/README.md +156 -0
- package/resources/templates/dashboard-ai/persona-config.json +180 -0
- package/resources/templates/voice-ai/README.md +123 -0
- package/resources/templates/voice-ai/persona-config.json +74 -0
- 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
|
+
}
|