@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
@@ -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
+ }
@@ -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
+ }