@ema.co/mcp-toolkit 1.5.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.
- package/README.md +2 -2
- package/dist/mcp/handlers-consolidated.js +773 -25
- package/dist/mcp/resources.js +124 -0
- package/dist/mcp/server.js +13 -205
- package/dist/mcp/tools-consolidated.js +163 -103
- package/dist/sdk/action-registry.js +128 -0
- package/dist/sdk/action-schema-parser.js +379 -0
- package/dist/sdk/client.js +757 -90
- package/dist/sdk/generated/api-types.js +11 -0
- package/dist/sdk/index.js +59 -2
- package/dist/sdk/intent-architect.js +883 -0
- package/dist/sdk/knowledge.js +38 -8
- package/dist/sdk/quality-gates.js +386 -0
- package/dist/sdk/sanitizer.js +1121 -0
- package/dist/sdk/structural-rules.js +290 -0
- package/dist/sdk/workflow-generator.js +88 -34
- package/dist/sdk/workflow-intent.js +237 -24
- package/dist/sdk/workflow-optimizer.js +665 -0
- package/dist/sdk/workflow-tracer.js +648 -0
- package/dist/sdk/workflow-transformer.js +10 -0
- package/dist/sdk/workflow-validator.js +609 -0
- package/docs/local-generation.md +508 -0
- package/docs/mcp-flow-diagram.md +135 -0
- package/docs/mcp-tools-guide.md +196 -204
- package/docs/release-process.md +153 -0
- package/docs/tool-consolidation-proposal.md +166 -378
- package/package.json +8 -2
- package/resources/action-schema.json +5678 -0
- package/resources/config/gates.json +88 -0
- package/resources/config/gates.schema.json +77 -0
- package/resources/templates/auto-builder-rules.md +222 -0
- package/resources/templates/demo-scenarios/test-published-package.md +116 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-Driven Workflow Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates workflow specs against actual API definitions:
|
|
5
|
+
* - ListActions: Available actions with input/output schemas
|
|
6
|
+
* - GetPersonaTemplates: Available templates with configurations
|
|
7
|
+
* - OpenAPI spec: Endpoint contracts (optional, for payload validation)
|
|
8
|
+
*
|
|
9
|
+
* This ensures workflows are valid BEFORE deployment, catching errors early.
|
|
10
|
+
*/
|
|
11
|
+
import { generateSchemaBundle, } from "./action-schema-parser.js";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
// Utilities
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Simple hash function for fingerprinting (not cryptographically secure)
|
|
18
|
+
* Used for detecting schema changes, not for security.
|
|
19
|
+
*/
|
|
20
|
+
function simpleHash(str) {
|
|
21
|
+
let hash = 0;
|
|
22
|
+
for (let i = 0; i < str.length; i++) {
|
|
23
|
+
const char = str.charCodeAt(i);
|
|
24
|
+
hash = ((hash << 5) - hash) + char;
|
|
25
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
26
|
+
}
|
|
27
|
+
// Convert to hex and pad to 8 chars
|
|
28
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
29
|
+
}
|
|
30
|
+
export class APISchemaRegistry {
|
|
31
|
+
actions = new Map();
|
|
32
|
+
templates = new Map();
|
|
33
|
+
loaded = false;
|
|
34
|
+
_metadata = {
|
|
35
|
+
source: "api",
|
|
36
|
+
version: "unknown",
|
|
37
|
+
generatedAt: new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Get schema metadata (source, version, etc.)
|
|
41
|
+
*/
|
|
42
|
+
get metadata() {
|
|
43
|
+
return this._metadata;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Load schemas from API (primary source - reflects deployed state)
|
|
47
|
+
*/
|
|
48
|
+
async load(client) {
|
|
49
|
+
const [actionDTOs, templateDTOs] = await Promise.all([
|
|
50
|
+
client.listActions().catch(() => []),
|
|
51
|
+
client.getPersonaTemplates().catch(() => []),
|
|
52
|
+
]);
|
|
53
|
+
// Parse actions
|
|
54
|
+
for (const dto of actionDTOs) {
|
|
55
|
+
const schema = this.parseActionDTO(dto);
|
|
56
|
+
if (schema) {
|
|
57
|
+
this.actions.set(schema.name, schema);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Parse templates
|
|
61
|
+
for (const dto of templateDTOs) {
|
|
62
|
+
const schema = this.parseTemplateDTO(dto);
|
|
63
|
+
if (schema) {
|
|
64
|
+
this.templates.set(schema.id, schema);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this._metadata = {
|
|
68
|
+
source: "api",
|
|
69
|
+
version: new Date().toISOString().split("T")[0],
|
|
70
|
+
generatedAt: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
this.loaded = true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load schemas from ema repo source code (for development/offline use)
|
|
76
|
+
* @param config - Configuration for ema repo paths
|
|
77
|
+
*/
|
|
78
|
+
loadFromEmaRepo(config) {
|
|
79
|
+
const bundle = generateSchemaBundle(config);
|
|
80
|
+
this.loadFromBundle(bundle);
|
|
81
|
+
this._metadata.source = "code";
|
|
82
|
+
this._metadata.sourcePath = config.basePath;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Load schemas from a pre-generated bundle (JSON file)
|
|
86
|
+
* @param bundleOrPath - Bundle object or path to JSON file
|
|
87
|
+
*/
|
|
88
|
+
loadFromBundle(bundleOrPath) {
|
|
89
|
+
const bundle = typeof bundleOrPath === "string"
|
|
90
|
+
? JSON.parse(fs.readFileSync(bundleOrPath, "utf-8"))
|
|
91
|
+
: bundleOrPath;
|
|
92
|
+
// Convert ParsedAction to ActionSchema
|
|
93
|
+
for (const [actionName, versions] of Object.entries(bundle.actions)) {
|
|
94
|
+
// Use the latest version (last in array)
|
|
95
|
+
const action = versions[versions.length - 1];
|
|
96
|
+
const schema = this.parsedActionToSchema(action);
|
|
97
|
+
this.actions.set(actionName, schema);
|
|
98
|
+
}
|
|
99
|
+
this._metadata = {
|
|
100
|
+
source: bundle.source,
|
|
101
|
+
version: bundle.version,
|
|
102
|
+
generatedAt: bundle.generatedAt,
|
|
103
|
+
sourcePath: bundle.sourcePath,
|
|
104
|
+
};
|
|
105
|
+
this.loaded = true;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Load with fallback strategy: API -> Code -> Bundle
|
|
109
|
+
*/
|
|
110
|
+
async loadWithFallback(options) {
|
|
111
|
+
// 1. Try live API first (most accurate for deployed state)
|
|
112
|
+
if (options.client) {
|
|
113
|
+
try {
|
|
114
|
+
await this.load(options.client);
|
|
115
|
+
this._metadata.environment = options.environment;
|
|
116
|
+
console.log(`[SchemaRegistry] Loaded ${this.actions.size} actions from API`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.warn(`[SchemaRegistry] API load failed, trying fallback:`, e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// 2. Try parsing ema repo (for development)
|
|
124
|
+
if (options.emaRepoPath) {
|
|
125
|
+
try {
|
|
126
|
+
this.loadFromEmaRepo({ basePath: options.emaRepoPath });
|
|
127
|
+
console.log(`[SchemaRegistry] Loaded ${this.actions.size} actions from ema repo`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
console.warn(`[SchemaRegistry] Ema repo load failed, trying fallback:`, e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 3. Use bundled schema (offline/CI)
|
|
135
|
+
if (options.bundlePath) {
|
|
136
|
+
try {
|
|
137
|
+
this.loadFromBundle(options.bundlePath);
|
|
138
|
+
console.log(`[SchemaRegistry] Loaded ${this.actions.size} actions from bundle`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.warn(`[SchemaRegistry] Bundle load failed:`, e);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
console.warn("[SchemaRegistry] No schema source available - validation will be limited");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Convert ParsedAction to ActionSchema
|
|
149
|
+
*/
|
|
150
|
+
parsedActionToSchema(action) {
|
|
151
|
+
const inputs = new Map();
|
|
152
|
+
const outputs = new Map();
|
|
153
|
+
for (const input of action.inputs) {
|
|
154
|
+
inputs.set(input.name, {
|
|
155
|
+
name: input.name,
|
|
156
|
+
type: input.argType.wellKnownType ?? input.argType.typeParameter ?? "unknown",
|
|
157
|
+
required: !input.isOptional,
|
|
158
|
+
description: input.description ?? input.displayName,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
for (const output of action.outputs) {
|
|
162
|
+
outputs.set(output.name, {
|
|
163
|
+
name: output.name,
|
|
164
|
+
type: output.argType.wellKnownType ?? output.argType.typeParameter ?? "unknown",
|
|
165
|
+
description: output.displayName,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
name: action.name,
|
|
170
|
+
displayName: action.displayName,
|
|
171
|
+
description: action.description,
|
|
172
|
+
category: action.category,
|
|
173
|
+
version: action.version,
|
|
174
|
+
inputs,
|
|
175
|
+
outputs,
|
|
176
|
+
documentation: action.documentation, // Raw docs for LLM context
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
isLoaded() {
|
|
180
|
+
return this.loaded;
|
|
181
|
+
}
|
|
182
|
+
getAction(name) {
|
|
183
|
+
return this.actions.get(name);
|
|
184
|
+
}
|
|
185
|
+
getTemplate(id) {
|
|
186
|
+
return this.templates.get(id);
|
|
187
|
+
}
|
|
188
|
+
getAllActions() {
|
|
189
|
+
return Array.from(this.actions.values());
|
|
190
|
+
}
|
|
191
|
+
getAllTemplates() {
|
|
192
|
+
return Array.from(this.templates.values());
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Compute a fingerprint hash of all action definitions.
|
|
196
|
+
* Used for detecting skew between API and code schemas.
|
|
197
|
+
*/
|
|
198
|
+
computeFingerprint() {
|
|
199
|
+
const actions = this.getAllActions().sort((a, b) => a.name.localeCompare(b.name));
|
|
200
|
+
const data = actions.map(a => `${a.name}:${a.version}:${a.inputs.size}:${a.outputs.size}`).join("|");
|
|
201
|
+
return simpleHash(data);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Compare this registry against another to detect schema skew.
|
|
205
|
+
* Useful for detecting when API differs from code definitions.
|
|
206
|
+
*/
|
|
207
|
+
compareWith(other) {
|
|
208
|
+
const thisActions = new Map(this.actions);
|
|
209
|
+
const otherActions = new Map(other.actions);
|
|
210
|
+
const addedInOther = [];
|
|
211
|
+
const removedFromOther = [];
|
|
212
|
+
const versionDiff = [];
|
|
213
|
+
// Check for actions in "other" but not in "this"
|
|
214
|
+
for (const [name, action] of otherActions) {
|
|
215
|
+
if (!thisActions.has(name)) {
|
|
216
|
+
addedInOther.push(name);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const thisAction = thisActions.get(name);
|
|
220
|
+
if (thisAction.version !== action.version) {
|
|
221
|
+
versionDiff.push({
|
|
222
|
+
name,
|
|
223
|
+
apiVersion: otherActions === this.actions ? thisAction.version : action.version,
|
|
224
|
+
codeVersion: otherActions === this.actions ? action.version : thisAction.version,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Check for actions in "this" but not in "other"
|
|
230
|
+
for (const name of thisActions.keys()) {
|
|
231
|
+
if (!otherActions.has(name)) {
|
|
232
|
+
removedFromOther.push(name);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
hasSkew: addedInOther.length > 0 || removedFromOther.length > 0 || versionDiff.length > 0,
|
|
237
|
+
addedInApi: addedInOther,
|
|
238
|
+
removedFromApi: removedFromOther,
|
|
239
|
+
versionDiff,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Parse ActionDTO from ListActions API into ActionSchema
|
|
244
|
+
*/
|
|
245
|
+
parseActionDTO(dto) {
|
|
246
|
+
try {
|
|
247
|
+
const typeName = dto.typeName;
|
|
248
|
+
const name = typeName?.name?.name;
|
|
249
|
+
if (!name)
|
|
250
|
+
return null;
|
|
251
|
+
const inputs = new Map();
|
|
252
|
+
const outputs = new Map();
|
|
253
|
+
// Parse inputs from dto.inputs.inputs
|
|
254
|
+
const inputsObj = dto.inputs?.inputs;
|
|
255
|
+
if (inputsObj && typeof inputsObj === "object") {
|
|
256
|
+
for (const [inputName, inputDef] of Object.entries(inputsObj)) {
|
|
257
|
+
const def = inputDef;
|
|
258
|
+
inputs.set(inputName, {
|
|
259
|
+
name: inputName,
|
|
260
|
+
type: def.type?.wellKnownType ?? "unknown",
|
|
261
|
+
required: !def.isOptional,
|
|
262
|
+
description: def.description ?? def.displayName,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Parse outputs from dto.outputs.outputs
|
|
267
|
+
const outputsObj = dto.outputs?.outputs;
|
|
268
|
+
if (outputsObj && typeof outputsObj === "object") {
|
|
269
|
+
for (const [outputName, outputDef] of Object.entries(outputsObj)) {
|
|
270
|
+
const def = outputDef;
|
|
271
|
+
outputs.set(outputName, {
|
|
272
|
+
name: outputName,
|
|
273
|
+
type: def.type?.wellKnownType ?? "unknown",
|
|
274
|
+
description: def.description,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Get displayName - might be string or object with nested value
|
|
279
|
+
const displayName = typeof dto.displayName === "string"
|
|
280
|
+
? dto.displayName
|
|
281
|
+
: dto.name ?? name;
|
|
282
|
+
// Get description - might be string or object
|
|
283
|
+
const description = typeof dto.description === "string"
|
|
284
|
+
? dto.description
|
|
285
|
+
: "";
|
|
286
|
+
return {
|
|
287
|
+
name,
|
|
288
|
+
displayName: displayName,
|
|
289
|
+
description,
|
|
290
|
+
category: dto.category ?? "unknown",
|
|
291
|
+
version: typeName?.version ?? "v1",
|
|
292
|
+
inputs,
|
|
293
|
+
outputs,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Parse PersonaTemplateDTO from GetPersonaTemplates API
|
|
302
|
+
*/
|
|
303
|
+
parseTemplateDTO(dto) {
|
|
304
|
+
try {
|
|
305
|
+
if (!dto.id)
|
|
306
|
+
return null;
|
|
307
|
+
// Determine type from template name or ID
|
|
308
|
+
let type = "chat";
|
|
309
|
+
const nameLower = (dto.name ?? "").toLowerCase();
|
|
310
|
+
if (nameLower.includes("voice"))
|
|
311
|
+
type = "voice";
|
|
312
|
+
else if (nameLower.includes("dashboard"))
|
|
313
|
+
type = "dashboard";
|
|
314
|
+
// Extract widget names from proto_config
|
|
315
|
+
const defaultWidgets = [];
|
|
316
|
+
const protoConfig = dto.proto_config;
|
|
317
|
+
if (protoConfig?.widgets) {
|
|
318
|
+
for (const widget of protoConfig.widgets) {
|
|
319
|
+
if (widget.name)
|
|
320
|
+
defaultWidgets.push(widget.name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
id: dto.id,
|
|
325
|
+
name: dto.name ?? dto.id,
|
|
326
|
+
type,
|
|
327
|
+
description: dto.description,
|
|
328
|
+
defaultWidgets,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
// Workflow Validator
|
|
338
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
339
|
+
/**
|
|
340
|
+
* Validate a WorkflowSpec against API schemas
|
|
341
|
+
*/
|
|
342
|
+
export function validateWorkflowSpec(spec, registry) {
|
|
343
|
+
const errors = [];
|
|
344
|
+
const warnings = [];
|
|
345
|
+
const usedActions = [];
|
|
346
|
+
const unknownActions = [];
|
|
347
|
+
// Build node map for reference checking
|
|
348
|
+
const nodeMap = new Map();
|
|
349
|
+
for (const node of spec.nodes) {
|
|
350
|
+
nodeMap.set(node.id, node);
|
|
351
|
+
}
|
|
352
|
+
// Check if registry actually has data - if not, skip action-specific validation
|
|
353
|
+
// (API may be unavailable or mocked)
|
|
354
|
+
const registryHasData = registry.isLoaded() && registry.getAllActions().length > 0;
|
|
355
|
+
// 1. Check for trigger node
|
|
356
|
+
const triggerTypes = ["chat_trigger", "document_trigger", "voice_trigger"];
|
|
357
|
+
const hasTrigger = spec.nodes.some(n => triggerTypes.includes(n.actionType));
|
|
358
|
+
if (!hasTrigger) {
|
|
359
|
+
errors.push({
|
|
360
|
+
type: "missing_trigger",
|
|
361
|
+
message: "Workflow must have a trigger node (chat_trigger, document_trigger)",
|
|
362
|
+
suggestion: "Add a trigger node as the first node in your workflow",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// 2. Validate each node
|
|
366
|
+
for (const node of spec.nodes) {
|
|
367
|
+
const actionSchema = registry.getAction(node.actionType);
|
|
368
|
+
// Schema-dependent validation (only when registry has data)
|
|
369
|
+
if (!actionSchema && registryHasData) {
|
|
370
|
+
// Action not found in API (only flag if we have data to check against)
|
|
371
|
+
unknownActions.push(node.actionType);
|
|
372
|
+
errors.push({
|
|
373
|
+
type: "missing_action",
|
|
374
|
+
node_id: node.id,
|
|
375
|
+
message: `Action "${node.actionType}" not found in API`,
|
|
376
|
+
suggestion: `Check ListActions for available actions. Similar: ${findSimilarActions(node.actionType, registry)}`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else if (actionSchema) {
|
|
380
|
+
usedActions.push(node.actionType);
|
|
381
|
+
// Validate required inputs
|
|
382
|
+
for (const [inputName, inputSchema] of actionSchema.inputs) {
|
|
383
|
+
if (inputSchema.required && !node.inputs?.[inputName]) {
|
|
384
|
+
errors.push({
|
|
385
|
+
type: "missing_input",
|
|
386
|
+
node_id: node.id,
|
|
387
|
+
field: inputName,
|
|
388
|
+
message: `Required input "${inputName}" missing on node "${node.id}" (${node.actionType})`,
|
|
389
|
+
suggestion: `Add input "${inputName}" of type ${inputSchema.type}`,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Schema-aware input validation
|
|
394
|
+
if (node.inputs) {
|
|
395
|
+
for (const [inputName, binding] of Object.entries(node.inputs)) {
|
|
396
|
+
const inputSchema = actionSchema.inputs.get(inputName);
|
|
397
|
+
// Check if input exists on action
|
|
398
|
+
if (!inputSchema && actionSchema.inputs.size > 0) {
|
|
399
|
+
warnings.push({
|
|
400
|
+
type: "suboptimal_wiring",
|
|
401
|
+
node_id: node.id,
|
|
402
|
+
message: `Input "${inputName}" may not exist on action "${node.actionType}"`,
|
|
403
|
+
suggestion: `Valid inputs: ${Array.from(actionSchema.inputs.keys()).join(", ")}`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Schema-INDEPENDENT validation (ALWAYS runs - checks node references)
|
|
410
|
+
if (node.inputs) {
|
|
411
|
+
for (const [inputName, binding] of Object.entries(node.inputs)) {
|
|
412
|
+
// Validate action_output references - doesn't need schema
|
|
413
|
+
if (binding.type === "action_output" && binding.actionName) {
|
|
414
|
+
const sourceNode = nodeMap.get(binding.actionName);
|
|
415
|
+
if (!sourceNode) {
|
|
416
|
+
errors.push({
|
|
417
|
+
type: "invalid_wiring",
|
|
418
|
+
node_id: node.id,
|
|
419
|
+
field: inputName,
|
|
420
|
+
message: `Input "${inputName}" references non-existent node "${binding.actionName}"`,
|
|
421
|
+
suggestion: `Valid nodes: ${Array.from(nodeMap.keys()).join(", ")}`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
else if (actionSchema) {
|
|
425
|
+
// Output validation only if we have schema
|
|
426
|
+
const sourceSchema = registry.getAction(sourceNode.actionType);
|
|
427
|
+
if (sourceSchema && binding.output && !sourceSchema.outputs.has(binding.output)) {
|
|
428
|
+
warnings.push({
|
|
429
|
+
type: "suboptimal_wiring",
|
|
430
|
+
node_id: node.id,
|
|
431
|
+
message: `Output "${binding.output}" may not exist on action "${sourceNode.actionType}"`,
|
|
432
|
+
suggestion: `Valid outputs: ${Array.from(sourceSchema.outputs.keys()).join(", ")}`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Validate runIf references - also schema-independent
|
|
440
|
+
if (node.runIf && "actionName" in node.runIf) {
|
|
441
|
+
const runIfNode = nodeMap.get(node.runIf.actionName);
|
|
442
|
+
if (!runIfNode) {
|
|
443
|
+
errors.push({
|
|
444
|
+
type: "invalid_wiring",
|
|
445
|
+
node_id: node.id,
|
|
446
|
+
message: `runIf references non-existent node "${node.runIf.actionName}"`,
|
|
447
|
+
suggestion: `Valid nodes: ${Array.from(nodeMap.keys()).join(", ")}`,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// 3. Validate result mappings
|
|
453
|
+
for (const mapping of spec.resultMappings) {
|
|
454
|
+
if (!nodeMap.has(mapping.nodeId)) {
|
|
455
|
+
errors.push({
|
|
456
|
+
type: "invalid_structure",
|
|
457
|
+
message: `Result mapping references non-existent node "${mapping.nodeId}"`,
|
|
458
|
+
suggestion: `Valid nodes: ${Array.from(nodeMap.keys()).join(", ")}`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
valid: errors.length === 0,
|
|
464
|
+
errors,
|
|
465
|
+
warnings,
|
|
466
|
+
action_coverage: {
|
|
467
|
+
used: [...new Set(usedActions)],
|
|
468
|
+
available: registry.getAllActions().map(a => a.name),
|
|
469
|
+
unknown: unknownActions,
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Find similar action names for suggestions
|
|
475
|
+
*/
|
|
476
|
+
function findSimilarActions(name, registry) {
|
|
477
|
+
const allActions = registry.getAllActions();
|
|
478
|
+
const similar = allActions
|
|
479
|
+
.filter(a => {
|
|
480
|
+
const aLower = a.name.toLowerCase();
|
|
481
|
+
const nLower = name.toLowerCase();
|
|
482
|
+
return aLower.includes(nLower) || nLower.includes(aLower) ||
|
|
483
|
+
levenshteinDistance(aLower, nLower) <= 3;
|
|
484
|
+
})
|
|
485
|
+
.slice(0, 3)
|
|
486
|
+
.map(a => a.name);
|
|
487
|
+
return similar.length > 0 ? similar.join(", ") : "none found";
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Simple Levenshtein distance for fuzzy matching
|
|
491
|
+
*/
|
|
492
|
+
function levenshteinDistance(a, b) {
|
|
493
|
+
if (a.length === 0)
|
|
494
|
+
return b.length;
|
|
495
|
+
if (b.length === 0)
|
|
496
|
+
return a.length;
|
|
497
|
+
const matrix = [];
|
|
498
|
+
for (let i = 0; i <= b.length; i++) {
|
|
499
|
+
matrix[i] = [i];
|
|
500
|
+
}
|
|
501
|
+
for (let j = 0; j <= a.length; j++) {
|
|
502
|
+
matrix[0][j] = j;
|
|
503
|
+
}
|
|
504
|
+
for (let i = 1; i <= b.length; i++) {
|
|
505
|
+
for (let j = 1; j <= a.length; j++) {
|
|
506
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
507
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return matrix[b.length][a.length];
|
|
515
|
+
}
|
|
516
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
517
|
+
// LLM Context Generation
|
|
518
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
519
|
+
/**
|
|
520
|
+
* Generate action catalog for LLM context
|
|
521
|
+
*/
|
|
522
|
+
export function generateActionCatalogForLLM(registry) {
|
|
523
|
+
const sections = [];
|
|
524
|
+
sections.push("# Available Actions (from API)\n");
|
|
525
|
+
// Group by category
|
|
526
|
+
const byCategory = new Map();
|
|
527
|
+
for (const action of registry.getAllActions()) {
|
|
528
|
+
const cat = action.category || "other";
|
|
529
|
+
if (!byCategory.has(cat))
|
|
530
|
+
byCategory.set(cat, []);
|
|
531
|
+
byCategory.get(cat).push(action);
|
|
532
|
+
}
|
|
533
|
+
for (const [category, actions] of byCategory) {
|
|
534
|
+
sections.push(`## ${category}\n`);
|
|
535
|
+
for (const action of actions) {
|
|
536
|
+
const inputs = Array.from(action.inputs.values())
|
|
537
|
+
.map(i => `${i.name}${i.required ? "*" : ""}: ${i.type}`)
|
|
538
|
+
.join(", ");
|
|
539
|
+
const outputs = Array.from(action.outputs.keys()).join(", ");
|
|
540
|
+
sections.push(`### ${action.name} (${action.version})`);
|
|
541
|
+
sections.push(`${action.description}`);
|
|
542
|
+
sections.push(`- Inputs: ${inputs || "none"}`);
|
|
543
|
+
sections.push(`- Outputs: ${outputs || "none"}\n`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return sections.join("\n");
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Generate template catalog for LLM context
|
|
550
|
+
*/
|
|
551
|
+
export function generateTemplateCatalogForLLM(registry) {
|
|
552
|
+
const sections = [];
|
|
553
|
+
sections.push("# Available Templates (from API)\n");
|
|
554
|
+
for (const template of registry.getAllTemplates()) {
|
|
555
|
+
sections.push(`## ${template.name} (${template.type})`);
|
|
556
|
+
sections.push(`ID: ${template.id}`);
|
|
557
|
+
if (template.description)
|
|
558
|
+
sections.push(template.description);
|
|
559
|
+
sections.push(`Default widgets: ${template.defaultWidgets.join(", ") || "none"}\n`);
|
|
560
|
+
}
|
|
561
|
+
return sections.join("\n");
|
|
562
|
+
}
|
|
563
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
564
|
+
// Singleton Registry
|
|
565
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
566
|
+
let _schemaRegistry = null;
|
|
567
|
+
/**
|
|
568
|
+
* Ensure schema registry is loaded with layered fallback:
|
|
569
|
+
* 1. API (live data from ListActions)
|
|
570
|
+
* 2. Bundled schema (resources/action-schema.json)
|
|
571
|
+
*/
|
|
572
|
+
export async function ensureSchemaRegistry(client) {
|
|
573
|
+
if (!_schemaRegistry) {
|
|
574
|
+
_schemaRegistry = new APISchemaRegistry();
|
|
575
|
+
// Use layered loading: API first, then bundled schema fallback
|
|
576
|
+
const bundlePath = resolveBundlePath();
|
|
577
|
+
await _schemaRegistry.loadWithFallback({
|
|
578
|
+
client,
|
|
579
|
+
bundlePath,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
return _schemaRegistry;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Resolve the path to the bundled action schema.
|
|
586
|
+
* Works both in development (src/) and production (dist/).
|
|
587
|
+
*/
|
|
588
|
+
function resolveBundlePath() {
|
|
589
|
+
const paths = [
|
|
590
|
+
// Development: relative to src/sdk/
|
|
591
|
+
new URL("../../resources/action-schema.json", import.meta.url).pathname,
|
|
592
|
+
// Production: relative to dist/sdk/
|
|
593
|
+
new URL("../../resources/action-schema.json", import.meta.url).pathname,
|
|
594
|
+
];
|
|
595
|
+
for (const p of paths) {
|
|
596
|
+
if (fs.existsSync(p))
|
|
597
|
+
return p;
|
|
598
|
+
}
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
export function getSchemaRegistry() {
|
|
602
|
+
return _schemaRegistry;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Reset the schema registry (for testing)
|
|
606
|
+
*/
|
|
607
|
+
export function resetSchemaRegistry() {
|
|
608
|
+
_schemaRegistry = null;
|
|
609
|
+
}
|