@ema.co/mcp-toolkit 2026.2.19 → 2026.2.23
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/dist/cli/index.js +2 -2
- package/dist/mcp/domain/loop-detection.js +46 -54
- package/dist/mcp/domain/sanitizer.js +1 -1
- package/dist/mcp/domain/workflow-graph.js +2 -2
- package/dist/mcp/domain/workflow-path-enumerator.js +7 -4
- package/dist/mcp/guidance.js +53 -0
- package/dist/mcp/handlers/debug/adapter.js +15 -0
- package/dist/mcp/handlers/debug/formatters.js +282 -0
- package/dist/mcp/handlers/debug/index.js +133 -0
- package/dist/mcp/handlers/demo/adapter.js +180 -0
- package/dist/mcp/handlers/env/config.js +2 -2
- package/dist/mcp/handlers/index.js +0 -1
- package/dist/mcp/handlers/persona/adapter.js +135 -0
- package/dist/mcp/handlers/sync/adapter.js +200 -0
- package/dist/mcp/handlers/workflow/adapter.js +174 -0
- package/dist/mcp/handlers/workflow/fix.js +11 -12
- package/dist/mcp/handlers/workflow/index.js +0 -24
- package/dist/mcp/knowledge-guidance-topics.js +615 -0
- package/dist/mcp/knowledge.js +23 -612
- package/dist/mcp/resources-dynamic.js +2395 -0
- package/dist/mcp/resources-validation.js +408 -0
- package/dist/mcp/resources.js +72 -2724
- package/dist/mcp/server.js +33 -832
- package/dist/mcp/tools.js +104 -2
- package/dist/sdk/client-adapter.js +265 -24
- package/dist/sdk/ema-client.js +100 -9
- package/dist/sdk/generated/well-known-types.js +99 -0
- package/dist/sdk/grpc-client.js +115 -1
- package/dist/sync/sdk.js +2 -2
- package/dist/sync.js +4 -3
- package/package.json +3 -2
- package/dist/mcp/handlers/knowledge/index.js +0 -54
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Handler — Method Dispatch
|
|
3
|
+
*
|
|
4
|
+
* Routes debug tool methods to the appropriate SDK calls and formatters.
|
|
5
|
+
* Follows the drill-down pattern: conversations → conversation_detail → show_work → action_detail
|
|
6
|
+
*/
|
|
7
|
+
import { formatConversations, formatConversationDetail, formatShowWork, formatActionDetail, formatSearch, } from "./formatters.js";
|
|
8
|
+
export async function handleDebug(args, client) {
|
|
9
|
+
const method = args.method;
|
|
10
|
+
if (!method) {
|
|
11
|
+
return {
|
|
12
|
+
error: "Missing required parameter: method",
|
|
13
|
+
available_methods: ["conversations", "conversation_detail", "show_work", "action_detail", "search"],
|
|
14
|
+
_tip: "Start with debug(method='conversations', persona_id='...') to list audit conversations.",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
switch (method) {
|
|
18
|
+
case "conversations":
|
|
19
|
+
return handleConversations(args, client);
|
|
20
|
+
case "conversation_detail":
|
|
21
|
+
return handleConversationDetail(args, client);
|
|
22
|
+
case "show_work":
|
|
23
|
+
return handleShowWork(args, client);
|
|
24
|
+
case "action_detail":
|
|
25
|
+
return handleActionDetail(args, client);
|
|
26
|
+
case "search":
|
|
27
|
+
return handleSearch(args, client);
|
|
28
|
+
default:
|
|
29
|
+
return {
|
|
30
|
+
error: `Unknown method: ${method}`,
|
|
31
|
+
available_methods: ["conversations", "conversation_detail", "show_work", "action_detail", "search"],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Method handlers
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
async function handleConversations(args, client) {
|
|
39
|
+
const personaId = args.persona_id;
|
|
40
|
+
if (!personaId) {
|
|
41
|
+
return { error: "Missing required parameter: persona_id" };
|
|
42
|
+
}
|
|
43
|
+
// Build filters matching the gRPC ConversationReviewFilters proto shape
|
|
44
|
+
const filters = {};
|
|
45
|
+
if (args.start_time)
|
|
46
|
+
filters.createdFrom = new Date(String(args.start_time));
|
|
47
|
+
if (args.end_time)
|
|
48
|
+
filters.createdTo = new Date(String(args.end_time));
|
|
49
|
+
if (args.channel)
|
|
50
|
+
filters.channel = Number(args.channel);
|
|
51
|
+
if (args.status)
|
|
52
|
+
filters.status = Number(args.status);
|
|
53
|
+
if (args.search_query)
|
|
54
|
+
filters.searchQuery = String(args.search_query);
|
|
55
|
+
if (args.workflow_failure)
|
|
56
|
+
filters.workflowFailure = true;
|
|
57
|
+
if (args.has_user_frustration)
|
|
58
|
+
filters.hasUserFrustration = true;
|
|
59
|
+
if (args.no_results_found)
|
|
60
|
+
filters.noResultsFound = true;
|
|
61
|
+
const opts = {
|
|
62
|
+
...(Object.keys(filters).length > 0 ? { filters } : {}),
|
|
63
|
+
...(args.limit ? { limit: Number(args.limit) } : {}),
|
|
64
|
+
...(args.pagination_token ? { paginationToken: String(args.pagination_token) } : {}),
|
|
65
|
+
};
|
|
66
|
+
const response = await client.getConversationReviews(personaId, opts);
|
|
67
|
+
return formatConversations(response, personaId);
|
|
68
|
+
}
|
|
69
|
+
async function handleConversationDetail(args, client) {
|
|
70
|
+
const conversationId = args.conversation_id;
|
|
71
|
+
if (!conversationId) {
|
|
72
|
+
return { error: "Missing required parameter: conversation_id" };
|
|
73
|
+
}
|
|
74
|
+
const personaId = args.persona_id;
|
|
75
|
+
const response = await client.getConversationReviewDetail(conversationId);
|
|
76
|
+
return formatConversationDetail(response, conversationId, personaId);
|
|
77
|
+
}
|
|
78
|
+
async function handleShowWork(args, client) {
|
|
79
|
+
const workflowRunId = args.workflow_run_id;
|
|
80
|
+
const personaId = args.persona_id;
|
|
81
|
+
if (!workflowRunId) {
|
|
82
|
+
return { error: "Missing required parameter: workflow_run_id" };
|
|
83
|
+
}
|
|
84
|
+
if (!personaId) {
|
|
85
|
+
return {
|
|
86
|
+
error: "Missing required parameter: persona_id",
|
|
87
|
+
_tip: "DebugLogService requires persona context. Pass persona_id alongside workflow_run_id.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const response = await client.getWorkflowLevelDebugLog(workflowRunId, personaId);
|
|
91
|
+
return formatShowWork(response, workflowRunId, personaId);
|
|
92
|
+
}
|
|
93
|
+
async function handleActionDetail(args, client) {
|
|
94
|
+
const workflowRunId = args.workflow_run_id;
|
|
95
|
+
const actionName = args.action_name;
|
|
96
|
+
const personaId = args.persona_id;
|
|
97
|
+
if (!workflowRunId) {
|
|
98
|
+
return { error: "Missing required parameter: workflow_run_id" };
|
|
99
|
+
}
|
|
100
|
+
if (!actionName) {
|
|
101
|
+
return {
|
|
102
|
+
error: "Missing required parameter: action_name",
|
|
103
|
+
_tip: "Use debug(method='show_work') first to see available action names.",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (!personaId) {
|
|
107
|
+
return {
|
|
108
|
+
error: "Missing required parameter: persona_id",
|
|
109
|
+
_tip: "DebugLogService requires persona context. Pass persona_id alongside workflow_run_id and action_name.",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const response = await client.getActionLevelShowWorkLog(actionName, workflowRunId, personaId);
|
|
113
|
+
return formatActionDetail(response, workflowRunId, actionName);
|
|
114
|
+
}
|
|
115
|
+
async function handleSearch(args, client) {
|
|
116
|
+
const personaId = args.persona_id;
|
|
117
|
+
const query = args.query;
|
|
118
|
+
if (!personaId) {
|
|
119
|
+
return { error: "Missing required parameter: persona_id" };
|
|
120
|
+
}
|
|
121
|
+
if (!query) {
|
|
122
|
+
return { error: "Missing required parameter: query" };
|
|
123
|
+
}
|
|
124
|
+
const opts = { personaId, query };
|
|
125
|
+
if (args.conversation_id)
|
|
126
|
+
opts.conversationId = String(args.conversation_id);
|
|
127
|
+
if (args.start_time)
|
|
128
|
+
opts.startTime = new Date(String(args.start_time));
|
|
129
|
+
if (args.end_time)
|
|
130
|
+
opts.endTime = new Date(String(args.end_time));
|
|
131
|
+
const response = await client.searchMessages(opts);
|
|
132
|
+
return formatSearch(response, query, personaId);
|
|
133
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo V2 Adapter
|
|
3
|
+
*
|
|
4
|
+
* Routes the consolidated demo tool modes (kit, validate_kit, scenarios,
|
|
5
|
+
* consolidate, generate, validate, template) to the appropriate handlers.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from server.ts to keep the dispatch table thin.
|
|
8
|
+
*/
|
|
9
|
+
import { handleConsolidateDemoData, handleGenerateDemoDocument, handleValidateDemoDocument, handleGetDemoDataTemplate, } from "./index.js";
|
|
10
|
+
export async function handleDemoAdapter(args, createClient) {
|
|
11
|
+
const normalizedArgs = { ...(args ?? {}) };
|
|
12
|
+
const mode = normalizedArgs.mode ? String(normalizedArgs.mode) : "template";
|
|
13
|
+
const deprecationWarning = {
|
|
14
|
+
_deprecation: {
|
|
15
|
+
message: "The 'demo' tool is deprecated. Please use 'data' and 'persona' tools instead.",
|
|
16
|
+
migration: {
|
|
17
|
+
"demo(mode='kit')": "persona(from='demo-sales-sdr', include_data=true)",
|
|
18
|
+
"demo(mode='generate')": "data(mode='generate', from='customer', count=5)",
|
|
19
|
+
"demo(mode='scenarios')": "data(mode='templates')",
|
|
20
|
+
"demo(mode='template')": "data(mode='templates', template='customer')",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
switch (mode) {
|
|
25
|
+
case "kit": {
|
|
26
|
+
const personaId = String(normalizedArgs.persona_id ?? "");
|
|
27
|
+
const scenarioId = String(normalizedArgs.scenario ?? "sales-sdr");
|
|
28
|
+
if (!personaId) {
|
|
29
|
+
throw new Error('demo(mode="kit") requires: persona_id');
|
|
30
|
+
}
|
|
31
|
+
const { generateDemoKit, DEMO_SCENARIOS, generateDemoScriptMarkdown, validateDemoKit } = await import("../../demo-generator.js");
|
|
32
|
+
const scenario = DEMO_SCENARIOS[scenarioId];
|
|
33
|
+
if (!scenario) {
|
|
34
|
+
throw new Error(`Unknown scenario: ${scenarioId}. Available: ${Object.keys(DEMO_SCENARIOS).join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
const client = await createClient(normalizedArgs.env);
|
|
37
|
+
const persona = await client.getPersonaById(personaId);
|
|
38
|
+
if (!persona) {
|
|
39
|
+
throw new Error(`Persona not found: ${personaId}`);
|
|
40
|
+
}
|
|
41
|
+
const workflowDef = persona.workflow_def || {};
|
|
42
|
+
const customQA = normalizedArgs.custom_qa;
|
|
43
|
+
const kit = generateDemoKit(personaId, persona.name || personaId, workflowDef, scenario, customQA);
|
|
44
|
+
const demoScript = generateDemoScriptMarkdown(kit);
|
|
45
|
+
const validation = validateDemoKit(kit);
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
persona_id: personaId,
|
|
49
|
+
persona_name: persona.name,
|
|
50
|
+
scenario: scenarioId,
|
|
51
|
+
kit_summary: {
|
|
52
|
+
kb_documents: kit.kb_documents.length,
|
|
53
|
+
demo_questions: kit.demo_script.length,
|
|
54
|
+
fixed_responses: kit.fixed_responses.length,
|
|
55
|
+
validation_queries: kit.validation_queries.length,
|
|
56
|
+
},
|
|
57
|
+
validation,
|
|
58
|
+
demo_script_preview: demoScript.slice(0, 2000) + (demoScript.length > 2000 ? "\n\n... (truncated)" : ""),
|
|
59
|
+
kit,
|
|
60
|
+
instructions: [
|
|
61
|
+
"1. Upload KB documents to the persona's knowledge base",
|
|
62
|
+
"2. Review the demo script and practice the questions",
|
|
63
|
+
"3. Optionally apply fixed_responses for guaranteed fallbacks",
|
|
64
|
+
"4. Run validation queries to verify demo readiness",
|
|
65
|
+
"5. Conduct the demo with confidence!",
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
case "validate_kit": {
|
|
70
|
+
const personaId = String(normalizedArgs.persona_id ?? "");
|
|
71
|
+
if (!personaId) {
|
|
72
|
+
throw new Error('demo(mode="validate_kit") requires: persona_id');
|
|
73
|
+
}
|
|
74
|
+
const { analyzeWorkflowForDemo, DEMO_SCENARIOS } = await import("../../demo-generator.js");
|
|
75
|
+
const client = await createClient(normalizedArgs.env);
|
|
76
|
+
const persona = await client.getPersonaById(personaId);
|
|
77
|
+
if (!persona) {
|
|
78
|
+
throw new Error(`Persona not found: ${personaId}`);
|
|
79
|
+
}
|
|
80
|
+
const analysis = analyzeWorkflowForDemo(persona.workflow_def || {});
|
|
81
|
+
const dataSourcesResult = await client.listDataSourceFiles(personaId);
|
|
82
|
+
const dataSources = dataSourcesResult.files || [];
|
|
83
|
+
const hasKnowledgeBase = dataSources.length > 0;
|
|
84
|
+
const issues = [];
|
|
85
|
+
if (!hasKnowledgeBase) {
|
|
86
|
+
issues.push("No knowledge base documents uploaded - RAG search will fail");
|
|
87
|
+
}
|
|
88
|
+
if (analysis.intents.length === 0) {
|
|
89
|
+
issues.push("No categorizer intents detected - workflow may not route correctly");
|
|
90
|
+
}
|
|
91
|
+
if (!analysis.has_search) {
|
|
92
|
+
issues.push("No search nodes detected - cannot retrieve KB data");
|
|
93
|
+
}
|
|
94
|
+
let suggestedScenario = "sales-sdr";
|
|
95
|
+
for (const [id, scenario] of Object.entries(DEMO_SCENARIOS)) {
|
|
96
|
+
const intentOverlap = scenario.intents.filter(i => analysis.intents.some(ai => ai.toLowerCase().includes(i.name.toLowerCase()))).length;
|
|
97
|
+
if (intentOverlap > 0) {
|
|
98
|
+
suggestedScenario = id;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
persona_id: personaId,
|
|
104
|
+
persona_name: persona.name,
|
|
105
|
+
ready: issues.length === 0,
|
|
106
|
+
issues,
|
|
107
|
+
workflow_analysis: analysis,
|
|
108
|
+
knowledge_base: {
|
|
109
|
+
has_documents: hasKnowledgeBase,
|
|
110
|
+
document_count: dataSources.length,
|
|
111
|
+
},
|
|
112
|
+
suggested_scenario: suggestedScenario,
|
|
113
|
+
next_steps: issues.length > 0
|
|
114
|
+
? issues.map((issue, i) => `${i + 1}. Fix: ${issue}`)
|
|
115
|
+
: [`Generate demo kit: demo(mode="kit", persona_id="${personaId}", scenario="${suggestedScenario}")`],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
case "scenarios": {
|
|
119
|
+
const { DEMO_SCENARIOS } = await import("../../demo-generator.js");
|
|
120
|
+
return {
|
|
121
|
+
scenarios: Object.entries(DEMO_SCENARIOS).map(([id, scenario]) => ({
|
|
122
|
+
id,
|
|
123
|
+
name: scenario.name,
|
|
124
|
+
description: scenario.description,
|
|
125
|
+
persona_types: scenario.persona_types,
|
|
126
|
+
tags: scenario.tags,
|
|
127
|
+
intent_count: scenario.intents.length,
|
|
128
|
+
qa_count: scenario.qa_pairs.length,
|
|
129
|
+
entity_types: scenario.entities.map(e => e.type),
|
|
130
|
+
})),
|
|
131
|
+
usage: 'demo(mode="kit", persona_id="...", scenario="<scenario_id>")',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case "consolidate": {
|
|
135
|
+
const source = String(normalizedArgs.source ?? "");
|
|
136
|
+
const output = String(normalizedArgs.output ?? "");
|
|
137
|
+
const entity = String(normalizedArgs.entity ?? "");
|
|
138
|
+
if (!source || !output || !entity) {
|
|
139
|
+
throw new Error('demo(mode="consolidate") requires: source, output, entity');
|
|
140
|
+
}
|
|
141
|
+
return handleConsolidateDemoData({
|
|
142
|
+
source_dir: source,
|
|
143
|
+
output_dir: output,
|
|
144
|
+
entity_type: entity,
|
|
145
|
+
primary_file: normalizedArgs.primary ?? `${entity}s.json`,
|
|
146
|
+
joins: normalizedArgs.joins ?? [],
|
|
147
|
+
tags: normalizedArgs.tags,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
case "generate": {
|
|
151
|
+
const entity = String(normalizedArgs.entity ?? "");
|
|
152
|
+
if (!entity)
|
|
153
|
+
throw new Error('demo(mode="generate") requires: entity');
|
|
154
|
+
return handleGenerateDemoDocument({
|
|
155
|
+
entity_type: entity,
|
|
156
|
+
data: normalizedArgs.data ?? {},
|
|
157
|
+
related_data: normalizedArgs.related ?? {},
|
|
158
|
+
output_path: normalizedArgs.output,
|
|
159
|
+
tags: normalizedArgs.tags,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
case "validate": {
|
|
163
|
+
return handleValidateDemoDocument({
|
|
164
|
+
file_path: normalizedArgs.file,
|
|
165
|
+
content: normalizedArgs.content,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
case "template": {
|
|
169
|
+
const entity = String(normalizedArgs.entity ?? "");
|
|
170
|
+
if (!entity)
|
|
171
|
+
throw new Error('demo(mode="template") requires: entity');
|
|
172
|
+
return handleGetDemoDataTemplate({
|
|
173
|
+
entity_type: entity,
|
|
174
|
+
include_example: normalizedArgs.include_example,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
throw new Error(`Unknown demo mode: ${mode}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -9,7 +9,7 @@ import { readFileSync } from "node:fs";
|
|
|
9
9
|
import { execSync } from "node:child_process";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { EmaClientAdapter } from "../../../sdk/client-adapter.js";
|
|
13
13
|
import { loadConfigOptional } from "../../../sdk/config.js";
|
|
14
14
|
import { SyncSDK } from "../../../sync/sdk.js";
|
|
15
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -282,7 +282,7 @@ export function createClient(envName) {
|
|
|
282
282
|
},
|
|
283
283
|
}
|
|
284
284
|
: undefined;
|
|
285
|
-
return new
|
|
285
|
+
return new EmaClientAdapter({
|
|
286
286
|
name: envInfo.name,
|
|
287
287
|
baseUrl: envInfo.baseUrl,
|
|
288
288
|
bearerToken,
|
|
@@ -27,6 +27,5 @@ export { handleEnv } from "./env/index.js";
|
|
|
27
27
|
export { handleTemplate } from "./template/index.js";
|
|
28
28
|
export { handleAction } from "./action/index.js";
|
|
29
29
|
export { handleReference } from "./reference/index.js";
|
|
30
|
-
export { handleKnowledge } from "./knowledge/index.js";
|
|
31
30
|
export { handleSync } from "./sync/index.js";
|
|
32
31
|
// Note: handleDashboardGenerate removed - LLM generates content, MCP uploads via data.method="upload"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona V2 Adapter
|
|
3
|
+
*
|
|
4
|
+
* Converts the V2 method-based persona tool args into the internal
|
|
5
|
+
* mode-based format expected by handlePersona, routes data sub-resource
|
|
6
|
+
* operations directly to handleData, and executes action composition
|
|
7
|
+
* post-processing.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from server.ts to keep the dispatch table thin.
|
|
10
|
+
*/
|
|
11
|
+
import { handlePersona } from "./index.js";
|
|
12
|
+
export async function handlePersonaAdapter(args, createClient, getDefaultEnvName) {
|
|
13
|
+
const targetEnv = args.env ?? getDefaultEnvName();
|
|
14
|
+
const client = createClient(targetEnv);
|
|
15
|
+
const versionContext = {
|
|
16
|
+
workspaceRoot: process.cwd(),
|
|
17
|
+
environment: targetEnv,
|
|
18
|
+
tenant_id: targetEnv,
|
|
19
|
+
};
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Debug Sub-Resource (intercepted before data)
|
|
22
|
+
//
|
|
23
|
+
// persona(id="abc", debug={method:"conversations"}) routes to handleDebug.
|
|
24
|
+
// Injects persona_id for methods that need it (conversations, search).
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
26
|
+
if (args.debug && typeof args.debug === "object") {
|
|
27
|
+
const debug = args.debug;
|
|
28
|
+
const personaId = args.id;
|
|
29
|
+
if (typeof debug.method === "string") {
|
|
30
|
+
const { handleDebugAdapter } = await import("../debug/adapter.js");
|
|
31
|
+
return handleDebugAdapter({ ...debug, persona_id: debug.persona_id ?? personaId, env: args.env }, createClient, getDefaultEnvName);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
error: "Missing debug.method",
|
|
35
|
+
hint: "Use debug={method:'conversations'} format",
|
|
36
|
+
available_methods: ["conversations", "conversation_detail", "show_work", "action_detail", "search"],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Data Sub-Resource (intercepted after debug — bypasses persona routing entirely)
|
|
41
|
+
//
|
|
42
|
+
// persona(id="abc", data={method:"list"}) routes directly to handleData.
|
|
43
|
+
// This avoids data operations flowing through handlePersona's routing.
|
|
44
|
+
// Direct callers of handlePersona (e.g. tests) still use its own data routing.
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
46
|
+
if (args.data && typeof args.data === "object") {
|
|
47
|
+
const data = args.data;
|
|
48
|
+
const personaId = args.id;
|
|
49
|
+
if (typeof data.method === "string") {
|
|
50
|
+
const { handleData: handleDataDirect } = await import("../data/index.js");
|
|
51
|
+
return handleDataDirect({ persona_id: personaId, env: args.env, data }, client);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
error: "Missing data.method",
|
|
55
|
+
hint: "Use data={method:'list'} format",
|
|
56
|
+
available_methods: ["list", "stats", "upload", "copy", "replicate", "delete", "search", "refresh", "regenerate", "replace"],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
60
|
+
// V2 Parameter Transformation
|
|
61
|
+
// Convert v2 structure to v1 mode-based structure for handler compatibility
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
63
|
+
const transformedArgs = { ...args };
|
|
64
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
65
|
+
// Method → Mode mapping
|
|
66
|
+
// persona(method="create|get|list|update|delete|sanitize|snapshot|history|restore")
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
68
|
+
if (args.method) {
|
|
69
|
+
const method = String(args.method);
|
|
70
|
+
if (method === "analyze" || method === "compare") {
|
|
71
|
+
return {
|
|
72
|
+
error: `method="${method}" is removed. Use persona(method="get", id="...") and analyze/compare the results yourself.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const methodToMode = {
|
|
76
|
+
create: "create",
|
|
77
|
+
get: "get",
|
|
78
|
+
list: "list",
|
|
79
|
+
update: "update",
|
|
80
|
+
delete: "delete",
|
|
81
|
+
sanitize: "sanitize",
|
|
82
|
+
schema: "schema",
|
|
83
|
+
clone: "create",
|
|
84
|
+
snapshot: "version_create",
|
|
85
|
+
history: "version_list",
|
|
86
|
+
restore: "version_restore",
|
|
87
|
+
};
|
|
88
|
+
const mode = methodToMode[method];
|
|
89
|
+
if (!mode) {
|
|
90
|
+
return { error: `Unknown method: ${method}`, valid_methods: Object.keys(methodToMode) };
|
|
91
|
+
}
|
|
92
|
+
transformedArgs.mode = mode;
|
|
93
|
+
delete transformedArgs.method;
|
|
94
|
+
if (args.actions && Array.isArray(args.actions)) {
|
|
95
|
+
transformedArgs._actions = args.actions;
|
|
96
|
+
delete transformedArgs.actions;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (args.id) {
|
|
100
|
+
transformedArgs.mode = "get";
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
transformedArgs.mode = "list";
|
|
104
|
+
}
|
|
105
|
+
const result = await handlePersona(transformedArgs, client, () => undefined, (env) => createClient(env), versionContext);
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// Action Composition Post-Processing
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
109
|
+
const actions = transformedArgs._actions;
|
|
110
|
+
if (actions && actions.length > 0) {
|
|
111
|
+
const resultObj = result;
|
|
112
|
+
const targetId = resultObj.id ??
|
|
113
|
+
resultObj.persona_id;
|
|
114
|
+
const sourceId = args.from;
|
|
115
|
+
if (!targetId) {
|
|
116
|
+
return {
|
|
117
|
+
...(typeof result === "object" && result !== null ? result : {}),
|
|
118
|
+
_actions_error: "No persona ID available for action execution",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const actionExecutor = await import("../action-executor.js");
|
|
122
|
+
const context = {
|
|
123
|
+
source: sourceId,
|
|
124
|
+
target: targetId,
|
|
125
|
+
env: targetEnv,
|
|
126
|
+
originalArgs: args,
|
|
127
|
+
};
|
|
128
|
+
const actionsResult = await actionExecutor.executeActions(actions, context, client);
|
|
129
|
+
return {
|
|
130
|
+
...(typeof result === "object" && result !== null ? result : {}),
|
|
131
|
+
_actions: actionsResult,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync V2 Adapter
|
|
3
|
+
*
|
|
4
|
+
* Contains syncRunImpl / syncInfoImpl implementation functions and the
|
|
5
|
+
* top-level sync tool handler that routes method ↔ mode and normalises arg
|
|
6
|
+
* names between the V2 tool schema and the internal sync implementation.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from server.ts to keep the dispatch table thin.
|
|
9
|
+
*/
|
|
10
|
+
import { resolveSyncBehavior, loadSyncOptions } from "../../../sync/sync-options.js";
|
|
11
|
+
import { directSyncPersona, directSyncPersonaById, directSyncAll } from "./direct.js";
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// Implementation helpers
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
async function syncRunImpl(args, getDefaultEnvName, getSyncSDK) {
|
|
16
|
+
const targetEnv = String(args.target_env);
|
|
17
|
+
const sourceEnv = args.source_env ? String(args.source_env) : getDefaultEnvName();
|
|
18
|
+
const dryRun = args.dry_run === true;
|
|
19
|
+
const includeStatus = args.include_status === true;
|
|
20
|
+
const scope = args.scope === "all" ? "all" : "one";
|
|
21
|
+
const identifier = args.identifier ? String(args.identifier) : undefined;
|
|
22
|
+
if (scope === "all" || !identifier) {
|
|
23
|
+
const sdk = getSyncSDK();
|
|
24
|
+
if (sdk) {
|
|
25
|
+
try {
|
|
26
|
+
const result = await sdk.runSync();
|
|
27
|
+
return { success: true, mode: "config", ...result };
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
sdk.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const result = await directSyncAll({ targetEnv, dryRun });
|
|
35
|
+
return { success: true, mode: "tags", ...result };
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
|
42
|
+
const behavior = resolveSyncBehavior({
|
|
43
|
+
personaName: isUUID ? undefined : identifier,
|
|
44
|
+
targetEnv,
|
|
45
|
+
overrides: {
|
|
46
|
+
dry_run: dryRun ? true : undefined,
|
|
47
|
+
sync_status: includeStatus ? true : undefined,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const result = isUUID
|
|
52
|
+
? await directSyncPersonaById({
|
|
53
|
+
personaId: identifier,
|
|
54
|
+
sourceEnv,
|
|
55
|
+
targetEnv,
|
|
56
|
+
dryRun: behavior.dry_run,
|
|
57
|
+
syncStatus: behavior.sync_status,
|
|
58
|
+
})
|
|
59
|
+
: await directSyncPersona({
|
|
60
|
+
name: identifier,
|
|
61
|
+
sourceEnv,
|
|
62
|
+
targetEnv,
|
|
63
|
+
dryRun: behavior.dry_run,
|
|
64
|
+
syncStatus: behavior.sync_status,
|
|
65
|
+
});
|
|
66
|
+
return { ...result, resolved_behavior: behavior };
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function syncInfoImpl(args, createClient, getSyncSDK) {
|
|
73
|
+
const client = args.env ? createClient(args.env) : undefined;
|
|
74
|
+
if (args.persona_id) {
|
|
75
|
+
if (!client)
|
|
76
|
+
throw new Error("env required when checking persona sync status");
|
|
77
|
+
const personaId = String(args.persona_id);
|
|
78
|
+
const personas = await client.getPersonasForTenant();
|
|
79
|
+
const persona = personas.find((p) => p.id === personaId);
|
|
80
|
+
if (!persona)
|
|
81
|
+
throw new Error(`AI Employee not found: ${personaId}`);
|
|
82
|
+
const meta = client.getSyncMetadata(persona);
|
|
83
|
+
return {
|
|
84
|
+
environment: client["env"].name,
|
|
85
|
+
persona_id: personaId,
|
|
86
|
+
persona_name: persona.name,
|
|
87
|
+
is_synced: !!meta,
|
|
88
|
+
sync_metadata: meta,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (args.persona_name) {
|
|
92
|
+
const sdk = getSyncSDK();
|
|
93
|
+
if (!sdk)
|
|
94
|
+
return { error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG." };
|
|
95
|
+
try {
|
|
96
|
+
const persona = await sdk.getMasterPersonaByName(String(args.persona_name));
|
|
97
|
+
if (!persona)
|
|
98
|
+
return { error: `Persona not found: ${args.persona_name}` };
|
|
99
|
+
return await sdk.getPersonaSyncStatus(persona.id);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
sdk.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (args.list_synced === true) {
|
|
106
|
+
if (!client)
|
|
107
|
+
throw new Error("env required when listing synced personas");
|
|
108
|
+
const personas = await client.getPersonasForTenant();
|
|
109
|
+
const masterEnvFilter = args.master_env ? String(args.master_env).toLowerCase() : undefined;
|
|
110
|
+
const synced = [];
|
|
111
|
+
for (const p of personas) {
|
|
112
|
+
const meta = client.getSyncMetadata(p);
|
|
113
|
+
if (meta) {
|
|
114
|
+
if (masterEnvFilter && meta.master_env.toLowerCase() !== masterEnvFilter)
|
|
115
|
+
continue;
|
|
116
|
+
synced.push({ persona_id: p.id, persona_name: p.name, sync_metadata: meta });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { environment: client["env"].name, count: synced.length, synced_personas: synced };
|
|
120
|
+
}
|
|
121
|
+
const sdk = getSyncSDK();
|
|
122
|
+
const options = args.include_options === true ? loadSyncOptions() : undefined;
|
|
123
|
+
if (!sdk) {
|
|
124
|
+
return {
|
|
125
|
+
configured: false,
|
|
126
|
+
error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG.",
|
|
127
|
+
options,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const master = sdk.getMasterEnvironment();
|
|
132
|
+
const envs = sdk.getEnvironments();
|
|
133
|
+
const personas = await sdk.listMasterPersonas();
|
|
134
|
+
return {
|
|
135
|
+
configured: true,
|
|
136
|
+
master_environment: { name: master.name, url: master.baseUrl },
|
|
137
|
+
target_environments: envs.filter((e) => !e.isMaster).map((e) => ({ name: e.name, url: e.baseUrl })),
|
|
138
|
+
total_personas: personas.length,
|
|
139
|
+
options,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
sdk.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
// Top-level sync tool handler
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
export async function handleSyncAdapter(args, createClient, getDefaultEnvName, getSyncSDK) {
|
|
150
|
+
const normalizedArgs = { ...(args ?? {}) };
|
|
151
|
+
const rawMethod = normalizedArgs.method ? String(normalizedArgs.method) : normalizedArgs.mode ? String(normalizedArgs.mode) : "run";
|
|
152
|
+
const mode = rawMethod === "preview" ? "run" : rawMethod === "execute" ? "run" : rawMethod;
|
|
153
|
+
if (rawMethod === "preview") {
|
|
154
|
+
normalizedArgs.dry_run = true;
|
|
155
|
+
}
|
|
156
|
+
const target = (normalizedArgs.target ?? normalizedArgs.target_env);
|
|
157
|
+
const source = (normalizedArgs.source ?? normalizedArgs.source_env);
|
|
158
|
+
const id = normalizedArgs.id;
|
|
159
|
+
const identifier = normalizedArgs.identifier;
|
|
160
|
+
const idOrIdentifier = id ?? identifier;
|
|
161
|
+
if (mode === "config") {
|
|
162
|
+
return syncInfoImpl({ include_options: true }, createClient, getSyncSDK);
|
|
163
|
+
}
|
|
164
|
+
if (mode === "status") {
|
|
165
|
+
const env = normalizedArgs.env;
|
|
166
|
+
if (normalizedArgs.list_synced === true) {
|
|
167
|
+
if (!env)
|
|
168
|
+
throw new Error('env is required for sync(mode="status", list_synced=true)');
|
|
169
|
+
return syncInfoImpl({ list_synced: true, master_env: normalizedArgs.master_env, env }, createClient, getSyncSDK);
|
|
170
|
+
}
|
|
171
|
+
if (idOrIdentifier) {
|
|
172
|
+
if (!env)
|
|
173
|
+
throw new Error('env is required for sync(mode="status", id="...")');
|
|
174
|
+
const identifierToResolve = String(idOrIdentifier);
|
|
175
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifierToResolve);
|
|
176
|
+
if (isUUID) {
|
|
177
|
+
return syncInfoImpl({ persona_id: identifierToResolve, env }, createClient, getSyncSDK);
|
|
178
|
+
}
|
|
179
|
+
const client = createClient(env);
|
|
180
|
+
const personas = await client.getPersonasForTenant();
|
|
181
|
+
const match = personas.find((p) => p.name === identifierToResolve);
|
|
182
|
+
if (!match)
|
|
183
|
+
throw new Error(`AI Employee not found by name in ${env}: ${identifierToResolve}`);
|
|
184
|
+
return syncInfoImpl({ persona_id: match.id, env }, createClient, getSyncSDK);
|
|
185
|
+
}
|
|
186
|
+
return syncInfoImpl({ include_options: normalizedArgs.include_options === true }, createClient, getSyncSDK);
|
|
187
|
+
}
|
|
188
|
+
// mode === "run" (default)
|
|
189
|
+
if (!target) {
|
|
190
|
+
throw new Error('target (or target_env) is required for sync(mode="run")');
|
|
191
|
+
}
|
|
192
|
+
return syncRunImpl({
|
|
193
|
+
identifier: idOrIdentifier,
|
|
194
|
+
target_env: target,
|
|
195
|
+
source_env: source,
|
|
196
|
+
scope: normalizedArgs.scope,
|
|
197
|
+
dry_run: normalizedArgs.dry_run,
|
|
198
|
+
include_status: normalizedArgs.include_status,
|
|
199
|
+
}, getDefaultEnvName, getSyncSDK);
|
|
200
|
+
}
|