@ema.co/mcp-toolkit 2026.1.21 → 2026.1.24
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 +33 -9
- package/dist/mcp/handlers/action/index.js +148 -0
- package/dist/mcp/handlers/action-executor.js +350 -0
- package/dist/mcp/handlers/data/dashboard-clone.js +352 -0
- package/dist/mcp/handlers/data/index.js +381 -0
- package/dist/mcp/handlers/env/index.js +44 -0
- package/dist/mcp/handlers/index.js +23 -2
- package/dist/mcp/handlers/knowledge/index.js +247 -0
- package/dist/mcp/handlers/persona/analyze.js +18 -0
- package/dist/mcp/handlers/persona/create.js +94 -8
- package/dist/mcp/handlers/persona/get.js +3 -2
- package/dist/mcp/handlers/persona/sanitize.js +12 -2
- package/dist/mcp/handlers/persona/update.js +17 -7
- package/dist/mcp/handlers/reference/index.js +291 -0
- package/dist/mcp/handlers/sync/index.js +129 -0
- package/dist/mcp/handlers/template/index.js +61 -0
- package/dist/mcp/handlers/utils.js +15 -6
- package/dist/mcp/handlers-consolidated.js +379 -1081
- package/dist/mcp/prompts.js +6 -3
- package/dist/mcp/resources.js +55 -6
- package/dist/mcp/server.js +198 -90
- package/dist/mcp/tools-consolidated.js +5 -2
- package/dist/mcp/tools-v2.js +335 -153
- package/dist/mcp/workflow-operations.js +100 -0
- package/dist/sdk/generation-schema.js +5 -5
- package/dist/sdk/guidance.js +560 -0
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/knowledge.js +145 -135
- package/dist/sdk/sanitizer.js +174 -2
- package/dist/sdk/structural-rules.js +1 -1
- package/dist/sdk/validation-rules.js +1 -1
- package/dist/sdk/workflow-generator.js +29 -3
- package/dist/sdk/workflow-transformer.js +47 -2
- package/docs/CODEBASE-ANALYSIS-2026-01-23.md +936 -0
- package/docs/CODEBASE-ANALYSIS-PRIORITIZED.md +774 -0
- package/docs/dashboard-operations.md +246 -0
- package/docs/lessons-learned.md +30 -0
- package/docs/mcp-tools-guide.md +68 -6
- package/docs/migration/action-composition-migration.md +270 -0
- package/docs/naming-conventions.md +85 -25
- package/docs/proposals/action-composition.md +490 -0
- package/docs/proposals/explicit-method-restructure.md +328 -0
- package/docs/proposals/self-contained-guidance.md +427 -0
- package/docs/release-impact.md +102 -0
- package/package.json +7 -2
- package/resources/templates/auto-builder-rules.md +8 -6
- package/resources/templates/demo-scenarios/test-published-package.md +2 -2
- package/resources/templates/voice-ai/workflow-prompt.md +11 -10
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ MCP (Model Context Protocol) server for managing Ema AI Employees. Works with Cu
|
|
|
7
7
|
### Option 1: npx (Recommended)
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx @ema.co/mcp-toolkit
|
|
10
|
+
npx -y @ema.co/mcp-toolkit@latest
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
### Option 2: Global Install
|
|
@@ -63,7 +63,7 @@ Then reload: `source ~/.zshrc`
|
|
|
63
63
|
"mcpServers": {
|
|
64
64
|
"ema": {
|
|
65
65
|
"command": "npx",
|
|
66
|
-
"args": ["@ema.co/mcp-toolkit"],
|
|
66
|
+
"args": ["-y", "@ema.co/mcp-toolkit@latest"],
|
|
67
67
|
"env": {
|
|
68
68
|
"EMA_ENV_NAME": "demo",
|
|
69
69
|
"EMA_PROD_BEARER_TOKEN": "${env:EMA_PROD_BEARER_TOKEN}",
|
|
@@ -77,6 +77,8 @@ Then reload: `source ~/.zshrc`
|
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
> **Important**:
|
|
80
|
+
> - Use `@latest` to always get the newest version (npx caches aggressively without it)
|
|
81
|
+
> - The `-y` flag auto-confirms the npx install prompt
|
|
80
82
|
> - Use `${env:VAR_NAME}` syntax to reference shell environment variables
|
|
81
83
|
> - After changing mcp.json, restart the MCP server: `Cmd+Shift+P` → "MCP: Restart Server"
|
|
82
84
|
> - After changing ~/.zshrc, reload it AND restart Cursor for changes to take effect
|
|
@@ -88,7 +90,7 @@ Then reload: `source ~/.zshrc`
|
|
|
88
90
|
"mcpServers": {
|
|
89
91
|
"ema": {
|
|
90
92
|
"command": "npx",
|
|
91
|
-
"args": ["@ema.co/mcp-toolkit"],
|
|
93
|
+
"args": ["-y", "@ema.co/mcp-toolkit@latest"],
|
|
92
94
|
"env": {
|
|
93
95
|
"EMA_ENV_NAME": "demo",
|
|
94
96
|
"EMA_DEMO_BEARER_TOKEN": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
@@ -238,13 +240,35 @@ reference(type="patterns", pattern="intent-routing") // Get pattern
|
|
|
238
240
|
|
|
239
241
|
## Dynamic Resources
|
|
240
242
|
|
|
241
|
-
| Resource |
|
|
242
|
-
|
|
243
|
-
| `ema://catalog/agents` | Live from API |
|
|
244
|
-
| `ema://catalog/templates` |
|
|
243
|
+
| Resource | Purpose |
|
|
244
|
+
|----------|---------|
|
|
245
|
+
| `ema://catalog/agents` | Live action catalog from API |
|
|
246
|
+
| `ema://catalog/templates` | Persona templates from API |
|
|
245
247
|
| `ema://catalog/patterns` | Workflow patterns |
|
|
246
|
-
| `ema://
|
|
247
|
-
| `ema://rules
|
|
248
|
+
| `ema://docs/usage-guide` | Complete usage guide (generated) |
|
|
249
|
+
| `ema://guidance/rules` | Structured rules as JSON |
|
|
250
|
+
| `ema://guidance/cursor-rule` | Export as Cursor .mdc rule |
|
|
251
|
+
| `ema://guidance/server-instructions` | Server instructions text |
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Self-Contained Guidance
|
|
256
|
+
|
|
257
|
+
The MCP is fully self-contained—no external configuration files needed.
|
|
258
|
+
|
|
259
|
+
**How it works:**
|
|
260
|
+
- Server instructions are injected on MCP init (system prompt)
|
|
261
|
+
- Tools include usage tips in their descriptions
|
|
262
|
+
- Responses include `_tip` and `_next_step` fields with contextual guidance
|
|
263
|
+
- `env()` returns a getting_started guide with workflow patterns
|
|
264
|
+
- `persona(..., analyze=true)` includes workflow_guidance with state-specific tips
|
|
265
|
+
|
|
266
|
+
**For other services:**
|
|
267
|
+
- Read `ema://guidance/rules` (JSON) for programmatic consumption
|
|
268
|
+
- Read `ema://docs/usage-guide` (markdown) for documentation
|
|
269
|
+
- Read `ema://guidance/cursor-rule` (.mdc) for IDE integration
|
|
270
|
+
|
|
271
|
+
All guidance flows from a single source (`src/sdk/guidance.ts`).
|
|
248
272
|
|
|
249
273
|
---
|
|
250
274
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Handler
|
|
3
|
+
*
|
|
4
|
+
* Provides action/agent lookup, filtering, and documentation.
|
|
5
|
+
*/
|
|
6
|
+
import { AGENT_CATALOG, getAgentByName, suggestAgentsForUseCase, } from "../../../sdk/knowledge.js";
|
|
7
|
+
// Deprecated param mappings for backwards compatibility
|
|
8
|
+
const DEPRECATED_PARAMS = {
|
|
9
|
+
identifier: { newName: "id", message: "'identifier' is deprecated, use 'id' instead (will be removed in v2.0.0)" },
|
|
10
|
+
};
|
|
11
|
+
function checkDeprecatedParams(args) {
|
|
12
|
+
const warnings = [];
|
|
13
|
+
for (const [oldName, info] of Object.entries(DEPRECATED_PARAMS)) {
|
|
14
|
+
if (args[oldName] !== undefined) {
|
|
15
|
+
warnings.push(info.message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return warnings;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Handle action tool requests - list, get, filter, and search actions
|
|
22
|
+
*/
|
|
23
|
+
export async function handleAction(args, client) {
|
|
24
|
+
// Check for deprecated params and log warnings
|
|
25
|
+
const deprecationWarnings = checkDeprecatedParams(args);
|
|
26
|
+
for (const warning of deprecationWarnings) {
|
|
27
|
+
console.warn(`[action] Deprecation: ${warning}`);
|
|
28
|
+
}
|
|
29
|
+
const id = args.id;
|
|
30
|
+
const identifier = args.identifier; // deprecated alias for 'id'
|
|
31
|
+
const idOrName = id ?? identifier;
|
|
32
|
+
// Categories list
|
|
33
|
+
if (args.categories) {
|
|
34
|
+
const categories = [...new Set(Object.values(AGENT_CATALOG).map(a => a.category))];
|
|
35
|
+
return { categories, count: categories.length };
|
|
36
|
+
}
|
|
37
|
+
// Suggest for use case
|
|
38
|
+
if (args.suggest) {
|
|
39
|
+
const suggestions = suggestAgentsForUseCase(args.suggest);
|
|
40
|
+
return { suggestions, use_case: args.suggest };
|
|
41
|
+
}
|
|
42
|
+
// Get single action
|
|
43
|
+
if (idOrName) {
|
|
44
|
+
// Try API first
|
|
45
|
+
try {
|
|
46
|
+
const actions = await client.listActions();
|
|
47
|
+
const action = actions.find(a => a.id === idOrName ||
|
|
48
|
+
a.name === idOrName ||
|
|
49
|
+
a.name?.toLowerCase() === idOrName.toLowerCase());
|
|
50
|
+
const result = action ? {
|
|
51
|
+
id: action.id,
|
|
52
|
+
name: action.name,
|
|
53
|
+
category: action.category,
|
|
54
|
+
enabled: action.enabled,
|
|
55
|
+
inputs: action.inputs,
|
|
56
|
+
outputs: action.outputs,
|
|
57
|
+
source: "api",
|
|
58
|
+
} : {};
|
|
59
|
+
// Include docs if requested or if not found in API
|
|
60
|
+
if (args.include_docs || !action) {
|
|
61
|
+
const doc = getAgentByName(idOrName);
|
|
62
|
+
if (doc) {
|
|
63
|
+
result.documentation = doc;
|
|
64
|
+
result.source = action ? "api+docs" : "docs";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (Object.keys(result).length === 0) {
|
|
68
|
+
return { error: `Action not found: ${idOrName}` };
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Fallback to docs only
|
|
74
|
+
const doc = getAgentByName(idOrName);
|
|
75
|
+
if (doc) {
|
|
76
|
+
return { ...doc, source: "docs" };
|
|
77
|
+
}
|
|
78
|
+
return { error: `Action not found: ${idOrName}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// List actions
|
|
82
|
+
try {
|
|
83
|
+
let actions = await client.listActions();
|
|
84
|
+
// Apply filters
|
|
85
|
+
if (args.query) {
|
|
86
|
+
const q = args.query.toLowerCase();
|
|
87
|
+
actions = actions.filter(a => a.name?.toLowerCase().includes(q));
|
|
88
|
+
}
|
|
89
|
+
if (args.category) {
|
|
90
|
+
actions = actions.filter(a => a.category === args.category);
|
|
91
|
+
}
|
|
92
|
+
if (args.enabled !== undefined) {
|
|
93
|
+
actions = actions.filter(a => a.enabled === args.enabled);
|
|
94
|
+
}
|
|
95
|
+
// Filter by persona workflow
|
|
96
|
+
if (args.persona_id) {
|
|
97
|
+
const persona = await client.getPersonaById(args.persona_id);
|
|
98
|
+
if (persona?.workflow_id) {
|
|
99
|
+
const workflowActionIds = await client.listActionsFromWorkflow(persona.workflow_id);
|
|
100
|
+
const actionIdSet = new Set(workflowActionIds);
|
|
101
|
+
actions = actions.filter(a => actionIdSet.has(a.id));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Apply limit
|
|
105
|
+
const limit = args.limit || 100;
|
|
106
|
+
actions = actions.slice(0, limit);
|
|
107
|
+
// Include docs if requested
|
|
108
|
+
if (args.include_docs) {
|
|
109
|
+
return {
|
|
110
|
+
count: actions.length,
|
|
111
|
+
actions: actions.map(a => ({
|
|
112
|
+
...a,
|
|
113
|
+
documentation: a.name ? getAgentByName(a.name) : undefined,
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
count: actions.length,
|
|
119
|
+
actions: actions.map(a => ({
|
|
120
|
+
id: a.id,
|
|
121
|
+
name: a.name,
|
|
122
|
+
category: a.category,
|
|
123
|
+
enabled: a.enabled,
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
// Fallback to catalog only
|
|
129
|
+
let agents = Object.entries(AGENT_CATALOG);
|
|
130
|
+
if (args.category) {
|
|
131
|
+
agents = agents.filter(([_, a]) => a.category === args.category);
|
|
132
|
+
}
|
|
133
|
+
if (args.query) {
|
|
134
|
+
const q = args.query.toLowerCase();
|
|
135
|
+
agents = agents.filter(([name]) => name.toLowerCase().includes(q));
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
count: agents.length,
|
|
139
|
+
actions: agents.map(([name, agent]) => ({
|
|
140
|
+
name,
|
|
141
|
+
category: agent.category,
|
|
142
|
+
description: agent.description,
|
|
143
|
+
source: "catalog",
|
|
144
|
+
})),
|
|
145
|
+
note: "Live API unavailable, showing catalog data",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Executor - Executes composable action sequences
|
|
3
|
+
*
|
|
4
|
+
* Replaces flag-based parameters with explicit action composition.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
* ```
|
|
8
|
+
* persona(
|
|
9
|
+
* method="create",
|
|
10
|
+
* from="source-id",
|
|
11
|
+
* name="Clone",
|
|
12
|
+
* actions=[
|
|
13
|
+
* {tool:"data", args:{method:"copy", from:"$source"}},
|
|
14
|
+
* {tool:"data", args:{method:"sanitize", examples:["Acme"]}},
|
|
15
|
+
* {tool:"snapshot", args:{message:"Clone ready"}},
|
|
16
|
+
* ]
|
|
17
|
+
* )
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Context variables:
|
|
21
|
+
* - $source: The `from` parameter (template/persona ID being cloned)
|
|
22
|
+
* - $target: The ID of the created/modified persona
|
|
23
|
+
* - $env: Current environment name
|
|
24
|
+
*/
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Action Aliases
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
/**
|
|
29
|
+
* Built-in action aliases for common patterns
|
|
30
|
+
*/
|
|
31
|
+
export const ACTION_ALIASES = {
|
|
32
|
+
// Copy data from source to target
|
|
33
|
+
"copy-data": [
|
|
34
|
+
{ tool: "data", args: { method: "copy", from: "$source" } },
|
|
35
|
+
],
|
|
36
|
+
// Copy and sanitize data
|
|
37
|
+
"copy-and-sanitize": [
|
|
38
|
+
{ tool: "data", args: { method: "copy", from: "$source" } },
|
|
39
|
+
{ tool: "data", args: { method: "sanitize" } },
|
|
40
|
+
],
|
|
41
|
+
// Standard demo setup: copy, sanitize, snapshot
|
|
42
|
+
"standard-demo-setup": [
|
|
43
|
+
{ tool: "data", args: { method: "copy", from: "$source" } },
|
|
44
|
+
{ tool: "data", args: { method: "sanitize" } },
|
|
45
|
+
{ tool: "snapshot", args: { message: "Demo setup complete" } },
|
|
46
|
+
],
|
|
47
|
+
// Snapshot only
|
|
48
|
+
"create-snapshot": [
|
|
49
|
+
{ tool: "snapshot", args: { message: "Checkpoint" } },
|
|
50
|
+
],
|
|
51
|
+
// Validate workflow
|
|
52
|
+
"validate-workflow": [
|
|
53
|
+
{ tool: "validate", args: { checks: ["workflow"] } },
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
// Context Substitution
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Substitute context variables in action args
|
|
61
|
+
*
|
|
62
|
+
* Variables:
|
|
63
|
+
* - $source → context.source
|
|
64
|
+
* - $target → context.target
|
|
65
|
+
* - $env → context.env
|
|
66
|
+
*/
|
|
67
|
+
function substituteContext(action, context) {
|
|
68
|
+
// Deep clone args to avoid mutation
|
|
69
|
+
const args = JSON.parse(JSON.stringify(action.args));
|
|
70
|
+
function substitute(obj) {
|
|
71
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
obj[key] = value
|
|
74
|
+
.replace(/\$source/g, context.source ?? "")
|
|
75
|
+
.replace(/\$target/g, context.target ?? "")
|
|
76
|
+
.replace(/\$env/g, context.env ?? "");
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(value)) {
|
|
79
|
+
for (let i = 0; i < value.length; i++) {
|
|
80
|
+
if (typeof value[i] === "string") {
|
|
81
|
+
value[i] = value[i]
|
|
82
|
+
.replace(/\$source/g, context.source ?? "")
|
|
83
|
+
.replace(/\$target/g, context.target ?? "")
|
|
84
|
+
.replace(/\$env/g, context.env ?? "");
|
|
85
|
+
}
|
|
86
|
+
else if (typeof value[i] === "object" && value[i] !== null) {
|
|
87
|
+
substitute(value[i]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (typeof value === "object" && value !== null) {
|
|
92
|
+
substitute(value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
substitute(args);
|
|
97
|
+
return { tool: action.tool, args };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Expand aliases into action specs
|
|
101
|
+
*/
|
|
102
|
+
function expandAliases(actions) {
|
|
103
|
+
const expanded = [];
|
|
104
|
+
for (const action of actions) {
|
|
105
|
+
if (typeof action === "string") {
|
|
106
|
+
// It's an alias
|
|
107
|
+
const aliasActions = ACTION_ALIASES[action];
|
|
108
|
+
if (aliasActions) {
|
|
109
|
+
expanded.push(...aliasActions);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Unknown alias - will be reported as error during execution
|
|
113
|
+
console.warn(`[action-executor] Unknown alias: ${action}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// It's an action spec
|
|
118
|
+
expanded.push(action);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return expanded;
|
|
122
|
+
}
|
|
123
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
124
|
+
// Action Handlers
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
126
|
+
/**
|
|
127
|
+
* Execute a data action using the real data handler
|
|
128
|
+
*/
|
|
129
|
+
async function executeDataAction(args, context, client) {
|
|
130
|
+
const method = args.method;
|
|
131
|
+
const targetId = context.target;
|
|
132
|
+
if (!targetId) {
|
|
133
|
+
throw new Error("No target persona ID available for data action");
|
|
134
|
+
}
|
|
135
|
+
// Substitute context variables in args
|
|
136
|
+
const resolvedArgs = { ...args };
|
|
137
|
+
if (typeof resolvedArgs.from === "string") {
|
|
138
|
+
resolvedArgs.from = resolvedArgs.from
|
|
139
|
+
.replace(/\$source/g, context.source ?? "")
|
|
140
|
+
.replace(/\$target/g, context.target ?? "");
|
|
141
|
+
}
|
|
142
|
+
// Import and use the unified data handler
|
|
143
|
+
const { handleData } = await import("./data/index.js");
|
|
144
|
+
const result = await handleData({
|
|
145
|
+
persona_id: targetId,
|
|
146
|
+
data: { method, ...resolvedArgs },
|
|
147
|
+
mode: "data", // Legacy mode marker
|
|
148
|
+
}, client);
|
|
149
|
+
// Check for errors in result
|
|
150
|
+
if (result.error) {
|
|
151
|
+
throw new Error(result.error);
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Execute a snapshot action
|
|
157
|
+
*/
|
|
158
|
+
async function executeSnapshotAction(args, context, client) {
|
|
159
|
+
const message = args.message ?? "Action-created snapshot";
|
|
160
|
+
const targetId = context.target;
|
|
161
|
+
if (!targetId) {
|
|
162
|
+
throw new Error("No target persona ID available for snapshot action");
|
|
163
|
+
}
|
|
164
|
+
// For now, return a placeholder
|
|
165
|
+
// Full implementation would call the version storage API
|
|
166
|
+
return {
|
|
167
|
+
status: "snapshot_created",
|
|
168
|
+
persona_id: targetId,
|
|
169
|
+
message,
|
|
170
|
+
timestamp: new Date().toISOString(),
|
|
171
|
+
note: "Full snapshot integration pending version storage implementation",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Execute a validate action
|
|
176
|
+
*/
|
|
177
|
+
async function executeValidateAction(args, context, client) {
|
|
178
|
+
const checks = args.checks ?? ["workflow", "config"];
|
|
179
|
+
const targetId = context.target;
|
|
180
|
+
if (!targetId) {
|
|
181
|
+
throw new Error("No target persona ID available for validate action");
|
|
182
|
+
}
|
|
183
|
+
// Fetch persona and run validation
|
|
184
|
+
const persona = await client.getPersonaById(targetId);
|
|
185
|
+
if (!persona) {
|
|
186
|
+
throw new Error(`Persona not found: ${targetId}`);
|
|
187
|
+
}
|
|
188
|
+
const results = {};
|
|
189
|
+
if (checks.includes("workflow")) {
|
|
190
|
+
const workflowDef = persona.workflow_def;
|
|
191
|
+
results.workflow = {
|
|
192
|
+
has_workflow: !!persona.workflow_def,
|
|
193
|
+
node_count: workflowDef?.nodes?.length ?? 0,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (checks.includes("config")) {
|
|
197
|
+
const protoConfig = persona.proto_config;
|
|
198
|
+
results.config = {
|
|
199
|
+
has_proto_config: !!persona.proto_config,
|
|
200
|
+
widget_count: protoConfig?.widgets?.length ?? 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
status: "validated",
|
|
205
|
+
persona_id: targetId,
|
|
206
|
+
checks,
|
|
207
|
+
results,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Execute a single action
|
|
212
|
+
*/
|
|
213
|
+
async function executeAction(action, context, client) {
|
|
214
|
+
switch (action.tool) {
|
|
215
|
+
case "data":
|
|
216
|
+
return executeDataAction(action.args, context, client);
|
|
217
|
+
case "snapshot":
|
|
218
|
+
return executeSnapshotAction(action.args, context, client);
|
|
219
|
+
case "validate":
|
|
220
|
+
return executeValidateAction(action.args, context, client);
|
|
221
|
+
default:
|
|
222
|
+
throw new Error(`Unknown action tool: ${action.tool}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
// Main Executor
|
|
227
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Execute a sequence of actions with context substitution
|
|
230
|
+
*
|
|
231
|
+
* @param actions - Array of action specs or alias strings
|
|
232
|
+
* @param context - Execution context with source/target IDs
|
|
233
|
+
* @param client - EmaClient for API calls
|
|
234
|
+
* @returns Results of all action executions
|
|
235
|
+
*/
|
|
236
|
+
export async function executeActions(actions, context, client) {
|
|
237
|
+
const results = [];
|
|
238
|
+
let succeeded = 0;
|
|
239
|
+
let failed = 0;
|
|
240
|
+
let stoppedOnError = false;
|
|
241
|
+
// Expand aliases
|
|
242
|
+
const expanded = expandAliases(actions);
|
|
243
|
+
// Execute in sequence
|
|
244
|
+
for (const action of expanded) {
|
|
245
|
+
const startTime = Date.now();
|
|
246
|
+
// Substitute context variables
|
|
247
|
+
const substituted = substituteContext(action, context);
|
|
248
|
+
try {
|
|
249
|
+
const result = await executeAction(substituted, context, client);
|
|
250
|
+
results.push({
|
|
251
|
+
tool: action.tool,
|
|
252
|
+
status: "success",
|
|
253
|
+
result,
|
|
254
|
+
duration_ms: Date.now() - startTime,
|
|
255
|
+
});
|
|
256
|
+
succeeded++;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
results.push({
|
|
260
|
+
tool: action.tool,
|
|
261
|
+
status: "error",
|
|
262
|
+
error: error instanceof Error ? error.message : String(error),
|
|
263
|
+
duration_ms: Date.now() - startTime,
|
|
264
|
+
});
|
|
265
|
+
failed++;
|
|
266
|
+
stoppedOnError = true;
|
|
267
|
+
break; // Stop on first error
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
executed: results.length,
|
|
272
|
+
succeeded,
|
|
273
|
+
failed,
|
|
274
|
+
results,
|
|
275
|
+
stopped_on_error: stoppedOnError ? true : undefined,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if actions array contains any valid actions
|
|
280
|
+
*/
|
|
281
|
+
export function hasActions(actions) {
|
|
282
|
+
return Array.isArray(actions) && actions.length > 0;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Validate actions array structure
|
|
286
|
+
*/
|
|
287
|
+
export function validateActions(actions) {
|
|
288
|
+
const errors = [];
|
|
289
|
+
if (!Array.isArray(actions)) {
|
|
290
|
+
return { valid: false, errors: ["actions must be an array"] };
|
|
291
|
+
}
|
|
292
|
+
for (let i = 0; i < actions.length; i++) {
|
|
293
|
+
const action = actions[i];
|
|
294
|
+
if (typeof action === "string") {
|
|
295
|
+
// Alias - check if known
|
|
296
|
+
if (!ACTION_ALIASES[action]) {
|
|
297
|
+
errors.push(`actions[${i}]: unknown alias "${action}". Valid: ${Object.keys(ACTION_ALIASES).join(", ")}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else if (typeof action === "object" && action !== null) {
|
|
301
|
+
// Action spec - check required fields
|
|
302
|
+
const spec = action;
|
|
303
|
+
if (!spec.tool) {
|
|
304
|
+
errors.push(`actions[${i}]: missing required field "tool"`);
|
|
305
|
+
}
|
|
306
|
+
else if (!["data", "snapshot", "validate"].includes(spec.tool)) {
|
|
307
|
+
errors.push(`actions[${i}]: invalid tool "${spec.tool}". Valid: data, snapshot, validate`);
|
|
308
|
+
}
|
|
309
|
+
if (!spec.args || typeof spec.args !== "object") {
|
|
310
|
+
errors.push(`actions[${i}]: missing or invalid "args" object`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
errors.push(`actions[${i}]: must be object or string, got ${typeof action}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { valid: errors.length === 0, errors };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get list of available aliases with descriptions
|
|
321
|
+
*/
|
|
322
|
+
export function getAvailableAliases() {
|
|
323
|
+
return [
|
|
324
|
+
{
|
|
325
|
+
name: "copy-data",
|
|
326
|
+
description: "Copy data from source to target persona",
|
|
327
|
+
actions: ACTION_ALIASES["copy-data"],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "copy-and-sanitize",
|
|
331
|
+
description: "Copy data then sanitize PII",
|
|
332
|
+
actions: ACTION_ALIASES["copy-and-sanitize"],
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "standard-demo-setup",
|
|
336
|
+
description: "Copy, sanitize, and create snapshot",
|
|
337
|
+
actions: ACTION_ALIASES["standard-demo-setup"],
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "create-snapshot",
|
|
341
|
+
description: "Create a checkpoint snapshot",
|
|
342
|
+
actions: ACTION_ALIASES["create-snapshot"],
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "validate-workflow",
|
|
346
|
+
description: "Run workflow validation checks",
|
|
347
|
+
actions: ACTION_ALIASES["validate-workflow"],
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
}
|