@ema.co/mcp-toolkit 1.5.2 → 1.6.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.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

@@ -46,138 +46,183 @@ export function generateConsolidatedTools(envNames, defaultEnv) {
46
46
  inputSchema: { type: "object", properties: {}, required: [] },
47
47
  },
48
48
  // ═══════════════════════════════════════════════════════════════════════
49
- // 2. PERSONA - AI Employee management (CRUD + compare + versioning)
49
+ // 2. PERSONA - Unified AI Employee management (create, modify, analyze, list)
50
50
  // ═══════════════════════════════════════════════════════════════════════
51
51
  {
52
52
  name: "persona",
53
- description: `Unified AI Employee (persona) management.
53
+ description: `Create, modify, analyze, or list AI Employees. ONE tool for everything.
54
54
 
55
- **Get single** (default when id provided):
56
- persona(id="IT Support Bot")
55
+ ## ⚠️ ONE CALL CREATES EVERYTHING - THEN STOP
56
+
57
+ If requirements are unclear, first call \`template(questions=true)\` to get what to ask.
58
+ Then make ONE call with all gathered info:
59
+
60
+ \`\`\`
61
+ persona(
62
+ input="Voice AI SDR: qualifies leads, identifies use-case, sends follow-up email",
63
+ type="voice",
64
+ name="SP - SDR Test", // REQUIRED: The actual persona name shown in Ema
65
+ preview=false
66
+ )
67
+ \`\`\`
68
+
69
+ **After success, STOP. Do NOT make follow-up calls to "fix" or "enhance".**
70
+
71
+ **MCP handles internally:** template selection, config generation, widget formatting, welcome message, API calls.
72
+
73
+ ## CRITICAL: The \`name\` Parameter
74
+
75
+ The \`name\` parameter is the **actual persona name** in the Ema platform - NOT derived from input.
76
+
77
+ ❌ name omitted → MCP parses name from input (often wrong)
78
+ ✅ name="SP - SDR Test" → Exact name shown in platform
79
+
80
+ ## Create NEW AI Employee
81
+
82
+ persona(input="<what it should do>", type="voice", name="Actual Name", preview=false)
83
+
84
+ ## Modify EXISTING (workflow changes)
85
+
86
+ persona(id="abc-123", input="add HITL before email", preview=false)
87
+
88
+ ## Update Config Only (voice settings, welcome message)
89
+
90
+ persona(id="abc-123", input="Update welcome message to: Hello!", preview=false)
91
+
92
+ MCP auto-detects config vs workflow changes.
93
+
94
+ ## Analyze/Get
95
+
96
+ persona(id="abc-123")
57
97
  persona(id="abc-123", include_workflow=true)
58
98
 
59
- **List/Search** (when --all or filters used):
99
+ ## Optimize (auto-fix issues)
100
+
101
+ persona(id="abc-123", optimize=true, preview=false)
102
+
103
+ ## List/Search
104
+
60
105
  persona(all=true)
61
106
  persona(query="support", status="active")
62
- persona(trigger_type="voice")
63
107
 
64
- **Create**:
65
- persona(mode="create", name="New Bot", type="voice")
108
+ ## Simple vs Complex Workflows
66
109
 
67
- **Update**:
68
- persona(id="abc-123", mode="update", name="Renamed")
110
+ **Simple** (Q&A, search + respond): Deploys directly → \`status: "success"\`
69
111
 
70
- **Compare**:
71
- persona(id="abc-123", mode="compare", compare_to="def-456")
112
+ **Complex** (email, HITL, multi-intent): Returns \`status: "needs_llm_generation"\` with:
113
+ - \`llm_prompt\`: System + user prompts for workflow generation
114
+ - \`available_actions\`: Action catalog from API
115
+ - \`hint\`: How to complete deployment
72
116
 
73
- **Templates** (list available templates):
74
- persona(templates=true)
117
+ For complex workflows, send the prompt to an LLM and deploy:
118
+ \`persona(workflow_def=<llm_response>, ...)\`
119
+
120
+ ## Key Rules
75
121
 
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)`,
122
+ 1. **ONE CALL** - Put everything in \`input\`, explicit \`name\`, MCP handles rest
123
+ 2. **STOP after success** - Don't make follow-up "fix" calls
124
+ 3. **preview=false** - Deploys to Ema platform`,
83
125
  inputSchema: withEnv({
84
- // ID (or exact name) (optional - if omitted with all=true, lists all)
126
+ // === IDENTITY ===
85
127
  id: {
86
128
  type: "string",
87
- description: "Persona ID (UUID) or exact name. Omit for list operations."
129
+ description: "Persona ID (UUID) or exact name. Omit when creating new."
88
130
  },
89
- // Deprecated alias (backwards compatibility)
90
131
  identifier: {
91
132
  type: "string",
92
133
  deprecated: true,
93
- description: "DEPRECATED: use id. Persona ID (UUID) or exact name.",
134
+ description: "DEPRECATED: use id.",
94
135
  },
95
- // Mode (defaults to "get" if id provided, "list" if all=true)
96
- mode: {
136
+ // === CREATE/MODIFY (the main way to use this tool) ===
137
+ input: {
97
138
  type: "string",
98
- enum: ["get", "list", "create", "update", "compare", "version_create", "version_list", "version_get", "version_compare", "version_restore", "version_policy"],
99
- description: "Operation mode. Default: 'get' with id, 'list' without."
139
+ description: "Natural language description. For new: 'Voice AI for sales...'. For modify: 'add HITL before email'.",
100
140
  },
101
- // List/Search flags
141
+ type: {
142
+ type: "string",
143
+ enum: ["voice", "chat", "dashboard"],
144
+ description: "AI Employee type. REQUIRED for creating new."
145
+ },
146
+ name: { type: "string", description: "REQUIRED for new: The actual persona name shown in Ema platform (e.g., 'SP - SDR Test'). Don't derive from input." },
147
+ description: { type: "string", description: "Description of what it does." },
148
+ preview: {
149
+ type: "boolean",
150
+ description: "Default: true (safe). Set false to deploy changes."
151
+ },
152
+ // === ANALYZE/OPTIMIZE ===
153
+ optimize: {
154
+ type: "boolean",
155
+ description: "Auto-fix detected issues. Use with id.",
156
+ },
157
+ include: {
158
+ type: "array",
159
+ items: { type: "string", enum: ["issues", "connections", "fixes", "metrics"] },
160
+ description: "What to include in analysis output.",
161
+ },
162
+ include_workflow: { type: "boolean", description: "Include full workflow_def in response" },
163
+ include_fingerprint: { type: "boolean", description: "Include config hash" },
164
+ // === LIST/SEARCH ===
102
165
  all: { type: "boolean", description: "List all personas" },
103
166
  query: { type: "string", description: "Search by name (partial match)" },
104
167
  status: { type: "string", description: "Filter: 'active', 'inactive', 'draft'" },
105
168
  trigger_type: { type: "string", description: "Filter: 'voice', 'chat', 'dashboard'" },
106
169
  limit: { type: "number", description: "Max results (default: 50)" },
107
- // Get flags
108
- include_workflow: { type: "boolean", description: "Include full workflow_def" },
109
- include_fingerprint: { type: "boolean", description: "Include config hash" },
110
- // Create flags
111
- name: { type: "string", description: "Name (for create/update)" },
112
- description: { type: "string", description: "Description (for create/update)" },
113
- type: {
114
- type: "string",
115
- enum: ["voice", "chat", "dashboard"],
116
- description: "Persona type (for create)"
117
- },
118
- template_id: { type: "string", description: "Template ID (for create)" },
119
- clone_from: { type: "string", description: "Clone from persona ID (for create)" },
120
- clone_data: { type: "boolean", description: "Also clone knowledge base files when using clone_from (default: false)" },
121
- // Update flags
122
- enabled: { type: "boolean", description: "Enable/disable (for update)" },
170
+ // === COMPARE ===
171
+ compare_to: { type: "string", description: "Second persona ID for comparison" },
172
+ compare_env: { type: "string", description: "Environment of compare_to persona" },
173
+ // === ADVANCED/OVERRIDE ===
123
174
  proto_config: {
124
175
  type: "object",
125
- description: "Voice/chat settings (welcomeMessage, identityAndPurpose, etc.) for update"
176
+ description: "Override voice/chat settings. Usually auto-generated from input."
126
177
  },
127
178
  workflow: {
128
179
  type: "object",
129
- description: "Workflow definition to set (for update)"
180
+ description: "Direct workflow JSON (advanced). Usually auto-generated."
130
181
  },
131
- // Compare flags
132
- compare_to: { type: "string", description: "Second persona ID (for compare)" },
133
- compare_env: { type: "string", description: "Environment of compare_to persona" },
134
- // Templates flag
182
+ workflow_def: {
183
+ type: "object",
184
+ description: "Alias for workflow (backwards compatibility)."
185
+ },
186
+ template_id: { type: "string", description: "Specific template ID (usually auto-selected)" },
187
+ clone_from: { type: "string", description: "Clone from existing persona ID" },
188
+ clone_data: { type: "boolean", description: "Also clone knowledge base files" },
189
+ enabled: { type: "boolean", description: "Enable/disable persona" },
190
+ // === TEMPLATES ===
135
191
  templates: { type: "boolean", description: "List available templates" },
136
- // Version management flags
137
- version: { type: "string", description: "Version identifier (e.g., 'v3', 'latest', or UUID)" },
138
- v1: { type: "string", description: "First version for comparison (version_compare mode)" },
139
- v2: { type: "string", description: "Second version for comparison (version_compare mode)" },
140
- message: { type: "string", description: "Version message/description (version_create mode)" },
141
- auto_on_deploy: { type: "boolean", description: "Auto-create version on deploy (version_policy mode)" },
142
- auto_on_sync: { type: "boolean", description: "Auto-create version on sync (version_policy mode)" },
143
- max_versions: { type: "number", description: "Max versions to keep (version_policy mode)" },
192
+ // === VERSION MANAGEMENT ===
193
+ mode: {
194
+ type: "string",
195
+ enum: ["version_create", "version_list", "version_get", "version_compare", "version_restore", "version_policy"],
196
+ description: "Version management mode. Only needed for version operations."
197
+ },
198
+ version: { type: "string", description: "Version identifier (e.g., 'v3', 'latest')" },
199
+ v1: { type: "string", description: "First version for comparison" },
200
+ v2: { type: "string", description: "Second version for comparison" },
201
+ message: { type: "string", description: "Version message/description" },
202
+ auto_on_deploy: { type: "boolean", description: "Auto-create version on deploy" },
203
+ auto_on_sync: { type: "boolean", description: "Auto-create version on sync" },
204
+ max_versions: { type: "number", description: "Max versions to keep" },
144
205
  }),
145
206
  },
146
207
  // ═══════════════════════════════════════════════════════════════════════
147
- // 3. WORKFLOW - Unified workflow operations (greenfield & brownfield)
208
+ // 3. WORKFLOW - DEPRECATED: Use persona() instead
148
209
  // ═══════════════════════════════════════════════════════════════════════
149
210
  {
150
211
  name: "workflow",
151
- description: `Create, modify, analyze, or deploy AI Employee workflows.
152
-
153
- ## Creating a NEW AI Employee (Greenfield)
154
-
155
- Creates persona from template, configures settings. Template provides valid workflow structure.
156
-
157
- workflow(input="Voice AI for sales development", name="My Sales SDR", type="voice", preview=false)
212
+ description: `⚠️ DEPRECATED: Use \`persona()\` instead. This tool routes to persona.
158
213
 
159
- Returns: { deployed_to: { persona_id, created: true }, next_steps: [...] }
214
+ ## Migration Guide
160
215
 
161
- To customize the workflow AFTER creation, use modify mode with the persona_id.
216
+ OLD (deprecated):
217
+ workflow(input="...", type="voice", name="Bot", preview=false)
218
+ workflow(persona_id="abc", input="add HITL")
162
219
 
163
- ## Modifying an EXISTING AI Employee (Brownfield)
220
+ NEW (use this):
221
+ persona(input="...", type="voice", name="Bot", preview=false)
222
+ persona(id="abc", input="add HITL")
164
223
 
165
- Uses LLM-native workflow transformation. Fetches existing workflow, transforms it, deploys.
166
-
167
- workflow(persona_id="abc-123", input="add HITL approval before sending emails")
168
- workflow(persona_id="abc-123", input="add a new Sales intent category", preview=false)
169
-
170
- ## Analyzing a Workflow
171
-
172
- workflow(persona_id="abc-123") # Returns issues, connections, metrics
173
- workflow(persona_id="abc-123", optimize=true, preview=false) # Auto-fix and deploy
174
-
175
- ## Key Rules
176
-
177
- 1. **preview=true (default)**: Safe - returns result without deploying
178
- 2. **preview=false**: Deploys changes to Ema platform
179
- 3. **Greenfield creates from template** - then modify workflow separately if needed
180
- 4. **Brownfield transforms existing** - uses decompile → transform → compile`,
224
+ The \`persona\` tool now handles everything: create, modify, analyze, list.
225
+ This \`workflow\` tool still works but shows a deprecation warning.`,
181
226
  inputSchema: withEnv({
182
227
  // === REQUIRED for creating/modifying ===
183
228
  input: {
@@ -296,23 +341,24 @@ Uses LLM-native workflow transformation. Fetches existing workflow, transforms i
296
341
  // ═══════════════════════════════════════════════════════════════════════
297
342
  {
298
343
  name: "template",
299
- description: `Get workflow patterns, widget references, and configuration templates.
344
+ description: `Get qualifying questions and reference patterns.
345
+
346
+ ## 🎯 PRIMARY USE: Get Questions to Ask User
347
+
348
+ **Before creating an AI Employee, call this to get what to ask:**
349
+ template(questions=true) // All qualifying questions
350
+ template(questions=true, category="Voice") // Voice-specific questions
300
351
 
301
- **Workflow patterns**:
302
- template(pattern="intent-routing")
303
- template(patterns=true)
304
- template(patterns=true, type="voice")
352
+ Returns structured questions about: type, intents, data sources, actions, approvals, etc.
353
+ Ask the user these questions, then put answers into ONE workflow() call.
305
354
 
306
- **Widget reference**:
307
- template(widgets="voice")
308
- template(widgets="chat")
355
+ ## Reference (understand options, not for manual building)
309
356
 
310
- **Persona config template**:
311
- template(config="voice") # Voice AI settings template
357
+ template(pattern="intent-routing") // See pattern structure
358
+ template(patterns=true) // List available patterns
359
+ template(widgets="voice") // Widget reference
312
360
 
313
- **Qualifying questions** (for requirements gathering):
314
- template(questions=true)
315
- template(questions=true, category="Voice")`,
361
+ ⚠️ Do NOT copy/paste configs from these - MCP generates them internally.`,
316
362
  inputSchema: {
317
363
  type: "object",
318
364
  properties: {
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Action Registry - Helpers for extracting action metadata from API
3
+ *
4
+ * Uses existing client.listActions() - no duplicate caching needed.
5
+ * Just provides helpers to extract version/namespace from raw API response.
6
+ */
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ // Helpers to extract version/namespace from raw API response
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ /**
11
+ * Extract action name, version, and namespaces from raw API response.
12
+ */
13
+ export function parseActionDefinition(action) {
14
+ // API returns typeName.name.namespaces and typeName.version
15
+ const raw = action;
16
+ const typeName = raw.typeName;
17
+ if (!typeName?.name?.name)
18
+ return null;
19
+ return {
20
+ name: typeName.name.name,
21
+ version: typeName.version ?? "v0",
22
+ namespaces: typeName.name.namespaces ?? ["actions", "emainternal"],
23
+ };
24
+ }
25
+ /**
26
+ * Extract template info from raw API response.
27
+ */
28
+ export function parseTemplateDefinition(template) {
29
+ if (!template.id || !template.name)
30
+ return null;
31
+ const raw = template;
32
+ return {
33
+ id: template.id,
34
+ name: template.name,
35
+ triggerType: raw.trigger_type ?? 1,
36
+ };
37
+ }
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // Registry Class (lightweight, uses client directly)
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+ export class ActionRegistry {
42
+ actions = new Map();
43
+ templates = new Map();
44
+ templatesByType = new Map();
45
+ loaded = false;
46
+ /**
47
+ * Load from raw API data.
48
+ * Call client.listActions() and client.getPersonaTemplates() externally
49
+ * and pass the results here.
50
+ */
51
+ loadFromData(actions, templates) {
52
+ this.actions.clear();
53
+ this.templates.clear();
54
+ this.templatesByType.clear();
55
+ for (const action of actions) {
56
+ const def = parseActionDefinition(action);
57
+ if (def) {
58
+ this.actions.set(def.name, def);
59
+ }
60
+ }
61
+ for (const template of templates) {
62
+ const def = parseTemplateDefinition(template);
63
+ if (def) {
64
+ this.templates.set(def.id, def);
65
+ // First template of each trigger type wins
66
+ if (!this.templatesByType.has(def.triggerType)) {
67
+ this.templatesByType.set(def.triggerType, def);
68
+ }
69
+ }
70
+ }
71
+ this.loaded = true;
72
+ }
73
+ /**
74
+ * Get action version. Falls back to "v0".
75
+ */
76
+ getVersion(actionName) {
77
+ return this.actions.get(actionName)?.version ?? "v0";
78
+ }
79
+ /**
80
+ * Get action namespaces. Falls back to ["actions", "emainternal"].
81
+ */
82
+ getNamespaces(actionName) {
83
+ return this.actions.get(actionName)?.namespaces ?? ["actions", "emainternal"];
84
+ }
85
+ /**
86
+ * Get action definition.
87
+ */
88
+ getAction(actionName) {
89
+ return this.actions.get(actionName);
90
+ }
91
+ /**
92
+ * Get template by ID.
93
+ */
94
+ getTemplate(id) {
95
+ return this.templates.get(id);
96
+ }
97
+ /**
98
+ * Get template for persona type.
99
+ * Trigger types: 1=CHAT, 2=DASHBOARD, 4=VOICE
100
+ */
101
+ getTemplateForType(type) {
102
+ const triggerTypes = {
103
+ voice: 4,
104
+ chat: 1,
105
+ dashboard: 2,
106
+ };
107
+ return this.templatesByType.get(triggerTypes[type]);
108
+ }
109
+ isLoaded() {
110
+ return this.loaded;
111
+ }
112
+ }
113
+ // ─────────────────────────────────────────────────────────────────────────────
114
+ // Factory function (loads from API)
115
+ // ─────────────────────────────────────────────────────────────────────────────
116
+ /**
117
+ * Create and load action registry from API.
118
+ * Uses client methods directly - no caching here (resources.ts handles that for MCP).
119
+ */
120
+ export async function ensureActionRegistry(client) {
121
+ const registry = new ActionRegistry();
122
+ const [actions, templates] = await Promise.all([
123
+ client.listActions().catch(() => []),
124
+ client.getPersonaTemplates().catch(() => []),
125
+ ]);
126
+ registry.loadFromData(actions, templates);
127
+ return registry;
128
+ }
@@ -100,80 +100,22 @@ export class EmaClient {
100
100
  const controller = new AbortController();
101
101
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
102
102
  const fullUrl = `${this.env.baseUrl.replace(/\/$/, "")}${path}`;
103
- // #region agent log
104
- // H3/H4: verify wire-level request shape + size (no secrets).
105
- try {
106
- if (process.env.EMA_MCP_DEBUG_LOGS !== "1") {
107
- // disabled
108
- }
109
- else {
110
- const isUpdatePersona = path.includes("/api/personas/update_persona");
111
- const jsonAny = opts?.json;
112
- const workflowAny = jsonAny?.workflow ?? jsonAny?.workflow_def;
113
- const actions = workflowAny?.actions ?? [];
114
- const actionNames = Array.isArray(actions)
115
- ? actions
116
- .slice(0, 50)
117
- .map((a) => a?.name)
118
- .filter((n) => typeof n === "string")
119
- : [];
120
- const containsHitl = actionNames.some((n) => n.toLowerCase().includes("hitl")) || JSON.stringify(actionNames).toLowerCase().includes("hitl");
121
- const jsonSize = opts?.json !== undefined ? JSON.stringify(opts.json).length : 0;
122
- fetch('http://127.0.0.1:7245/ingest/c9b6768a-494d-4365-bd46-bf6c43dc1e22', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'src/sdk/client.ts:requestWithRetries:pre', message: 'HTTP request', data: { env: this.env.name, codeDir: process.env.EMA_MCP_CODE_DIR ?? null, cwd: process.cwd(), method, path, isUpdatePersona, attempt, retries, hasJson: opts?.json !== undefined, jsonSize, hasWorkflow: !!workflowAny, actionCount: Array.isArray(actions) ? actions.length : 0, containsHitl }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'pre-fix', hypothesisId: 'H3' }) }).catch(() => { });
123
- }
103
+ // gRPC-Connect endpoints need Connect-Protocol-Version header
104
+ const isGrpcConnect = path.includes(".v1.") || path.includes(".v2.");
105
+ const headers = {
106
+ Authorization: `Bearer ${this.env.bearerToken}`,
107
+ "Content-Type": "application/json",
108
+ };
109
+ if (isGrpcConnect) {
110
+ headers["Connect-Protocol-Version"] = "1";
124
111
  }
125
- catch { }
126
- // #endregion agent log
127
112
  const response = await fetch(fullUrl, {
128
113
  method,
129
- headers: {
130
- Authorization: `Bearer ${this.env.bearerToken}`,
131
- "Content-Type": "application/json",
132
- },
114
+ headers,
133
115
  body: opts?.json !== undefined ? JSON.stringify(opts.json) : undefined,
134
116
  signal: controller.signal,
135
117
  });
136
118
  clearTimeout(timeoutId);
137
- // #region agent log
138
- // H1/H2/H4: capture response status + request id for correlation (no secrets).
139
- try {
140
- if (process.env.EMA_MCP_DEBUG_LOGS !== "1") {
141
- // disabled
142
- }
143
- else {
144
- const reqId = response.headers.get("x-request-id") ?? response.headers.get("x-correlation-id") ?? response.headers.get("x-amzn-trace-id");
145
- fetch('http://127.0.0.1:7245/ingest/c9b6768a-494d-4365-bd46-bf6c43dc1e22', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'src/sdk/client.ts:requestWithRetries:post', message: 'HTTP response', data: { env: this.env.name, method, path, status: response.status, ok: response.ok, attempt, durationMs: Date.now() - startedAt, reqId }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'pre-fix', hypothesisId: 'H1' }) }).catch(() => { });
146
- }
147
- }
148
- catch { }
149
- // #endregion agent log
150
- // #region agent log
151
- // H1/H2: for update_persona failures, log first part of error body (redacted/truncated).
152
- try {
153
- if (process.env.EMA_MCP_DEBUG_LOGS !== "1") {
154
- // disabled
155
- }
156
- else {
157
- const isUpdatePersona = path.includes("/api/personas/update_persona");
158
- if (isUpdatePersona && !response.ok) {
159
- const cloned = response.clone();
160
- const txt = await cloned.text();
161
- const snippet = txt.length > 1200 ? `${txt.slice(0, 1200)}…(truncated)` : txt;
162
- let parsedDetail;
163
- let hasGweIssues = false;
164
- try {
165
- const j = JSON.parse(txt);
166
- const detail = j.detail ?? j.message ?? j.error;
167
- parsedDetail = typeof detail === "string" ? detail : undefined;
168
- hasGweIssues = Array.isArray(j.gwe_issues);
169
- }
170
- catch { }
171
- fetch('http://127.0.0.1:7245/ingest/c9b6768a-494d-4365-bd46-bf6c43dc1e22', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'src/sdk/client.ts:requestWithRetries:errorBody', message: 'HTTP error body (update_persona)', data: { env: this.env.name, method, path, status: response.status, parsedDetail, hasGweIssues, bodySnippet: snippet }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'pre-fix', hypothesisId: 'H2' }) }).catch(() => { });
172
- }
173
- }
174
- }
175
- catch { }
176
- // #endregion agent log
177
119
  // Handle 401 - attempt token refresh once
178
120
  if (response.status === 401 && !hasAttemptedRefresh && this.tokenRefreshConfig?.refreshCallback) {
179
121
  hasAttemptedRefresh = true;
@@ -324,15 +266,6 @@ export class EmaClient {
324
266
  });
325
267
  if (!resp.ok) {
326
268
  const body = await resp.text();
327
- // #region agent log
328
- try {
329
- if (this.debugLogsEnabled) {
330
- const snippet = body.length > 2000 ? `${body.slice(0, 2000)}…(truncated)` : body;
331
- fetch('http://127.0.0.1:7245/ingest/c9b6768a-494d-4365-bd46-bf6c43dc1e22', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'src/sdk/client.ts:checkWorkflow:error', message: 'CheckWorkflow failed', data: { env: this.env.name, status: resp.status, bodySnippet: snippet }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'pre-fix', hypothesisId: 'H5' }) }).catch(() => { });
332
- }
333
- }
334
- catch { }
335
- // #endregion agent log
336
269
  throw new EmaApiError({
337
270
  statusCode: resp.status,
338
271
  body,
@@ -340,35 +273,70 @@ export class EmaClient {
340
273
  });
341
274
  }
342
275
  const data = (await resp.json());
343
- // #region agent log
344
- try {
345
- if (this.debugLogsEnabled) {
346
- const keys = Object.keys(data);
347
- fetch('http://127.0.0.1:7245/ingest/c9b6768a-494d-4365-bd46-bf6c43dc1e22', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'src/sdk/client.ts:checkWorkflow:ok', message: 'CheckWorkflow ok', data: { env: this.env.name, keys }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'pre-fix', hypothesisId: 'H5' }) }).catch(() => { });
348
- }
349
- }
350
- catch { }
351
- // #endregion agent log
352
276
  return data;
353
277
  }
354
278
  async updateAiEmployee(req, opts) {
355
279
  // Use /api/personas/update_persona - same endpoint as Ema frontend
280
+ // Debug logging for troubleshooting
281
+ if (opts?.verbose || process.env.EMA_DEBUG) {
282
+ console.error("[EmaClient] updateAiEmployee request:", JSON.stringify({
283
+ persona_id: req.persona_id,
284
+ has_workflow: !!req.workflow,
285
+ workflow_actions: req.workflow ? req.workflow.actions : undefined,
286
+ has_proto_config: !!req.proto_config,
287
+ }, null, 2));
288
+ }
356
289
  const resp = await this.requestWithRetries("POST", "/api/personas/update_persona", {
357
290
  json: req,
358
291
  });
359
292
  if (!resp.ok) {
360
293
  const body = await resp.text();
294
+ // Debug logging for errors
295
+ if (opts?.verbose || process.env.EMA_DEBUG) {
296
+ console.error("[EmaClient] updateAiEmployee FAILED:", resp.status, body);
297
+ console.error("[EmaClient] Request that failed:", JSON.stringify(req, null, 2).slice(0, 5000));
298
+ }
361
299
  // Try to parse error body for better error messages
362
300
  let errorDetail = "";
363
301
  try {
364
302
  const errorJson = JSON.parse(body);
365
303
  // Ema API returns InvalidPersonaResponse with 'detail' field
366
304
  errorDetail = errorJson.detail ?? errorJson.message ?? errorJson.error ?? "";
367
- // Also check for GWE (workflow) issues
368
- if (errorJson.gwe_issues) {
369
- const issues = errorJson.gwe_issues;
370
- if (Array.isArray(issues)) {
371
- errorDetail += ` Workflow issues: ${issues.map((i) => i.message ?? i.detail ?? JSON.stringify(i)).join("; ")}`;
305
+ // Parse GWE (workflow) issues - it's an object, not an array
306
+ // See WorkflowIssues schema in OpenAPI spec
307
+ if (errorJson.gwe_issues && typeof errorJson.gwe_issues === "object") {
308
+ const gwe = errorJson.gwe_issues;
309
+ const issuesParts = [];
310
+ if (gwe.detail) {
311
+ issuesParts.push(`Detail: ${gwe.detail}`);
312
+ }
313
+ if (Array.isArray(gwe.missing_widgets) && gwe.missing_widgets.length > 0) {
314
+ issuesParts.push(`Missing widgets: ${gwe.missing_widgets.join(", ")}`);
315
+ }
316
+ if (Array.isArray(gwe.unready_widgets) && gwe.unready_widgets.length > 0) {
317
+ const unready = gwe.unready_widgets.map((w) => `${w.widget_name}: ${w.message}`);
318
+ issuesParts.push(`Unready widgets: ${unready.join("; ")}`);
319
+ }
320
+ if (Array.isArray(gwe.missing_inputs) && gwe.missing_inputs.length > 0) {
321
+ const missing = gwe.missing_inputs.map((i) => `${i.action_name}.${i.input_name}`);
322
+ issuesParts.push(`Missing inputs: ${missing.join(", ")}`);
323
+ }
324
+ if (Array.isArray(gwe.mismatched_inputs) && gwe.mismatched_inputs.length > 0) {
325
+ const mismatched = gwe.mismatched_inputs.map((i) => `${i.action_name}.${i.input_name}`);
326
+ issuesParts.push(`Mismatched inputs: ${mismatched.join(", ")}`);
327
+ }
328
+ if (Array.isArray(gwe.named_result_errors) && gwe.named_result_errors.length > 0) {
329
+ const resultErrs = gwe.named_result_errors.map((e) => `${e.named_result_key}: ${e.error_description}`);
330
+ issuesParts.push(`Result errors: ${resultErrs.join("; ")}`);
331
+ }
332
+ if (gwe.has_cycles) {
333
+ issuesParts.push("Workflow has cycles");
334
+ }
335
+ if (gwe.has_no_outputs) {
336
+ issuesParts.push("Workflow has no outputs");
337
+ }
338
+ if (issuesParts.length > 0) {
339
+ errorDetail += ` [GWE Issues: ${issuesParts.join(" | ")}]`;
372
340
  }
373
341
  }
374
342
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Auto-generated TypeScript types from Ema Platform OpenAPI spec.
3
+ * Generated from: Ema Platform API v1.0.0
4
+ * Generated at: 2026-01-16T13:46:50.729Z
5
+ *
6
+ * DO NOT EDIT MANUALLY - regenerate with: npm run generate:types
7
+ */
8
+ export {};
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ // API Operation Types
11
+ // ─────────────────────────────────────────────────────────────────────────────