@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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Sync Metadata (stored in description as HTML comment)
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* The key used to store sync metadata in status_log.
|
|
6
|
+
* Prefixed with underscore to indicate internal/system use.
|
|
7
|
+
*/
|
|
8
|
+
export const SYNC_METADATA_KEY = "_ema_sync";
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Options Configuration
|
|
3
|
+
*
|
|
4
|
+
* Hierarchical configuration for sync behavior, loaded from:
|
|
5
|
+
* 1. .ema.yaml in repo root (shared team defaults)
|
|
6
|
+
* 2. ~/.ema.yaml in user home (personal overrides)
|
|
7
|
+
* 3. Tool parameters (runtime overrides)
|
|
8
|
+
*
|
|
9
|
+
* Resolution order (lowest → highest priority):
|
|
10
|
+
* Built-in defaults → defaults → targets[env] → personas[name] → tool params
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import yaml from "js-yaml";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Schema
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
const SyncBehaviorSchema = z.object({
|
|
21
|
+
sync_status: z.boolean().optional(),
|
|
22
|
+
dry_run: z.boolean().optional(),
|
|
23
|
+
}).strict();
|
|
24
|
+
const RoutingRuleSchema = z.object({
|
|
25
|
+
match: z.object({
|
|
26
|
+
names: z.array(z.string()).optional().default([]),
|
|
27
|
+
ids: z.array(z.string()).optional().default([]),
|
|
28
|
+
}).strict(),
|
|
29
|
+
targets: z.array(z.string()).min(1),
|
|
30
|
+
}).strict();
|
|
31
|
+
const SyncOptionsConfigSchema = z.object({
|
|
32
|
+
defaults: SyncBehaviorSchema.optional().default({}),
|
|
33
|
+
targets: z.record(SyncBehaviorSchema).optional().default({}),
|
|
34
|
+
personas: z.record(SyncBehaviorSchema).optional().default({}),
|
|
35
|
+
routing: z.array(RoutingRuleSchema).optional().default([]),
|
|
36
|
+
}).strict();
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
// Built-in defaults
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
const BUILTIN_DEFAULTS = {
|
|
41
|
+
sync_status: false,
|
|
42
|
+
dry_run: false,
|
|
43
|
+
};
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Config loading
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
let cachedSyncOptions = null;
|
|
48
|
+
function loadYamlFile(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
51
|
+
return yaml.load(content);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function findRepoRoot() {
|
|
58
|
+
// Start from current working directory and look for .git or package.json
|
|
59
|
+
let dir = process.cwd();
|
|
60
|
+
while (dir !== path.dirname(dir)) {
|
|
61
|
+
if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "package.json"))) {
|
|
62
|
+
return dir;
|
|
63
|
+
}
|
|
64
|
+
dir = path.dirname(dir);
|
|
65
|
+
}
|
|
66
|
+
return process.cwd();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load and merge sync options from all config sources.
|
|
70
|
+
* Caches result for performance.
|
|
71
|
+
*/
|
|
72
|
+
export function loadSyncOptions() {
|
|
73
|
+
if (cachedSyncOptions)
|
|
74
|
+
return cachedSyncOptions;
|
|
75
|
+
const repoRoot = findRepoRoot();
|
|
76
|
+
const homeDir = os.homedir();
|
|
77
|
+
// Load from repo .ema.yaml
|
|
78
|
+
const repoConfigPath = path.join(repoRoot, ".ema.yaml");
|
|
79
|
+
const repoConfig = loadYamlFile(repoConfigPath);
|
|
80
|
+
// Load from ~/.ema.yaml
|
|
81
|
+
const userConfigPath = path.join(homeDir, ".ema.yaml");
|
|
82
|
+
const userConfig = loadYamlFile(userConfigPath);
|
|
83
|
+
// Merge configs (user overrides repo)
|
|
84
|
+
const merged = {
|
|
85
|
+
defaults: {},
|
|
86
|
+
targets: {},
|
|
87
|
+
personas: {},
|
|
88
|
+
routing: [],
|
|
89
|
+
};
|
|
90
|
+
// Apply repo config
|
|
91
|
+
if (repoConfig && typeof repoConfig === "object") {
|
|
92
|
+
const parsed = SyncOptionsConfigSchema.safeParse(repoConfig);
|
|
93
|
+
if (parsed.success) {
|
|
94
|
+
merged.defaults = { ...merged.defaults, ...parsed.data.defaults };
|
|
95
|
+
merged.targets = { ...merged.targets, ...parsed.data.targets };
|
|
96
|
+
merged.personas = { ...merged.personas, ...parsed.data.personas };
|
|
97
|
+
merged.routing = [...(parsed.data.routing ?? [])];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Apply user config (overrides repo)
|
|
101
|
+
if (userConfig && typeof userConfig === "object") {
|
|
102
|
+
const parsed = SyncOptionsConfigSchema.safeParse(userConfig);
|
|
103
|
+
if (parsed.success) {
|
|
104
|
+
merged.defaults = { ...merged.defaults, ...parsed.data.defaults };
|
|
105
|
+
// Deep merge targets
|
|
106
|
+
for (const [env, behavior] of Object.entries(parsed.data.targets ?? {})) {
|
|
107
|
+
merged.targets[env] = { ...merged.targets[env], ...behavior };
|
|
108
|
+
}
|
|
109
|
+
// Deep merge personas
|
|
110
|
+
for (const [name, behavior] of Object.entries(parsed.data.personas ?? {})) {
|
|
111
|
+
merged.personas[name] = { ...merged.personas[name], ...behavior };
|
|
112
|
+
}
|
|
113
|
+
// Prepend user routing rules (higher priority)
|
|
114
|
+
merged.routing = [...(parsed.data.routing ?? []), ...merged.routing];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
cachedSyncOptions = merged;
|
|
118
|
+
return merged;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clear the cached sync options (for testing or config reload).
|
|
122
|
+
*/
|
|
123
|
+
export function clearSyncOptionsCache() {
|
|
124
|
+
cachedSyncOptions = null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve sync behavior for a specific persona and target environment.
|
|
128
|
+
*
|
|
129
|
+
* Resolution order (lowest → highest priority):
|
|
130
|
+
* 1. Built-in defaults
|
|
131
|
+
* 2. Config defaults
|
|
132
|
+
* 3. Config targets[targetEnv]
|
|
133
|
+
* 4. Config personas[personaName]
|
|
134
|
+
* 5. Runtime overrides (passed as parameter)
|
|
135
|
+
*/
|
|
136
|
+
export function resolveSyncBehavior(opts) {
|
|
137
|
+
const { personaName, targetEnv, overrides } = opts;
|
|
138
|
+
const config = loadSyncOptions();
|
|
139
|
+
// Start with built-in defaults
|
|
140
|
+
const resolved = { ...BUILTIN_DEFAULTS };
|
|
141
|
+
// Apply config defaults
|
|
142
|
+
if (config.defaults) {
|
|
143
|
+
if (config.defaults.sync_status !== undefined)
|
|
144
|
+
resolved.sync_status = config.defaults.sync_status;
|
|
145
|
+
if (config.defaults.dry_run !== undefined)
|
|
146
|
+
resolved.dry_run = config.defaults.dry_run;
|
|
147
|
+
}
|
|
148
|
+
// Apply target env overrides
|
|
149
|
+
const targetConfig = config.targets?.[targetEnv];
|
|
150
|
+
if (targetConfig) {
|
|
151
|
+
if (targetConfig.sync_status !== undefined)
|
|
152
|
+
resolved.sync_status = targetConfig.sync_status;
|
|
153
|
+
if (targetConfig.dry_run !== undefined)
|
|
154
|
+
resolved.dry_run = targetConfig.dry_run;
|
|
155
|
+
}
|
|
156
|
+
// Apply persona overrides
|
|
157
|
+
if (personaName) {
|
|
158
|
+
const personaConfig = config.personas?.[personaName];
|
|
159
|
+
if (personaConfig) {
|
|
160
|
+
if (personaConfig.sync_status !== undefined)
|
|
161
|
+
resolved.sync_status = personaConfig.sync_status;
|
|
162
|
+
if (personaConfig.dry_run !== undefined)
|
|
163
|
+
resolved.dry_run = personaConfig.dry_run;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Apply runtime overrides
|
|
167
|
+
if (overrides) {
|
|
168
|
+
if (overrides.sync_status !== undefined)
|
|
169
|
+
resolved.sync_status = overrides.sync_status;
|
|
170
|
+
if (overrides.dry_run !== undefined)
|
|
171
|
+
resolved.dry_run = overrides.dry_run;
|
|
172
|
+
}
|
|
173
|
+
return resolved;
|
|
174
|
+
}
|
|
175
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
176
|
+
// Routing
|
|
177
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
/**
|
|
179
|
+
* Check if a string matches a glob pattern (simple implementation).
|
|
180
|
+
* Supports * as wildcard.
|
|
181
|
+
*/
|
|
182
|
+
function matchGlob(pattern, value) {
|
|
183
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
184
|
+
return regex.test(value);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get target environments for a persona based on routing rules.
|
|
188
|
+
*/
|
|
189
|
+
export function getRoutingTargets(personaName, personaId) {
|
|
190
|
+
const config = loadSyncOptions();
|
|
191
|
+
const targets = new Set();
|
|
192
|
+
for (const rule of config.routing ?? []) {
|
|
193
|
+
// Check name patterns
|
|
194
|
+
for (const pattern of rule.match.names ?? []) {
|
|
195
|
+
if (matchGlob(pattern, personaName)) {
|
|
196
|
+
rule.targets.forEach((t) => targets.add(t));
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Check IDs
|
|
201
|
+
if (rule.match.ids?.includes(personaId)) {
|
|
202
|
+
rule.targets.forEach((t) => targets.add(t));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return [...targets];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Validate sync options config.
|
|
209
|
+
*/
|
|
210
|
+
export function validateSyncOptions(input) {
|
|
211
|
+
const result = SyncOptionsConfigSchema.safeParse(input);
|
|
212
|
+
if (!result.success) {
|
|
213
|
+
return { ok: false, errors: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`) };
|
|
214
|
+
}
|
|
215
|
+
return { ok: true, config: result.data };
|
|
216
|
+
}
|
package/dist/sdk/sync.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync SDK - Programmatic interface for persona synchronization
|
|
3
|
+
*/
|
|
4
|
+
import { getMasterEnv, resolveBearerToken } from "./config.js";
|
|
5
|
+
import { EmaClient } from "./client.js";
|
|
6
|
+
import { StateStore } from "./state.js";
|
|
7
|
+
import { fingerprintPersona, syncOnce } from "../sync.js";
|
|
8
|
+
/**
|
|
9
|
+
* Sync SDK for programmatic access to persona synchronization
|
|
10
|
+
*/
|
|
11
|
+
export class SyncSDK {
|
|
12
|
+
config;
|
|
13
|
+
state;
|
|
14
|
+
clients = new Map();
|
|
15
|
+
constructor(config, options) {
|
|
16
|
+
this.config = {
|
|
17
|
+
...config,
|
|
18
|
+
stateDbPath: options?.stateDbPath ?? config.stateDbPath ?? "./ema-agent-sync.sqlite3",
|
|
19
|
+
dryRun: options?.dryRun ?? config.dryRun ?? false,
|
|
20
|
+
};
|
|
21
|
+
this.state = new StateStore(this.config.stateDbPath);
|
|
22
|
+
}
|
|
23
|
+
getClient(envName) {
|
|
24
|
+
if (!this.clients.has(envName)) {
|
|
25
|
+
const envCfg = this.config.environments.find((e) => e.name === envName);
|
|
26
|
+
if (!envCfg)
|
|
27
|
+
throw new Error(`Environment not found: ${envName}`);
|
|
28
|
+
const env = {
|
|
29
|
+
name: envCfg.name,
|
|
30
|
+
baseUrl: envCfg.baseUrl,
|
|
31
|
+
bearerToken: resolveBearerToken(envCfg.bearerTokenEnv),
|
|
32
|
+
};
|
|
33
|
+
this.clients.set(envName, new EmaClient(env));
|
|
34
|
+
}
|
|
35
|
+
return this.clients.get(envName);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the master environment configuration
|
|
39
|
+
*/
|
|
40
|
+
getMasterEnvironment() {
|
|
41
|
+
const master = getMasterEnv(this.config);
|
|
42
|
+
if (!master) {
|
|
43
|
+
throw new Error("No master environment configured (isMaster: true)");
|
|
44
|
+
}
|
|
45
|
+
return master;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get all environment configurations
|
|
49
|
+
*/
|
|
50
|
+
getEnvironments() {
|
|
51
|
+
return this.config.environments;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run a full sync from master to all configured targets
|
|
55
|
+
*/
|
|
56
|
+
async runSync() {
|
|
57
|
+
return syncOnce(this.config, this.state);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* List all personas from the master environment
|
|
61
|
+
*/
|
|
62
|
+
async listMasterPersonas() {
|
|
63
|
+
const master = this.getMasterEnvironment();
|
|
64
|
+
const client = this.getClient(master.name);
|
|
65
|
+
return client.getPersonasForTenant();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get a specific persona by ID from the master environment
|
|
69
|
+
*/
|
|
70
|
+
async getMasterPersona(personaId) {
|
|
71
|
+
const personas = await this.listMasterPersonas();
|
|
72
|
+
return personas.find((p) => p.id === personaId) ?? null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get a specific persona by name from the master environment
|
|
76
|
+
*/
|
|
77
|
+
async getMasterPersonaByName(name) {
|
|
78
|
+
const personas = await this.listMasterPersonas();
|
|
79
|
+
return personas.find((p) => p.name === name) ?? null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get all persona mappings between master and target environments
|
|
83
|
+
*/
|
|
84
|
+
getAllMappings() {
|
|
85
|
+
// Query all mappings from state store
|
|
86
|
+
const master = this.getMasterEnvironment();
|
|
87
|
+
const mappings = [];
|
|
88
|
+
// We need to expose a method to get all mappings from state
|
|
89
|
+
// For now, return empty - this would need state.ts enhancement
|
|
90
|
+
return mappings;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the target persona ID for a master persona in a specific environment
|
|
94
|
+
*/
|
|
95
|
+
getTargetPersonaId(masterPersonaId, targetEnv) {
|
|
96
|
+
const master = this.getMasterEnvironment();
|
|
97
|
+
const targetCfg = this.config.environments.find((e) => e.name === targetEnv);
|
|
98
|
+
if (!targetCfg)
|
|
99
|
+
return null;
|
|
100
|
+
return this.state.getMapping({
|
|
101
|
+
masterEnv: master.name,
|
|
102
|
+
masterUserId: master.userId ?? master.name,
|
|
103
|
+
masterPersonaId,
|
|
104
|
+
targetEnv,
|
|
105
|
+
targetUserId: targetCfg.userId ?? targetCfg.name,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get sync status for a specific persona
|
|
110
|
+
*/
|
|
111
|
+
async getPersonaSyncStatus(personaId) {
|
|
112
|
+
const master = this.getMasterEnvironment();
|
|
113
|
+
const persona = await this.getMasterPersona(personaId);
|
|
114
|
+
if (!persona)
|
|
115
|
+
return null;
|
|
116
|
+
const fp = fingerprintPersona(persona);
|
|
117
|
+
const storedFp = this.state.getFingerprint({
|
|
118
|
+
env: master.name,
|
|
119
|
+
envUserId: master.userId ?? master.name,
|
|
120
|
+
personaId,
|
|
121
|
+
});
|
|
122
|
+
const targetMappings = [];
|
|
123
|
+
for (const env of this.config.environments) {
|
|
124
|
+
if (env.isMaster)
|
|
125
|
+
continue;
|
|
126
|
+
const targetId = this.state.getMapping({
|
|
127
|
+
masterEnv: master.name,
|
|
128
|
+
masterUserId: master.userId ?? master.name,
|
|
129
|
+
masterPersonaId: personaId,
|
|
130
|
+
targetEnv: env.name,
|
|
131
|
+
targetUserId: env.userId ?? env.name,
|
|
132
|
+
});
|
|
133
|
+
if (targetId) {
|
|
134
|
+
const targetFp = this.state.getFingerprint({
|
|
135
|
+
env: env.name,
|
|
136
|
+
envUserId: env.userId ?? env.name,
|
|
137
|
+
personaId: targetId,
|
|
138
|
+
});
|
|
139
|
+
targetMappings.push({
|
|
140
|
+
targetEnv: env.name,
|
|
141
|
+
targetPersonaId: targetId,
|
|
142
|
+
targetFingerprint: targetFp ?? undefined,
|
|
143
|
+
inSync: targetFp === fp,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
personaId,
|
|
149
|
+
personaName: persona.name,
|
|
150
|
+
fingerprint: fp,
|
|
151
|
+
lastSeenAt: undefined, // Would need state enhancement to get this
|
|
152
|
+
isSynced: targetMappings.every((m) => m.inSync),
|
|
153
|
+
targetMappings,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Sync a specific persona by ID
|
|
158
|
+
*/
|
|
159
|
+
async syncPersona(personaId) {
|
|
160
|
+
const persona = await this.getMasterPersona(personaId);
|
|
161
|
+
if (!persona) {
|
|
162
|
+
return { success: false, synced: [], errors: [`Persona not found: ${personaId}`] };
|
|
163
|
+
}
|
|
164
|
+
// Create a temporary config that only routes this persona
|
|
165
|
+
const tempConfig = {
|
|
166
|
+
...this.config,
|
|
167
|
+
routing: [
|
|
168
|
+
{
|
|
169
|
+
personaIds: [personaId],
|
|
170
|
+
targetEnvs: this.config.environments
|
|
171
|
+
.filter((e) => !e.isMaster)
|
|
172
|
+
.map((e) => e.name),
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
// Reuse existing state store to avoid SQLite lock contention
|
|
177
|
+
try {
|
|
178
|
+
const result = await syncOnce(tempConfig, this.state);
|
|
179
|
+
return {
|
|
180
|
+
success: result.synced > 0 || result.skipped > 0,
|
|
181
|
+
synced: result.synced > 0
|
|
182
|
+
? this.config.environments.filter((e) => !e.isMaster).map((e) => e.name)
|
|
183
|
+
: [],
|
|
184
|
+
errors: [],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
synced: [],
|
|
191
|
+
errors: [e instanceof Error ? e.message : String(e)],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Sync a specific persona by name
|
|
197
|
+
*/
|
|
198
|
+
async syncPersonaByName(name) {
|
|
199
|
+
const persona = await this.getMasterPersonaByName(name);
|
|
200
|
+
if (!persona) {
|
|
201
|
+
return { success: false, synced: [], errors: [`Persona not found: ${name}`] };
|
|
202
|
+
}
|
|
203
|
+
const result = await this.syncPersona(persona.id);
|
|
204
|
+
return { ...result, personaId: persona.id };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get recent sync runs
|
|
208
|
+
*/
|
|
209
|
+
getRecentRuns(limit = 10) {
|
|
210
|
+
// Would need state.ts enhancement to query runs
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Close all connections
|
|
215
|
+
*/
|
|
216
|
+
close() {
|
|
217
|
+
this.state.close();
|
|
218
|
+
this.clients.clear();
|
|
219
|
+
}
|
|
220
|
+
}
|