@ema.co/mcp-toolkit 2026.1.24 → 2026.1.26-3
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.
- package/README.md +10 -2
- package/dist/mcp/handlers/action/index.js +3 -18
- package/dist/mcp/handlers/data/index.js +385 -41
- package/dist/mcp/handlers/data/templates.js +107 -0
- package/dist/mcp/handlers/deprecation.js +50 -0
- package/dist/mcp/handlers/env/index.js +8 -4
- package/dist/mcp/handlers/knowledge/index.js +44 -237
- package/dist/mcp/handlers/persona/create.js +5 -11
- package/dist/mcp/handlers/persona/index.js +5 -1
- package/dist/mcp/handlers/persona/version.js +234 -0
- package/dist/mcp/handlers/reference/index.js +101 -1
- package/dist/mcp/handlers/sync/index.js +3 -18
- package/dist/mcp/handlers/template/index.js +75 -10
- package/dist/mcp/handlers/workflow/analyze.js +171 -0
- package/dist/mcp/handlers/workflow/compare.js +70 -0
- package/dist/mcp/handlers/workflow/compile.js +39 -0
- package/dist/mcp/handlers/workflow/deploy.js +73 -0
- package/dist/mcp/handlers/workflow/generate.js +350 -0
- package/dist/mcp/handlers/workflow/index.js +136 -0
- package/dist/mcp/handlers/workflow/modify.js +456 -0
- package/dist/mcp/handlers/workflow/optimize.js +136 -0
- package/dist/mcp/handlers/workflow/types.js +4 -0
- package/dist/mcp/handlers/workflow/utils.js +132 -0
- package/dist/mcp/handlers-consolidated.js +62 -2691
- package/dist/mcp/prompts.js +13 -14
- package/dist/mcp/resources.js +55 -54
- package/dist/mcp/server.js +93 -124
- package/dist/mcp/{tools-v2.js → tools.js} +1 -1
- package/dist/mcp/workflow-operations.js +2 -2
- package/dist/sdk/client-adapter.js +267 -32
- package/dist/sdk/client.js +31 -15
- package/dist/sdk/ema-client.js +183 -0
- package/dist/sdk/generated/template-fallbacks.js +123 -0
- package/dist/sdk/guidance.js +65 -11
- package/dist/sdk/index.js +3 -1
- package/dist/sdk/knowledge.js +16 -86
- package/dist/sdk/workflow-intent.js +27 -0
- package/dist/sdk/workflow-transformer.js +0 -342
- package/docs/DEBUG-ANALYSIS-unused-category-type-mismatch.md +481 -0
- package/docs/TODO-fix-analyzer-and-modify.md +182 -0
- package/package.json +9 -4
- package/dist/mcp/tools-consolidated.js +0 -866
- package/dist/mcp/tools-legacy.js +0 -736
- package/docs/CODEBASE-ANALYSIS-2026-01-23.md +0 -936
- package/docs/CODEBASE-ANALYSIS-PRIORITIZED.md +0 -774
- package/docs/api-contracts.md +0 -216
- package/docs/auto-builder-analysis.md +0 -271
- package/docs/blog/mcp-tool-design-lessons.md +0 -309
- package/docs/data-architecture.md +0 -166
- package/docs/ema-auto-builder-guide.html +0 -394
- package/docs/lessons-learned.md +0 -209
- package/docs/llm-native-workflow-design.md +0 -252
- package/docs/local-generation.md +0 -508
- package/docs/mcp-flow-diagram.md +0 -135
- package/docs/migration/action-composition-migration.md +0 -270
- package/docs/naming-conventions.md +0 -278
- package/docs/proposals/HANDOFF-tool-restructure.md +0 -526
- package/docs/proposals/action-composition.md +0 -490
- package/docs/proposals/explicit-method-restructure.md +0 -328
- package/docs/proposals/mcp-tool-restructure-2026-01.md +0 -366
- package/docs/proposals/self-contained-guidance.md +0 -427
- package/docs/proto-sdk-generation.md +0 -242
- package/docs/release-impact.md +0 -102
- package/docs/release-process.md +0 -157
- package/docs/staging.RULE.md +0 -142
- package/docs/test-persona-creation.md +0 -196
- package/docs/tool-consolidation-v2.md +0 -225
- package/docs/tool-response-standards.md +0 -256
- package/resources/docs/getting-started.md +0 -97
- package/resources/templates/auto-builder-rules.md +0 -224
- package/resources/templates/chat-ai/README.md +0 -119
- package/resources/templates/chat-ai/persona-config.json +0 -111
- package/resources/templates/dashboard-ai/README.md +0 -156
- package/resources/templates/dashboard-ai/persona-config.json +0 -180
- package/resources/templates/demo-scenarios/README.md +0 -63
- package/resources/templates/demo-scenarios/test-published-package.md +0 -116
- package/resources/templates/document-gen-ai/README.md +0 -132
- package/resources/templates/document-gen-ai/persona-config.json +0 -316
- package/resources/templates/voice-ai/README.md +0 -123
- package/resources/templates/voice-ai/persona-config.json +0 -74
- package/resources/templates/voice-ai/workflow-prompt.md +0 -121
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This file defines supported workflow modification operations.
|
|
5
5
|
* Used by:
|
|
6
6
|
* - analyzeModificationRequest() in handlers-consolidated.ts
|
|
7
|
-
* - Tool descriptions in tools
|
|
7
|
+
* - Tool descriptions in tools.ts
|
|
8
8
|
* - Tests to verify capabilities
|
|
9
9
|
*
|
|
10
10
|
* Add new operations here, and they'll automatically appear in tool docs.
|
|
@@ -64,7 +64,7 @@ export const ALTERNATIVES = {
|
|
|
64
64
|
};
|
|
65
65
|
/**
|
|
66
66
|
* Generate tool description for the `input` parameter.
|
|
67
|
-
* Called by tools
|
|
67
|
+
* Called by tools.ts to ensure description matches actual capabilities.
|
|
68
68
|
*/
|
|
69
69
|
export function generateInputDescription() {
|
|
70
70
|
const ops = SUPPORTED_OPERATIONS.map(op => op.example).join("', '");
|
|
@@ -1,34 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EmaClientAdapter -
|
|
2
|
+
* EmaClientAdapter - Drop-in replacement for legacy EmaClient using EmaClientV2
|
|
3
3
|
*
|
|
4
|
-
* This adapter
|
|
5
|
-
*
|
|
6
|
-
* the same interface as EmaClient for seamless comparison.
|
|
4
|
+
* This adapter provides full compatibility with the legacy EmaClient interface
|
|
5
|
+
* while using the new auto-generated EmaClientV2 under the hood.
|
|
7
6
|
*
|
|
8
7
|
* Usage:
|
|
9
8
|
* ```typescript
|
|
10
|
-
*
|
|
9
|
+
* // Replace: import { EmaClient } from './client.js';
|
|
10
|
+
* // With: import { EmaClientAdapter as EmaClient } from './client-adapter.js';
|
|
11
|
+
*
|
|
12
|
+
* const client = new EmaClientAdapter({
|
|
11
13
|
* name: 'dev',
|
|
12
14
|
* baseUrl: 'https://api.dev.ema.co',
|
|
13
15
|
* bearerToken: 'xxx',
|
|
14
16
|
* });
|
|
15
17
|
*
|
|
16
18
|
* // Same API as EmaClient
|
|
17
|
-
* const personas = await
|
|
19
|
+
* const personas = await client.getPersonasForTenant();
|
|
18
20
|
* ```
|
|
19
21
|
*/
|
|
20
22
|
import { EmaClientV2 } from './ema-client.js';
|
|
21
23
|
/**
|
|
22
24
|
* Adapter that wraps EmaClientV2 with the legacy EmaClient interface.
|
|
23
25
|
*
|
|
24
|
-
* This
|
|
26
|
+
* This is a drop-in replacement for EmaClient.
|
|
25
27
|
*/
|
|
26
28
|
export class EmaClientAdapter {
|
|
27
29
|
client;
|
|
28
30
|
env;
|
|
29
|
-
|
|
31
|
+
tokenRefreshConfig;
|
|
32
|
+
refreshIntervalId;
|
|
33
|
+
constructor(env, opts) {
|
|
30
34
|
this.env = env;
|
|
31
35
|
this.client = new EmaClientV2(env);
|
|
36
|
+
this.tokenRefreshConfig = opts?.tokenRefresh;
|
|
37
|
+
// Start background refresh if configured
|
|
38
|
+
if (this.tokenRefreshConfig?.backgroundRefresh !== false && this.tokenRefreshConfig?.refreshCallback) {
|
|
39
|
+
this.startBackgroundRefresh();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Start background token refresh */
|
|
43
|
+
startBackgroundRefresh() {
|
|
44
|
+
const intervalMs = this.tokenRefreshConfig?.refreshIntervalMs ?? 50 * 60 * 1000; // 50 minutes default
|
|
45
|
+
this.refreshIntervalId = setInterval(async () => {
|
|
46
|
+
try {
|
|
47
|
+
await this.refreshToken();
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
console.error('[EmaClientAdapter] Background token refresh failed:', e);
|
|
51
|
+
}
|
|
52
|
+
}, intervalMs);
|
|
53
|
+
}
|
|
54
|
+
/** Stop background token refresh */
|
|
55
|
+
stopBackgroundRefresh() {
|
|
56
|
+
if (this.refreshIntervalId) {
|
|
57
|
+
clearInterval(this.refreshIntervalId);
|
|
58
|
+
this.refreshIntervalId = undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Manually trigger token refresh */
|
|
62
|
+
async refreshToken() {
|
|
63
|
+
if (!this.tokenRefreshConfig?.refreshCallback) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const newToken = await this.tokenRefreshConfig.refreshCallback();
|
|
68
|
+
this.updateToken(newToken);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Access the underlying EmaClientV2 for advanced use */
|
|
76
|
+
get v2Client() {
|
|
77
|
+
return this.client;
|
|
32
78
|
}
|
|
33
79
|
/**
|
|
34
80
|
* Get the environment name
|
|
@@ -54,28 +100,23 @@ export class EmaClientAdapter {
|
|
|
54
100
|
updateToken(newToken) {
|
|
55
101
|
this.client.updateToken(newToken);
|
|
56
102
|
}
|
|
57
|
-
//
|
|
58
|
-
// Persona Operations
|
|
59
|
-
//
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
|
+
// Persona Operations
|
|
105
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
60
106
|
/**
|
|
61
107
|
* List all personas for the current tenant
|
|
62
108
|
*/
|
|
63
109
|
async getPersonasForTenant() {
|
|
64
110
|
const personas = await this.client.listPersonas();
|
|
65
|
-
// Map generated PersonaDto to legacy PersonaDTO interface
|
|
66
111
|
return personas.map(p => this.mapToLegacyPersona(p));
|
|
67
112
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Alias for getPersonasForTenant
|
|
70
|
-
*/
|
|
113
|
+
/** Alias for getPersonasForTenant */
|
|
71
114
|
async getAiEmployeesForTenant() {
|
|
72
115
|
return this.getPersonasForTenant();
|
|
73
116
|
}
|
|
74
117
|
/**
|
|
75
118
|
* Get a single persona by ID
|
|
76
|
-
*
|
|
77
|
-
* Returns null for non-existent personas (matching legacy EmaClient behavior).
|
|
78
|
-
* The API may return 404 or 422 for invalid IDs - both are treated as "not found".
|
|
119
|
+
* Returns null for non-existent personas.
|
|
79
120
|
*/
|
|
80
121
|
async getPersonaById(personaId) {
|
|
81
122
|
try {
|
|
@@ -85,7 +126,6 @@ export class EmaClientAdapter {
|
|
|
85
126
|
return this.mapToLegacyPersona(persona);
|
|
86
127
|
}
|
|
87
128
|
catch (error) {
|
|
88
|
-
// Handle 404 (not found) and 422 (invalid ID format) as null
|
|
89
129
|
if (error instanceof Error) {
|
|
90
130
|
const apiError = error;
|
|
91
131
|
if (apiError.statusCode === 404 || apiError.statusCode === 422) {
|
|
@@ -95,26 +135,220 @@ export class EmaClientAdapter {
|
|
|
95
135
|
throw error;
|
|
96
136
|
}
|
|
97
137
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Create a new AI Employee
|
|
140
|
+
*/
|
|
141
|
+
async createAiEmployee(req) {
|
|
142
|
+
// persona_template_id is required by the API
|
|
143
|
+
const templateId = req.persona_template_id ?? req.template_id;
|
|
144
|
+
if (!templateId) {
|
|
145
|
+
throw new Error('persona_template_id or template_id is required');
|
|
146
|
+
}
|
|
147
|
+
const result = await this.client.createAiEmployee({
|
|
148
|
+
name: req.name,
|
|
149
|
+
persona_template_id: templateId,
|
|
150
|
+
description: req.description,
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
persona_id: result.persona_id ?? '',
|
|
154
|
+
success: !!result.persona_id,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Update an existing AI Employee
|
|
159
|
+
*/
|
|
160
|
+
async updateAiEmployee(req) {
|
|
161
|
+
const result = await this.client.updateAiEmployee({
|
|
162
|
+
persona_id: req.persona_id,
|
|
163
|
+
workflow: req.workflow,
|
|
164
|
+
proto_config: req.proto_config,
|
|
165
|
+
name: req.name,
|
|
166
|
+
description: req.description,
|
|
167
|
+
});
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Delete a persona
|
|
172
|
+
*/
|
|
173
|
+
async deletePersona(personaId) {
|
|
174
|
+
return this.client.deletePersona(personaId);
|
|
175
|
+
}
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
177
|
+
// Template Operations
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
101
179
|
/**
|
|
102
180
|
* List all persona templates
|
|
103
181
|
*/
|
|
104
182
|
async getPersonaTemplates() {
|
|
105
183
|
const templates = await this.client.listPersonaTemplates();
|
|
106
|
-
// Map generated PersonaTemplateDto to legacy PersonaTemplateDTO interface
|
|
107
184
|
return templates.map(t => this.mapToLegacyTemplate(t));
|
|
108
185
|
}
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
186
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
187
|
+
// Action Operations
|
|
188
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
189
|
+
/**
|
|
190
|
+
* List all available actions
|
|
191
|
+
*/
|
|
192
|
+
async listActions() {
|
|
193
|
+
const response = await this.client.listActions();
|
|
194
|
+
// Map gRPC ActionType to ActionDTO
|
|
195
|
+
return (response.actions ?? []).map(a => {
|
|
196
|
+
const typeName = a.typeName;
|
|
197
|
+
const name = typeName?.name?.name ?? '';
|
|
198
|
+
return {
|
|
199
|
+
id: name,
|
|
200
|
+
name: name,
|
|
201
|
+
displayName: a.displayName ?? name,
|
|
202
|
+
description: a.description ?? '',
|
|
203
|
+
category: String(a.category ?? ''),
|
|
204
|
+
typeName: typeName,
|
|
205
|
+
// inputs/outputs are complex proto maps - keep as raw for now
|
|
206
|
+
inputs: a.inputs,
|
|
207
|
+
outputs: a.outputs,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/** Alias for listActions */
|
|
212
|
+
async listAgents() {
|
|
213
|
+
return this.listActions();
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* List actions used in a specific workflow
|
|
217
|
+
*/
|
|
218
|
+
async listActionsFromWorkflow(workflowId) {
|
|
219
|
+
const response = await this.client.listActionsFromWorkflow(workflowId);
|
|
220
|
+
// Map ActionType[] to string[] of action names
|
|
221
|
+
return (response.actions ?? []).map(a => {
|
|
222
|
+
const typeName = a.typeName;
|
|
223
|
+
return typeName?.name?.name ?? '';
|
|
224
|
+
}).filter(Boolean);
|
|
225
|
+
}
|
|
226
|
+
/** Alias for listActionsFromWorkflow */
|
|
227
|
+
async listAgentsFromWorkflow(workflowId) {
|
|
228
|
+
return this.listActionsFromWorkflow(workflowId);
|
|
229
|
+
}
|
|
230
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
231
|
+
// Dashboard Operations
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
233
|
+
/**
|
|
234
|
+
* Get dashboard schema
|
|
235
|
+
*/
|
|
236
|
+
async getDashboardSchema(personaId, _dashboardId) {
|
|
237
|
+
const response = await this.client.getDashboardSchema();
|
|
238
|
+
// Extract columns from proto response
|
|
239
|
+
const schema = response;
|
|
240
|
+
return {
|
|
241
|
+
columns: schema.schema?.columns ?? [],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
245
|
+
// Data Source Operations
|
|
246
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
247
|
+
/**
|
|
248
|
+
* Upload a data source file
|
|
249
|
+
*/
|
|
250
|
+
async uploadDataSource(personaId, fileContent, filename, opts) {
|
|
251
|
+
return this.client.uploadDataSource(personaId, fileContent, filename, opts);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Delete a data source file
|
|
255
|
+
*/
|
|
256
|
+
async deleteDataSource(personaId, fileId) {
|
|
257
|
+
return this.client.deleteDataSource(personaId, fileId);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* List data source files
|
|
261
|
+
*/
|
|
262
|
+
async listDataSourceFiles(personaId, opts) {
|
|
263
|
+
const response = await this.client.listDataSourceFiles(personaId, opts);
|
|
264
|
+
// Map proto ContentNode to simplified file object
|
|
265
|
+
const nodes = response.nodes ?? [];
|
|
266
|
+
return {
|
|
267
|
+
files: nodes.map(n => ({
|
|
268
|
+
id: n.id ?? '',
|
|
269
|
+
name: n.name ?? '',
|
|
270
|
+
size: n.size ?? 0,
|
|
271
|
+
uploadedAt: n.createdAt ?? '',
|
|
272
|
+
})),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Get data source aggregates
|
|
277
|
+
*/
|
|
278
|
+
async getDataSourceAggregates(personaId, widgetName) {
|
|
279
|
+
const response = await this.client.getDataSourceAggregates(personaId, widgetName);
|
|
280
|
+
// Map proto aggregates
|
|
281
|
+
const agg = response;
|
|
282
|
+
const aggregates = agg.aggregates ?? [];
|
|
283
|
+
let total = 0, processed = 0, failed = 0;
|
|
284
|
+
for (const a of aggregates) {
|
|
285
|
+
total += a.count ?? 0;
|
|
286
|
+
if (a.status === 'processed' || a.status === 'PROCESSED')
|
|
287
|
+
processed += a.count ?? 0;
|
|
288
|
+
if (a.status === 'failed' || a.status === 'FAILED')
|
|
289
|
+
failed += a.count ?? 0;
|
|
290
|
+
}
|
|
291
|
+
return { total, processed, failed };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Replicate data between personas
|
|
295
|
+
*/
|
|
296
|
+
async replicateData(sourcePersonaId, targetPersonaId, widgetMappings) {
|
|
297
|
+
const response = await this.client.replicateData(sourcePersonaId, targetPersonaId, widgetMappings);
|
|
298
|
+
const rep = response;
|
|
299
|
+
return { requestId: rep.requestId ?? '' };
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get replication status
|
|
303
|
+
*/
|
|
304
|
+
async getReplicationStatus(requestId, _personaId) {
|
|
305
|
+
const response = await this.client.getReplicationStatus(requestId);
|
|
306
|
+
const rep = response;
|
|
307
|
+
const statusMap = {
|
|
308
|
+
0: 'unknown',
|
|
309
|
+
1: 'in_progress',
|
|
310
|
+
2: 'completed',
|
|
311
|
+
3: 'failed',
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
status: statusMap[rep.status ?? 0] ?? 'unknown',
|
|
315
|
+
progress: rep.progress,
|
|
316
|
+
failedReason: rep.failedReason,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Wait for replication to complete
|
|
321
|
+
*/
|
|
322
|
+
async waitForReplication(requestId, personaId, opts) {
|
|
323
|
+
return this.client.waitForReplication(requestId, personaId, opts);
|
|
324
|
+
}
|
|
325
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
326
|
+
// Sync Metadata Operations
|
|
327
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
328
|
+
/**
|
|
329
|
+
* Get sync metadata from a persona
|
|
330
|
+
*/
|
|
331
|
+
getSyncMetadata(persona) {
|
|
332
|
+
// Sync metadata is stored in custom_metadata
|
|
333
|
+
const customMeta = persona.custom_metadata;
|
|
334
|
+
if (!customMeta)
|
|
335
|
+
return null;
|
|
336
|
+
const syncData = customMeta['ema_mcp_sync'];
|
|
337
|
+
if (!syncData || typeof syncData !== 'object')
|
|
338
|
+
return null;
|
|
339
|
+
return syncData;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Check if persona was synced from another environment
|
|
343
|
+
*/
|
|
344
|
+
isSyncedPersona(persona) {
|
|
345
|
+
return this.getSyncMetadata(persona) !== null;
|
|
346
|
+
}
|
|
347
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
348
|
+
// Private Helpers
|
|
349
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
112
350
|
/**
|
|
113
351
|
* Map generated PersonaDto to legacy PersonaDTO interface
|
|
114
|
-
*
|
|
115
|
-
* The generated type has nullable fields (string | null | undefined)
|
|
116
|
-
* while the legacy type expects (string | undefined). This method
|
|
117
|
-
* normalizes null to undefined for compatibility.
|
|
118
352
|
*/
|
|
119
353
|
mapToLegacyPersona(p) {
|
|
120
354
|
return {
|
|
@@ -122,10 +356,10 @@ export class EmaClientAdapter {
|
|
|
122
356
|
name: this.nullToUndefined(p.name),
|
|
123
357
|
description: this.nullToUndefined(p.description),
|
|
124
358
|
template_id: this.nullToUndefined(p.template_id),
|
|
125
|
-
templateId: this.nullToUndefined(p.template_id),
|
|
359
|
+
templateId: this.nullToUndefined(p.template_id),
|
|
126
360
|
workflow_id: this.nullToUndefined(p.workflow_id),
|
|
127
361
|
workflow_def: p.workflow_def,
|
|
128
|
-
workflow: p.workflow_def,
|
|
362
|
+
workflow: p.workflow_def,
|
|
129
363
|
workflow_interface: p.workflow_interface,
|
|
130
364
|
proto_config: p.proto_config,
|
|
131
365
|
status: this.nullToUndefined(p.status),
|
|
@@ -137,6 +371,7 @@ export class EmaClientAdapter {
|
|
|
137
371
|
workflow_dashboard_id: this.nullToUndefined(p.workflow_dashboard_id),
|
|
138
372
|
persona_interaction_type: this.nullToUndefined(p.persona_interaction_type),
|
|
139
373
|
embedding_enabled: p.embedding_enabled,
|
|
374
|
+
custom_metadata: p.custom_metadata,
|
|
140
375
|
};
|
|
141
376
|
}
|
|
142
377
|
/**
|
package/dist/sdk/client.js
CHANGED
|
@@ -393,27 +393,43 @@ export class EmaClient {
|
|
|
393
393
|
const existingPersona = await this.getPersonaById(req.persona_id);
|
|
394
394
|
const existingWorkflow = existingPersona?.workflow_def;
|
|
395
395
|
const existingWfName = existingWorkflow?.workflowName;
|
|
396
|
+
// Deep clone workflow for modifications
|
|
397
|
+
const fixedWorkflow = JSON.parse(JSON.stringify(req.workflow));
|
|
396
398
|
if (existingWfName?.name) {
|
|
397
|
-
//
|
|
398
|
-
const fixedWorkflow = JSON.parse(JSON.stringify(req.workflow));
|
|
399
|
+
// Copy namespace from existing workflow
|
|
399
400
|
fixedWorkflow.workflowName = existingWfName;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (results) {
|
|
403
|
-
const fixedResults = {};
|
|
404
|
-
for (const [key, value] of Object.entries(results)) {
|
|
405
|
-
if (value.actionName && value.outputName) {
|
|
406
|
-
const correctKey = `${value.actionName}.${value.outputName}`;
|
|
407
|
-
fixedResults[correctKey] = value;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
fixedWorkflow.results = fixedResults;
|
|
401
|
+
if (opts?.verbose || process.env.EMA_DEBUG) {
|
|
402
|
+
console.error("[EmaClient] Copied workflow namespace from existing:", existingWfName.name);
|
|
411
403
|
}
|
|
412
|
-
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
// CRITICAL FIX: Generate a valid namespace for personas without existing workflows.
|
|
407
|
+
// The API requires workflowName to match the persona's namespace format.
|
|
408
|
+
// Format: ["ema", "personas", "<persona_id>"] with name "workflow"
|
|
409
|
+
const generatedWfName = {
|
|
410
|
+
name: {
|
|
411
|
+
namespaces: ["ema", "personas", req.persona_id],
|
|
412
|
+
name: "workflow",
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
fixedWorkflow.workflowName = generatedWfName;
|
|
413
416
|
if (opts?.verbose || process.env.EMA_DEBUG) {
|
|
414
|
-
console.error("[EmaClient]
|
|
417
|
+
console.error("[EmaClient] Generated workflow namespace for new persona:", generatedWfName.name);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Also fix results format if needed - API expects "<actionName>.<outputName>" keys
|
|
421
|
+
const results = fixedWorkflow.results;
|
|
422
|
+
if (results) {
|
|
423
|
+
const fixedResults = {};
|
|
424
|
+
for (const [key, value] of Object.entries(results)) {
|
|
425
|
+
if (value.actionName && value.outputName) {
|
|
426
|
+
const correctKey = `${value.actionName}.${value.outputName}`;
|
|
427
|
+
fixedResults[correctKey] = value;
|
|
428
|
+
}
|
|
415
429
|
}
|
|
430
|
+
fixedWorkflow.results = fixedResults;
|
|
416
431
|
}
|
|
432
|
+
finalReq = { ...req, workflow: fixedWorkflow };
|
|
417
433
|
}
|
|
418
434
|
// Debug logging for troubleshooting
|
|
419
435
|
if (opts?.verbose || process.env.EMA_DEBUG) {
|
package/dist/sdk/ema-client.js
CHANGED
|
@@ -166,6 +166,17 @@ export class EmaClientV2 {
|
|
|
166
166
|
// Copy namespace from existing workflow
|
|
167
167
|
workflow = { ...workflow, workflowName: existingWfName };
|
|
168
168
|
}
|
|
169
|
+
else {
|
|
170
|
+
// Generate a valid namespace for personas without existing workflows
|
|
171
|
+
// Format: ["ema", "personas", "<persona_id>"] with name "workflow"
|
|
172
|
+
const generatedWfName = {
|
|
173
|
+
name: {
|
|
174
|
+
namespaces: ["ema", "personas", personaId],
|
|
175
|
+
name: "workflow",
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
workflow = { ...workflow, workflowName: generatedWfName };
|
|
179
|
+
}
|
|
169
180
|
}
|
|
170
181
|
const result = await api.updatePersona({
|
|
171
182
|
client: this.restClient,
|
|
@@ -287,6 +298,178 @@ export class EmaClientV2 {
|
|
|
287
298
|
const personas = await this.listPersonas();
|
|
288
299
|
return personas.filter(p => this.getSyncMetadata(p) !== null);
|
|
289
300
|
}
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
302
|
+
// File Upload/Delete - REST API (not yet in OpenAPI spec)
|
|
303
|
+
// These are hand-written until the OpenAPI spec is updated
|
|
304
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
305
|
+
/**
|
|
306
|
+
* Upload a file to an AI Employee's knowledge base.
|
|
307
|
+
* API endpoint: POST /api/v2/upload/files (multipart/form-data)
|
|
308
|
+
*/
|
|
309
|
+
async uploadDataSource(personaId, fileContent, filename, opts) {
|
|
310
|
+
const widgetName = opts?.widgetName ?? "fileUpload";
|
|
311
|
+
const tags = opts?.tags ?? widgetName;
|
|
312
|
+
const mimeType = opts?.mimeType ?? this.detectMimeType(filename);
|
|
313
|
+
// Build multipart form data manually
|
|
314
|
+
const boundary = `----EmaUploadBoundary${Date.now()}`;
|
|
315
|
+
const parts = [];
|
|
316
|
+
// persona_id field
|
|
317
|
+
parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="persona_id"\r\n\r\n${personaId}`);
|
|
318
|
+
// widget_name field
|
|
319
|
+
parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="widget_name"\r\n\r\n${widgetName}`);
|
|
320
|
+
// tags field (required by API, defaults to widget_name)
|
|
321
|
+
parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="tags"\r\n\r\n${tags}`);
|
|
322
|
+
// Build the file part
|
|
323
|
+
const filePart = [
|
|
324
|
+
`--${boundary}`,
|
|
325
|
+
`Content-Disposition: form-data; name="file"; filename="${filename}"`,
|
|
326
|
+
`Content-Type: ${mimeType}`,
|
|
327
|
+
"",
|
|
328
|
+
"", // Content will be appended as binary
|
|
329
|
+
].join("\r\n");
|
|
330
|
+
// Combine everything
|
|
331
|
+
const headerBuffer = Buffer.from(parts.join("\r\n") + "\r\n" + filePart);
|
|
332
|
+
const fileBuffer = Buffer.isBuffer(fileContent) ? fileContent : Buffer.from(fileContent);
|
|
333
|
+
const footerBuffer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
334
|
+
const bodyBuffer = Buffer.concat([headerBuffer, fileBuffer, footerBuffer]);
|
|
335
|
+
const controller = new AbortController();
|
|
336
|
+
const timeoutId = setTimeout(() => controller.abort(), 60_000);
|
|
337
|
+
try {
|
|
338
|
+
const resp = await fetch(`${this.env.baseUrl.replace(/\/$/, "")}/api/v2/upload/files`, {
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers: {
|
|
341
|
+
Authorization: `Bearer ${this.env.bearerToken}`,
|
|
342
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
343
|
+
"X-Persona-Id": personaId,
|
|
344
|
+
},
|
|
345
|
+
body: bodyBuffer,
|
|
346
|
+
signal: controller.signal,
|
|
347
|
+
});
|
|
348
|
+
if (!resp.ok) {
|
|
349
|
+
const errorBody = await resp.text();
|
|
350
|
+
throw new EmaApiError({
|
|
351
|
+
statusCode: resp.status,
|
|
352
|
+
body: errorBody,
|
|
353
|
+
message: `uploadDataSource failed (${this.env.name})`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const result = await resp.json();
|
|
357
|
+
return {
|
|
358
|
+
fileId: result.file_id ?? result.id ?? "",
|
|
359
|
+
status: result.status ?? "uploaded",
|
|
360
|
+
filename: filename,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
clearTimeout(timeoutId);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Delete a data source file from an AI Employee's knowledge base.
|
|
369
|
+
* API endpoint: DELETE /api/v2/upload/files/{file_id}
|
|
370
|
+
*/
|
|
371
|
+
async deleteDataSource(personaId, fileId) {
|
|
372
|
+
const controller = new AbortController();
|
|
373
|
+
const timeoutId = setTimeout(() => controller.abort(), 30_000);
|
|
374
|
+
try {
|
|
375
|
+
const resp = await fetch(`${this.env.baseUrl.replace(/\/$/, "")}/api/v2/upload/files/${fileId}?persona_id=${personaId}`, {
|
|
376
|
+
method: "DELETE",
|
|
377
|
+
headers: {
|
|
378
|
+
Authorization: `Bearer ${this.env.bearerToken}`,
|
|
379
|
+
"X-Persona-Id": personaId,
|
|
380
|
+
},
|
|
381
|
+
signal: controller.signal,
|
|
382
|
+
});
|
|
383
|
+
if (!resp.ok && resp.status !== 404) {
|
|
384
|
+
const errorBody = await resp.text();
|
|
385
|
+
throw new EmaApiError({
|
|
386
|
+
statusCode: resp.status,
|
|
387
|
+
body: errorBody,
|
|
388
|
+
message: `deleteDataSource failed (${this.env.name})`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return { success: true, fileId };
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
clearTimeout(timeoutId);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Delete a persona
|
|
399
|
+
* API endpoint: DELETE /api/personas/{persona_id}
|
|
400
|
+
*/
|
|
401
|
+
async deletePersona(personaId) {
|
|
402
|
+
const controller = new AbortController();
|
|
403
|
+
const timeoutId = setTimeout(() => controller.abort(), 30_000);
|
|
404
|
+
try {
|
|
405
|
+
const resp = await fetch(`${this.env.baseUrl.replace(/\/$/, "")}/api/personas/${personaId}`, {
|
|
406
|
+
method: "DELETE",
|
|
407
|
+
headers: {
|
|
408
|
+
Authorization: `Bearer ${this.env.bearerToken}`,
|
|
409
|
+
},
|
|
410
|
+
signal: controller.signal,
|
|
411
|
+
});
|
|
412
|
+
if (!resp.ok) {
|
|
413
|
+
const errorBody = await resp.text();
|
|
414
|
+
throw new EmaApiError({
|
|
415
|
+
statusCode: resp.status,
|
|
416
|
+
body: errorBody,
|
|
417
|
+
message: `deletePersona failed (${this.env.name})`,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return { success: true };
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
clearTimeout(timeoutId);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Wait for data replication to complete (polling helper)
|
|
428
|
+
*/
|
|
429
|
+
async waitForReplication(requestId, personaId, opts) {
|
|
430
|
+
const timeout = opts?.timeoutMs ?? 60_000;
|
|
431
|
+
const interval = opts?.pollIntervalMs ?? 2_000;
|
|
432
|
+
const start = Date.now();
|
|
433
|
+
while (Date.now() - start < timeout) {
|
|
434
|
+
const result = await this.getReplicationStatus(requestId);
|
|
435
|
+
if (result.status === 2 /* COMPLETED */) {
|
|
436
|
+
return { status: "completed" };
|
|
437
|
+
}
|
|
438
|
+
if (result.status === 3 /* FAILED */) {
|
|
439
|
+
return {
|
|
440
|
+
status: "failed",
|
|
441
|
+
failedReason: result.failedReason || "Unknown error"
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
445
|
+
}
|
|
446
|
+
return { status: "timeout" };
|
|
447
|
+
}
|
|
448
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
449
|
+
// Private Helpers
|
|
450
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
451
|
+
/** Detect MIME type from filename extension */
|
|
452
|
+
detectMimeType(filename) {
|
|
453
|
+
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
|
|
454
|
+
const mimeTypes = {
|
|
455
|
+
'pdf': 'application/pdf',
|
|
456
|
+
'txt': 'text/plain',
|
|
457
|
+
'csv': 'text/csv',
|
|
458
|
+
'json': 'application/json',
|
|
459
|
+
'xml': 'application/xml',
|
|
460
|
+
'html': 'text/html',
|
|
461
|
+
'md': 'text/markdown',
|
|
462
|
+
'doc': 'application/msword',
|
|
463
|
+
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
464
|
+
'xls': 'application/vnd.ms-excel',
|
|
465
|
+
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
466
|
+
'png': 'image/png',
|
|
467
|
+
'jpg': 'image/jpeg',
|
|
468
|
+
'jpeg': 'image/jpeg',
|
|
469
|
+
'gif': 'image/gif',
|
|
470
|
+
};
|
|
471
|
+
return mimeTypes[ext] ?? 'application/octet-stream';
|
|
472
|
+
}
|
|
290
473
|
}
|
|
291
474
|
// Default export for convenience
|
|
292
475
|
export default EmaClientV2;
|