@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.

Files changed (81) hide show
  1. package/README.md +10 -2
  2. package/dist/mcp/handlers/action/index.js +3 -18
  3. package/dist/mcp/handlers/data/index.js +385 -41
  4. package/dist/mcp/handlers/data/templates.js +107 -0
  5. package/dist/mcp/handlers/deprecation.js +50 -0
  6. package/dist/mcp/handlers/env/index.js +8 -4
  7. package/dist/mcp/handlers/knowledge/index.js +44 -237
  8. package/dist/mcp/handlers/persona/create.js +5 -11
  9. package/dist/mcp/handlers/persona/index.js +5 -1
  10. package/dist/mcp/handlers/persona/version.js +234 -0
  11. package/dist/mcp/handlers/reference/index.js +101 -1
  12. package/dist/mcp/handlers/sync/index.js +3 -18
  13. package/dist/mcp/handlers/template/index.js +75 -10
  14. package/dist/mcp/handlers/workflow/analyze.js +171 -0
  15. package/dist/mcp/handlers/workflow/compare.js +70 -0
  16. package/dist/mcp/handlers/workflow/compile.js +39 -0
  17. package/dist/mcp/handlers/workflow/deploy.js +73 -0
  18. package/dist/mcp/handlers/workflow/generate.js +350 -0
  19. package/dist/mcp/handlers/workflow/index.js +136 -0
  20. package/dist/mcp/handlers/workflow/modify.js +456 -0
  21. package/dist/mcp/handlers/workflow/optimize.js +136 -0
  22. package/dist/mcp/handlers/workflow/types.js +4 -0
  23. package/dist/mcp/handlers/workflow/utils.js +132 -0
  24. package/dist/mcp/handlers-consolidated.js +62 -2691
  25. package/dist/mcp/prompts.js +13 -14
  26. package/dist/mcp/resources.js +55 -54
  27. package/dist/mcp/server.js +93 -124
  28. package/dist/mcp/{tools-v2.js → tools.js} +1 -1
  29. package/dist/mcp/workflow-operations.js +2 -2
  30. package/dist/sdk/client-adapter.js +267 -32
  31. package/dist/sdk/client.js +31 -15
  32. package/dist/sdk/ema-client.js +183 -0
  33. package/dist/sdk/generated/template-fallbacks.js +123 -0
  34. package/dist/sdk/guidance.js +65 -11
  35. package/dist/sdk/index.js +3 -1
  36. package/dist/sdk/knowledge.js +16 -86
  37. package/dist/sdk/workflow-intent.js +27 -0
  38. package/dist/sdk/workflow-transformer.js +0 -342
  39. package/docs/DEBUG-ANALYSIS-unused-category-type-mismatch.md +481 -0
  40. package/docs/TODO-fix-analyzer-and-modify.md +182 -0
  41. package/package.json +9 -4
  42. package/dist/mcp/tools-consolidated.js +0 -866
  43. package/dist/mcp/tools-legacy.js +0 -736
  44. package/docs/CODEBASE-ANALYSIS-2026-01-23.md +0 -936
  45. package/docs/CODEBASE-ANALYSIS-PRIORITIZED.md +0 -774
  46. package/docs/api-contracts.md +0 -216
  47. package/docs/auto-builder-analysis.md +0 -271
  48. package/docs/blog/mcp-tool-design-lessons.md +0 -309
  49. package/docs/data-architecture.md +0 -166
  50. package/docs/ema-auto-builder-guide.html +0 -394
  51. package/docs/lessons-learned.md +0 -209
  52. package/docs/llm-native-workflow-design.md +0 -252
  53. package/docs/local-generation.md +0 -508
  54. package/docs/mcp-flow-diagram.md +0 -135
  55. package/docs/migration/action-composition-migration.md +0 -270
  56. package/docs/naming-conventions.md +0 -278
  57. package/docs/proposals/HANDOFF-tool-restructure.md +0 -526
  58. package/docs/proposals/action-composition.md +0 -490
  59. package/docs/proposals/explicit-method-restructure.md +0 -328
  60. package/docs/proposals/mcp-tool-restructure-2026-01.md +0 -366
  61. package/docs/proposals/self-contained-guidance.md +0 -427
  62. package/docs/proto-sdk-generation.md +0 -242
  63. package/docs/release-impact.md +0 -102
  64. package/docs/release-process.md +0 -157
  65. package/docs/staging.RULE.md +0 -142
  66. package/docs/test-persona-creation.md +0 -196
  67. package/docs/tool-consolidation-v2.md +0 -225
  68. package/docs/tool-response-standards.md +0 -256
  69. package/resources/docs/getting-started.md +0 -97
  70. package/resources/templates/auto-builder-rules.md +0 -224
  71. package/resources/templates/chat-ai/README.md +0 -119
  72. package/resources/templates/chat-ai/persona-config.json +0 -111
  73. package/resources/templates/dashboard-ai/README.md +0 -156
  74. package/resources/templates/dashboard-ai/persona-config.json +0 -180
  75. package/resources/templates/demo-scenarios/README.md +0 -63
  76. package/resources/templates/demo-scenarios/test-published-package.md +0 -116
  77. package/resources/templates/document-gen-ai/README.md +0 -132
  78. package/resources/templates/document-gen-ai/persona-config.json +0 -316
  79. package/resources/templates/voice-ai/README.md +0 -123
  80. package/resources/templates/voice-ai/persona-config.json +0 -74
  81. 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-v2.ts
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-v2.ts to ensure description matches actual capabilities.
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 - Adapter providing legacy EmaClient interface over EmaClientV2
2
+ * EmaClientAdapter - Drop-in replacement for legacy EmaClient using EmaClientV2
3
3
  *
4
- * This adapter enables parity testing between the old hand-written EmaClient
5
- * and the new auto-generated EmaClientV2. It wraps EmaClientV2 and exposes
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
- * const adapter = new EmaClientAdapter({
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 adapter.getPersonasForTenant();
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 enables parity testing between the old and new client implementations.
26
+ * This is a drop-in replacement for EmaClient.
25
27
  */
26
28
  export class EmaClientAdapter {
27
29
  client;
28
30
  env;
29
- constructor(env) {
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 (delegated to EmaClientV2)
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
- // Template Operations (delegated to EmaClientV2)
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
- // Private Helper Methods
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), // legacy alias
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, // legacy alias
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
  /**
@@ -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
- // Deep clone workflow and fix the namespace
398
- const fixedWorkflow = JSON.parse(JSON.stringify(req.workflow));
399
+ // Copy namespace from existing workflow
399
400
  fixedWorkflow.workflowName = existingWfName;
400
- // Also fix results format if needed - API expects "<actionName>.<outputName>" keys
401
- const results = fixedWorkflow.results;
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
- finalReq = { ...req, workflow: fixedWorkflow };
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] Fixed workflow namespace:", existingWfName.name);
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) {
@@ -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;