0nmcp 2.5.0 → 2.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.
- package/README.md +233 -695
- package/cli.js +9 -1
- package/crm/agent-studio.js +114 -0
- package/crm/funnels.js +90 -0
- package/crm/index.js +15 -0
- package/crm/knowledge-base.js +69 -0
- package/crm/objects.js +5 -69
- package/crm/saas.js +147 -0
- package/crm/users.js +5 -80
- package/crm/voice-ai.js +150 -0
- package/engine/index.js +338 -2
- package/engine/multi-ai.js +525 -0
- package/engine/plugin-builder.js +578 -0
- package/engine/plugin-registry.js +419 -0
- package/engine/plugin.js +448 -0
- package/engine/training-feed.js +520 -0
- package/engine/training.js +875 -0
- package/index.js +9 -1
- package/lib/stats.json +1 -1
- package/package.json +12 -2
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Plugin Builder (0nEngine Core)
|
|
3
|
+
// ============================================================
|
|
4
|
+
// The factory that reads catalog.js + fields.js and auto-generates
|
|
5
|
+
// fully operational service plugins. Each plugin gets:
|
|
6
|
+
// - .0n field resolution (canonical → service-specific)
|
|
7
|
+
// - All catalog endpoints as executable methods
|
|
8
|
+
// - Auth header injection
|
|
9
|
+
// - Rate limiting
|
|
10
|
+
// - MCP tool schema generation
|
|
11
|
+
//
|
|
12
|
+
// This is the 0nEngine — the bridge between the .0n Field Standard
|
|
13
|
+
// and the Service Catalog. Write once in .0n, run on any service.
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// import { PluginBuilder } from './plugin-builder.js'
|
|
17
|
+
//
|
|
18
|
+
// const builder = new PluginBuilder()
|
|
19
|
+
// const stripe = builder.build('stripe')
|
|
20
|
+
// stripe.connect({ apiKey: 'sk_live_...' })
|
|
21
|
+
// const result = await stripe.execute('create_customer', {
|
|
22
|
+
// 'email.0n': 'mike@rocketopp.com',
|
|
23
|
+
// 'fullname.0n': 'Mike Mento'
|
|
24
|
+
// })
|
|
25
|
+
//
|
|
26
|
+
// // Build ALL catalog services at once
|
|
27
|
+
// const plugins = builder.buildAll()
|
|
28
|
+
//
|
|
29
|
+
// // Build a custom plugin from a .0n plugin definition
|
|
30
|
+
// const custom = builder.buildFromSpec(pluginSpec)
|
|
31
|
+
//
|
|
32
|
+
// // Generate a plugin spec for a new service
|
|
33
|
+
// const spec = builder.generate({
|
|
34
|
+
// key: 'acme',
|
|
35
|
+
// name: 'Acme API',
|
|
36
|
+
// type: 'crm',
|
|
37
|
+
// baseUrl: 'https://api.acme.com/v1',
|
|
38
|
+
// authType: 'api_key',
|
|
39
|
+
// credentialKeys: ['apiKey'],
|
|
40
|
+
// endpoints: { ... }
|
|
41
|
+
// })
|
|
42
|
+
// ============================================================
|
|
43
|
+
|
|
44
|
+
import { SERVICE_CATALOG } from "../catalog.js";
|
|
45
|
+
import { resolveFields, reverseResolve, CANONICAL_FIELDS, listFields, getServiceMappings } from "../fields.js";
|
|
46
|
+
import { Plugin } from "./plugin.js";
|
|
47
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
48
|
+
import { join } from "path";
|
|
49
|
+
import { homedir } from "os";
|
|
50
|
+
|
|
51
|
+
const PLUGINS_DIR = join(homedir(), ".0n", "plugins");
|
|
52
|
+
|
|
53
|
+
export class PluginBuilder {
|
|
54
|
+
/**
|
|
55
|
+
* @param {object} [options]
|
|
56
|
+
* @param {object} [options.catalog] — Override SERVICE_CATALOG
|
|
57
|
+
* @param {object} [options.fields] — Override CANONICAL_FIELDS
|
|
58
|
+
* @param {number} [options.defaultRateLimit] — Default requests/sec per plugin
|
|
59
|
+
*/
|
|
60
|
+
constructor(options = {}) {
|
|
61
|
+
this.catalog = options.catalog || SERVICE_CATALOG;
|
|
62
|
+
this.fields = options.fields || CANONICAL_FIELDS;
|
|
63
|
+
this.defaultRateLimit = options.defaultRateLimit || 10;
|
|
64
|
+
|
|
65
|
+
// Cache built plugins
|
|
66
|
+
this._cache = new Map();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Build from Catalog ────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a Plugin from a catalog entry.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} serviceKey — Service key from SERVICE_CATALOG
|
|
75
|
+
* @param {object} [options]
|
|
76
|
+
* @param {object} [options.credentials] — Pre-load credentials
|
|
77
|
+
* @param {number} [options.rateLimit] — Override rate limit
|
|
78
|
+
* @returns {Plugin}
|
|
79
|
+
*/
|
|
80
|
+
build(serviceKey, options = {}) {
|
|
81
|
+
// Check cache first
|
|
82
|
+
const cacheKey = `catalog:${serviceKey}`;
|
|
83
|
+
if (this._cache.has(cacheKey) && !options.credentials) {
|
|
84
|
+
return this._cache.get(cacheKey);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const entry = this.catalog[serviceKey];
|
|
88
|
+
if (!entry) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Unknown service: "${serviceKey}". Available: ${Object.keys(this.catalog).join(", ")}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const plugin = new Plugin(serviceKey, entry, {
|
|
95
|
+
resolveFields,
|
|
96
|
+
reverseResolve,
|
|
97
|
+
credentials: options.credentials || null,
|
|
98
|
+
rateLimit: options.rateLimit || this.defaultRateLimit,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!options.credentials) {
|
|
102
|
+
this._cache.set(cacheKey, plugin);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return plugin;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build ALL catalog services as plugins.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} [options]
|
|
112
|
+
* @param {object} [options.connections] — Map of { serviceKey: credentials }
|
|
113
|
+
* @returns {Map<string, Plugin>}
|
|
114
|
+
*/
|
|
115
|
+
buildAll(options = {}) {
|
|
116
|
+
const plugins = new Map();
|
|
117
|
+
const connections = options.connections || {};
|
|
118
|
+
|
|
119
|
+
for (const serviceKey of Object.keys(this.catalog)) {
|
|
120
|
+
const plugin = this.build(serviceKey, {
|
|
121
|
+
credentials: connections[serviceKey] || null,
|
|
122
|
+
});
|
|
123
|
+
plugins.set(serviceKey, plugin);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return plugins;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build plugins only for connected services.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} connections — Map of { serviceKey: credentials }
|
|
133
|
+
* @returns {Map<string, Plugin>}
|
|
134
|
+
*/
|
|
135
|
+
buildConnected(connections) {
|
|
136
|
+
const plugins = new Map();
|
|
137
|
+
|
|
138
|
+
for (const [serviceKey, credentials] of Object.entries(connections)) {
|
|
139
|
+
if (!this.catalog[serviceKey]) continue;
|
|
140
|
+
const plugin = this.build(serviceKey, { credentials });
|
|
141
|
+
plugins.set(serviceKey, plugin);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return plugins;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Build from Spec ───────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build a Plugin from a .0n plugin spec (custom service definition).
|
|
151
|
+
* This is how users define their own services beyond the catalog.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} spec — Plugin specification
|
|
154
|
+
* @param {string} spec.service — Service key
|
|
155
|
+
* @param {string} spec.name — Display name
|
|
156
|
+
* @param {string} spec.type — Category type
|
|
157
|
+
* @param {string} spec.baseUrl — API base URL
|
|
158
|
+
* @param {string} spec.authType — Auth type (api_key, oauth, basic)
|
|
159
|
+
* @param {string[]} spec.credentialKeys — Required credential keys
|
|
160
|
+
* @param {object} spec.endpoints — Endpoint definitions
|
|
161
|
+
* @param {object[]} [spec.capabilities] — Capability list
|
|
162
|
+
* @param {Function|string} [spec.authHeader] — Auth header builder
|
|
163
|
+
* @param {object} [spec.fieldMappings] — Custom .0n field mappings
|
|
164
|
+
* @returns {Plugin}
|
|
165
|
+
*/
|
|
166
|
+
buildFromSpec(spec, options = {}) {
|
|
167
|
+
if (!spec.service || !spec.baseUrl || !spec.endpoints) {
|
|
168
|
+
throw new Error("Plugin spec requires: service, baseUrl, endpoints");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Build auth header function
|
|
172
|
+
let authHeaderFn;
|
|
173
|
+
if (typeof spec.authHeader === "function") {
|
|
174
|
+
authHeaderFn = spec.authHeader;
|
|
175
|
+
} else if (spec.authType === "api_key") {
|
|
176
|
+
const keyName = spec.credentialKeys?.[0] || "apiKey";
|
|
177
|
+
authHeaderFn = (creds) => ({
|
|
178
|
+
Authorization: `Bearer ${creds[keyName]}`,
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
});
|
|
181
|
+
} else if (spec.authType === "basic") {
|
|
182
|
+
authHeaderFn = (creds) => ({
|
|
183
|
+
Authorization: `Basic ${Buffer.from(`${creds.username}:${creds.password}`).toString("base64")}`,
|
|
184
|
+
"Content-Type": "application/json",
|
|
185
|
+
});
|
|
186
|
+
} else if (spec.authType === "oauth") {
|
|
187
|
+
authHeaderFn = (creds) => ({
|
|
188
|
+
Authorization: `Bearer ${creds.access_token}`,
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
authHeaderFn = () => ({ "Content-Type": "application/json" });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Inject custom field mappings into the resolver
|
|
196
|
+
const customResolver = spec.fieldMappings
|
|
197
|
+
? this._buildCustomResolver(spec.service, spec.fieldMappings)
|
|
198
|
+
: resolveFields;
|
|
199
|
+
|
|
200
|
+
const catalogEntry = {
|
|
201
|
+
name: spec.name || spec.service,
|
|
202
|
+
type: spec.type || "custom",
|
|
203
|
+
description: spec.description || `Custom plugin: ${spec.service}`,
|
|
204
|
+
baseUrl: spec.baseUrl,
|
|
205
|
+
authType: spec.authType || "api_key",
|
|
206
|
+
credentialKeys: spec.credentialKeys || [],
|
|
207
|
+
capabilities: spec.capabilities || [],
|
|
208
|
+
endpoints: spec.endpoints,
|
|
209
|
+
authHeader: authHeaderFn,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return new Plugin(spec.service, catalogEntry, {
|
|
213
|
+
resolveFields: customResolver,
|
|
214
|
+
reverseResolve,
|
|
215
|
+
credentials: options.credentials || null,
|
|
216
|
+
rateLimit: options.rateLimit || this.defaultRateLimit,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Build a Plugin from a .0n plugin file on disk.
|
|
222
|
+
*/
|
|
223
|
+
buildFromFile(filePath) {
|
|
224
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
225
|
+
const spec = JSON.parse(raw);
|
|
226
|
+
|
|
227
|
+
if (spec.$0n?.type !== "plugin") {
|
|
228
|
+
throw new Error(`File is not a .0n plugin: ${filePath}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return this.buildFromSpec(spec);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Plugin Generation ─────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generate a new plugin spec from a minimal definition.
|
|
238
|
+
* Auto-infers capabilities from endpoints, adds .0n field mappings.
|
|
239
|
+
*
|
|
240
|
+
* @param {object} def — Minimal service definition
|
|
241
|
+
* @returns {object} — Complete .0n plugin spec
|
|
242
|
+
*/
|
|
243
|
+
generate(def) {
|
|
244
|
+
if (!def.key || !def.baseUrl || !def.endpoints) {
|
|
245
|
+
throw new Error("generate() requires: key, baseUrl, endpoints");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Auto-infer capabilities from endpoint names
|
|
249
|
+
const capabilities = this._inferCapabilities(def.endpoints);
|
|
250
|
+
|
|
251
|
+
// Auto-infer field mappings from endpoint body schemas
|
|
252
|
+
const fieldMappings = def.fieldMappings || this._inferFieldMappings(def.endpoints);
|
|
253
|
+
|
|
254
|
+
const spec = {
|
|
255
|
+
$0n: {
|
|
256
|
+
type: "plugin",
|
|
257
|
+
version: "1.0.0",
|
|
258
|
+
name: def.name || def.key,
|
|
259
|
+
description: def.description || `Custom plugin for ${def.key}`,
|
|
260
|
+
author: def.author || "0nEngine",
|
|
261
|
+
created: new Date().toISOString(),
|
|
262
|
+
},
|
|
263
|
+
service: def.key,
|
|
264
|
+
serviceType: def.type || "custom",
|
|
265
|
+
description: def.description || `Custom plugin for ${def.key}`,
|
|
266
|
+
baseUrl: def.baseUrl,
|
|
267
|
+
authType: def.authType || "api_key",
|
|
268
|
+
credentialKeys: def.credentialKeys || ["apiKey"],
|
|
269
|
+
capabilities,
|
|
270
|
+
endpoints: def.endpoints,
|
|
271
|
+
fieldMappings,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return spec;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Generate a plugin spec and save it as a .0n file.
|
|
279
|
+
*/
|
|
280
|
+
generateAndSave(def, outputPath) {
|
|
281
|
+
const spec = this.generate(def);
|
|
282
|
+
|
|
283
|
+
const savePath = outputPath || join(PLUGINS_DIR, `${def.key}.0n.json`);
|
|
284
|
+
const dir = join(savePath, "..");
|
|
285
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
286
|
+
|
|
287
|
+
writeFileSync(savePath, JSON.stringify(spec, null, 2));
|
|
288
|
+
|
|
289
|
+
return { spec, path: savePath };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Bulk MCP Tool Registration ────────────────────────────
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Generate MCP tool definitions for all catalog services.
|
|
296
|
+
* Returns a flat array of tool schemas ready for McpServer.tool().
|
|
297
|
+
*
|
|
298
|
+
* @param {object} [options]
|
|
299
|
+
* @param {string[]} [options.services] — Only these services (default: all)
|
|
300
|
+
* @returns {{ tools: object[], count: number, services: string[] }}
|
|
301
|
+
*/
|
|
302
|
+
generateAllTools(options = {}) {
|
|
303
|
+
const serviceKeys = options.services || Object.keys(this.catalog);
|
|
304
|
+
const allTools = [];
|
|
305
|
+
const includedServices = [];
|
|
306
|
+
|
|
307
|
+
for (const key of serviceKeys) {
|
|
308
|
+
if (!this.catalog[key]) continue;
|
|
309
|
+
const plugin = this.build(key);
|
|
310
|
+
const tools = plugin.toMcpTools();
|
|
311
|
+
allTools.push(...tools);
|
|
312
|
+
includedServices.push(key);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
tools: allTools,
|
|
317
|
+
count: allTools.length,
|
|
318
|
+
services: includedServices,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Field Analysis ────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get field coverage report — which .0n fields are mapped for which services.
|
|
326
|
+
*/
|
|
327
|
+
getFieldCoverage() {
|
|
328
|
+
const fields = listFields();
|
|
329
|
+
const services = Object.keys(this.catalog);
|
|
330
|
+
|
|
331
|
+
const coverage = fields.map(f => {
|
|
332
|
+
const mapped = services.filter(s => {
|
|
333
|
+
const canonical = this.fields[f.field];
|
|
334
|
+
return canonical?.mappings?.[s] !== undefined;
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
field: f.field,
|
|
338
|
+
label: f.label,
|
|
339
|
+
type: f.type,
|
|
340
|
+
mappedServices: mapped.length,
|
|
341
|
+
totalServices: services.length,
|
|
342
|
+
coverage: Math.round((mapped.length / services.length) * 100),
|
|
343
|
+
services: mapped,
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const totalMappings = coverage.reduce((sum, f) => sum + f.mappedServices, 0);
|
|
348
|
+
const maxMappings = fields.length * services.length;
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
fields: coverage,
|
|
352
|
+
summary: {
|
|
353
|
+
totalFields: fields.length,
|
|
354
|
+
totalServices: services.length,
|
|
355
|
+
totalMappings,
|
|
356
|
+
maxPossibleMappings: maxMappings,
|
|
357
|
+
overallCoverage: Math.round((totalMappings / maxMappings) * 100),
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get all .0n field mappings for a specific service.
|
|
364
|
+
*/
|
|
365
|
+
getServiceFields(serviceKey) {
|
|
366
|
+
return getServiceMappings(serviceKey);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Show what a .0n data object looks like after resolution for a service.
|
|
371
|
+
* Useful for debugging field mappings.
|
|
372
|
+
*/
|
|
373
|
+
previewResolution(data, serviceKey) {
|
|
374
|
+
const resolved = resolveFields(data, serviceKey);
|
|
375
|
+
return {
|
|
376
|
+
input: data,
|
|
377
|
+
service: serviceKey,
|
|
378
|
+
resolved,
|
|
379
|
+
fieldCount: Object.keys(data).filter(k => k.endsWith(".0n")).length,
|
|
380
|
+
resolvedCount: Object.keys(resolved).length,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Stats ─────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get aggregated stats across all cached plugins.
|
|
388
|
+
*/
|
|
389
|
+
getStats() {
|
|
390
|
+
const stats = {
|
|
391
|
+
cachedPlugins: this._cache.size,
|
|
392
|
+
catalogServices: Object.keys(this.catalog).length,
|
|
393
|
+
totalEndpoints: 0,
|
|
394
|
+
totalCapabilities: 0,
|
|
395
|
+
serviceBreakdown: {},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
for (const [key, entry] of Object.entries(this.catalog)) {
|
|
399
|
+
const endpointCount = Object.keys(entry.endpoints || {}).length;
|
|
400
|
+
const capCount = entry.capabilities?.length || 0;
|
|
401
|
+
stats.totalEndpoints += endpointCount;
|
|
402
|
+
stats.totalCapabilities += capCount;
|
|
403
|
+
stats.serviceBreakdown[key] = {
|
|
404
|
+
name: entry.name,
|
|
405
|
+
type: entry.type,
|
|
406
|
+
endpoints: endpointCount,
|
|
407
|
+
capabilities: capCount,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return stats;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── Private Helpers ───────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Build a custom field resolver that merges spec-defined mappings
|
|
418
|
+
* with the canonical CANONICAL_FIELDS.
|
|
419
|
+
*/
|
|
420
|
+
_buildCustomResolver(serviceKey, fieldMappings) {
|
|
421
|
+
return (data, service) => {
|
|
422
|
+
if (service !== serviceKey) return resolveFields(data, service);
|
|
423
|
+
|
|
424
|
+
const resolved = {};
|
|
425
|
+
for (const [key, value] of Object.entries(data)) {
|
|
426
|
+
if (!key.endsWith(".0n")) {
|
|
427
|
+
resolved[key] = value;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check custom mappings first
|
|
432
|
+
if (fieldMappings[key]) {
|
|
433
|
+
const target = fieldMappings[key];
|
|
434
|
+
if (typeof target === "string") {
|
|
435
|
+
resolved[target] = value;
|
|
436
|
+
} else if (Array.isArray(target) && typeof value === "string") {
|
|
437
|
+
const parts = value.split(" ");
|
|
438
|
+
resolved[target[0]] = parts[0] || "";
|
|
439
|
+
if (target[1]) resolved[target[1]] = parts.slice(1).join(" ") || "";
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Fall back to canonical resolver
|
|
445
|
+
const canonical = CANONICAL_FIELDS[key];
|
|
446
|
+
if (canonical?.mappings?.[serviceKey]) {
|
|
447
|
+
const mapping = canonical.mappings[serviceKey];
|
|
448
|
+
if (typeof mapping === "string") {
|
|
449
|
+
resolved[mapping] = value;
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
// Strip .0n suffix as fallback
|
|
453
|
+
resolved[key.replace(".0n", "")] = value;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return resolved;
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Infer capabilities from endpoint names.
|
|
462
|
+
* Groups by entity and extracts actions.
|
|
463
|
+
*/
|
|
464
|
+
_inferCapabilities(endpoints) {
|
|
465
|
+
const groups = {};
|
|
466
|
+
|
|
467
|
+
for (const name of Object.keys(endpoints)) {
|
|
468
|
+
// Pattern: action_entity (e.g., create_customer, list_orders)
|
|
469
|
+
const parts = name.split("_");
|
|
470
|
+
if (parts.length < 2) continue;
|
|
471
|
+
|
|
472
|
+
const action = parts[0];
|
|
473
|
+
let entity = parts.slice(1).join("_");
|
|
474
|
+
// Normalize plural → singular for grouping (contacts → contact, etc.)
|
|
475
|
+
if (entity.endsWith("ies")) {
|
|
476
|
+
entity = entity.slice(0, -3) + "y"; // e.g., entries → entry
|
|
477
|
+
} else if (entity.endsWith("ses") || entity.endsWith("xes") || entity.endsWith("zes")) {
|
|
478
|
+
entity = entity.slice(0, -2); // e.g., boxes → box
|
|
479
|
+
} else if (entity.endsWith("s") && !entity.endsWith("ss") && !entity.endsWith("us")) {
|
|
480
|
+
entity = entity.slice(0, -1); // e.g., contacts → contact
|
|
481
|
+
}
|
|
482
|
+
const capName = `manage_${entity}`;
|
|
483
|
+
|
|
484
|
+
if (!groups[capName]) {
|
|
485
|
+
groups[capName] = { name: capName, actions: [], description: "" };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!groups[capName].actions.includes(action)) {
|
|
489
|
+
groups[capName].actions.push(action);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Build descriptions
|
|
494
|
+
for (const cap of Object.values(groups)) {
|
|
495
|
+
cap.description = `${cap.actions.join(", ")} ${cap.name.replace("manage_", "")}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return Object.values(groups);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Infer .0n field mappings from endpoint body schemas.
|
|
503
|
+
* Tries to match body keys to known canonical fields.
|
|
504
|
+
*/
|
|
505
|
+
_inferFieldMappings(endpoints) {
|
|
506
|
+
const mappings = {};
|
|
507
|
+
const knownFields = new Set();
|
|
508
|
+
|
|
509
|
+
// Collect all body keys
|
|
510
|
+
for (const def of Object.values(endpoints)) {
|
|
511
|
+
if (!def.body) continue;
|
|
512
|
+
for (const key of Object.keys(def.body)) {
|
|
513
|
+
knownFields.add(key);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Try to match against canonical fields
|
|
518
|
+
for (const [canonical, fieldDef] of Object.entries(CANONICAL_FIELDS)) {
|
|
519
|
+
// Check if any service mapping value matches a body key
|
|
520
|
+
for (const mapping of Object.values(fieldDef.mappings)) {
|
|
521
|
+
if (typeof mapping === "string" && knownFields.has(mapping)) {
|
|
522
|
+
mappings[canonical] = mapping;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Also check the stripped canonical name
|
|
528
|
+
const stripped = canonical.replace(".0n", "");
|
|
529
|
+
if (knownFields.has(stripped)) {
|
|
530
|
+
mappings[canonical] = stripped;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return Object.keys(mappings).length > 0 ? mappings : undefined;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ── Singleton convenience ─────────────────────────────────
|
|
539
|
+
|
|
540
|
+
let _defaultBuilder = null;
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get the default PluginBuilder singleton.
|
|
544
|
+
*/
|
|
545
|
+
export function getPluginBuilder() {
|
|
546
|
+
if (!_defaultBuilder) {
|
|
547
|
+
_defaultBuilder = new PluginBuilder();
|
|
548
|
+
}
|
|
549
|
+
return _defaultBuilder;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Quick-build a plugin from catalog.
|
|
554
|
+
*/
|
|
555
|
+
export function buildPlugin(serviceKey, options) {
|
|
556
|
+
return getPluginBuilder().build(serviceKey, options);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Quick-build all catalog plugins.
|
|
561
|
+
*/
|
|
562
|
+
export function buildAllPlugins(options) {
|
|
563
|
+
return getPluginBuilder().buildAll(options);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Quick-build from a .0n spec.
|
|
568
|
+
*/
|
|
569
|
+
export function buildFromSpec(spec, options) {
|
|
570
|
+
return getPluginBuilder().buildFromSpec(spec, options);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Quick-generate a plugin spec.
|
|
575
|
+
*/
|
|
576
|
+
export function generatePluginSpec(def) {
|
|
577
|
+
return getPluginBuilder().generate(def);
|
|
578
|
+
}
|