@majkapp/plugin-kit 1.3.8 → 2.0.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/dist/generator/generator.js +1 -3
- package/dist/index.d.ts +12 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/majk-interface-types.d.ts +14 -14
- package/dist/majk-interface-types.d.ts.map +1 -1
- package/dist/plugin-kit.d.ts +8 -3
- package/dist/plugin-kit.d.ts.map +1 -1
- package/dist/plugin-kit.js +109 -837
- package/dist/registry.d.ts +15 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +41 -1
- package/dist/resource-provider.d.ts +28 -0
- package/dist/resource-provider.d.ts.map +1 -0
- package/dist/resource-provider.js +86 -0
- package/dist/types.d.ts +145 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/transports.d.ts +0 -59
- package/dist/transports.d.ts.map +0 -1
- package/dist/transports.js +0 -171
package/dist/plugin-kit.js
CHANGED
|
@@ -4,11 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.definePlugin = definePlugin;
|
|
7
|
-
const http_1 = __importDefault(require("http"));
|
|
8
7
|
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
8
|
const registry_1 = require("./registry");
|
|
11
|
-
const transports_1 = require("./transports");
|
|
12
9
|
/**
|
|
13
10
|
* Conditional logging - suppressed in extract mode
|
|
14
11
|
*/
|
|
@@ -153,332 +150,16 @@ function validateEntity(entityType, entity) {
|
|
|
153
150
|
}
|
|
154
151
|
return errors;
|
|
155
152
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Convert JSON Schema to OpenAPI Schema
|
|
158
|
-
*/
|
|
159
|
-
function jsonSchemaToOpenApi(schema) {
|
|
160
|
-
// OpenAPI doesn't use the $schema property
|
|
161
|
-
const { ...openApiSchema } = schema;
|
|
162
|
-
return openApiSchema;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Generate OpenAPI specification from routes
|
|
166
|
-
*/
|
|
167
|
-
function generateOpenApiSpec(pluginId, pluginName, pluginVersion, routes, baseUrl) {
|
|
168
|
-
const paths = {};
|
|
169
|
-
for (const route of routes) {
|
|
170
|
-
const openApiPath = route.path.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
|
|
171
|
-
if (!paths[openApiPath]) {
|
|
172
|
-
paths[openApiPath] = {};
|
|
173
|
-
}
|
|
174
|
-
const operation = {
|
|
175
|
-
operationId: `${pluginId}_${route.name.replace(/\s+/g, '_')}`,
|
|
176
|
-
summary: route.name,
|
|
177
|
-
description: route.description,
|
|
178
|
-
tags: route.tags || [pluginId],
|
|
179
|
-
deprecated: route.deprecated
|
|
180
|
-
};
|
|
181
|
-
// Add parameters from request schema
|
|
182
|
-
if (route.requestSchema) {
|
|
183
|
-
const parameters = [];
|
|
184
|
-
// Path parameters
|
|
185
|
-
if (route.requestSchema.params) {
|
|
186
|
-
const paramProps = route.requestSchema.params.properties || {};
|
|
187
|
-
for (const [name, schema] of Object.entries(paramProps)) {
|
|
188
|
-
parameters.push({
|
|
189
|
-
name,
|
|
190
|
-
in: 'path',
|
|
191
|
-
required: route.requestSchema.params.required?.includes(name) ?? true,
|
|
192
|
-
schema: jsonSchemaToOpenApi(schema),
|
|
193
|
-
description: schema.description
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// Query parameters
|
|
198
|
-
if (route.requestSchema.query) {
|
|
199
|
-
const queryProps = route.requestSchema.query.properties || {};
|
|
200
|
-
for (const [name, schema] of Object.entries(queryProps)) {
|
|
201
|
-
parameters.push({
|
|
202
|
-
name,
|
|
203
|
-
in: 'query',
|
|
204
|
-
required: route.requestSchema.query.required?.includes(name) ?? false,
|
|
205
|
-
schema: jsonSchemaToOpenApi(schema),
|
|
206
|
-
description: schema.description
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (parameters.length > 0) {
|
|
211
|
-
operation.parameters = parameters;
|
|
212
|
-
}
|
|
213
|
-
// Request body
|
|
214
|
-
if (route.requestSchema.body) {
|
|
215
|
-
operation.requestBody = {
|
|
216
|
-
required: true,
|
|
217
|
-
content: {
|
|
218
|
-
'application/json': {
|
|
219
|
-
schema: jsonSchemaToOpenApi(route.requestSchema.body)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
// Response schema
|
|
226
|
-
operation.responses = {
|
|
227
|
-
'200': {
|
|
228
|
-
description: 'Successful response',
|
|
229
|
-
content: route.responseSchema ? {
|
|
230
|
-
'application/json': {
|
|
231
|
-
schema: jsonSchemaToOpenApi(route.responseSchema)
|
|
232
|
-
}
|
|
233
|
-
} : {}
|
|
234
|
-
},
|
|
235
|
-
'400': {
|
|
236
|
-
description: 'Bad request'
|
|
237
|
-
},
|
|
238
|
-
'500': {
|
|
239
|
-
description: 'Internal server error'
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
paths[openApiPath][route.method.toLowerCase()] = operation;
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
openapi: '3.0.0',
|
|
246
|
-
info: {
|
|
247
|
-
title: `${pluginName} API`,
|
|
248
|
-
version: pluginVersion,
|
|
249
|
-
description: `API documentation for the ${pluginName} MAJK plugin`
|
|
250
|
-
},
|
|
251
|
-
servers: [
|
|
252
|
-
{
|
|
253
|
-
url: baseUrl,
|
|
254
|
-
description: 'Plugin API server'
|
|
255
|
-
}
|
|
256
|
-
],
|
|
257
|
-
paths,
|
|
258
|
-
components: {
|
|
259
|
-
schemas: {}
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Path parameter regex builder
|
|
265
|
-
*/
|
|
266
|
-
function pathToRegex(p) {
|
|
267
|
-
const keys = [];
|
|
268
|
-
log(`[pathToRegex] Input path: ${p}`);
|
|
269
|
-
// First, escape special regex characters EXCEPT the colon (for path params)
|
|
270
|
-
const escaped = p.replace(/([.+*?=^!${}()[\]|\\])/g, '\\$1');
|
|
271
|
-
log(`[pathToRegex] After escaping: ${escaped}`);
|
|
272
|
-
// Then, replace path parameters with capturing groups
|
|
273
|
-
const pattern = escaped.replace(/\/:([A-Za-z0-9_]+)/g, (_m, k) => {
|
|
274
|
-
keys.push(k);
|
|
275
|
-
log(`[pathToRegex] Found param: ${k}`);
|
|
276
|
-
return '/([^/]+)';
|
|
277
|
-
});
|
|
278
|
-
log(`[pathToRegex] Final pattern: ${pattern}`);
|
|
279
|
-
log(`[pathToRegex] Keys: [${keys.join(', ')}]`);
|
|
280
|
-
const regex = new RegExp(`^${pattern}$`);
|
|
281
|
-
log(`[pathToRegex] Final regex: ${regex}`);
|
|
282
|
-
return { regex, keys };
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* CORS headers
|
|
286
|
-
*/
|
|
287
|
-
function corsHeaders(extra = {}) {
|
|
288
|
-
return {
|
|
289
|
-
'Access-Control-Allow-Origin': '*',
|
|
290
|
-
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
291
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
292
|
-
...extra
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Read request body
|
|
297
|
-
*/
|
|
298
|
-
async function readBody(req) {
|
|
299
|
-
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
|
|
300
|
-
return undefined;
|
|
301
|
-
}
|
|
302
|
-
return new Promise((resolve) => {
|
|
303
|
-
let data = '';
|
|
304
|
-
req.on('data', (chunk) => (data += chunk.toString()));
|
|
305
|
-
req.on('end', () => {
|
|
306
|
-
if (!data) {
|
|
307
|
-
resolve(undefined);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
try {
|
|
311
|
-
resolve(JSON.parse(data));
|
|
312
|
-
}
|
|
313
|
-
catch {
|
|
314
|
-
resolve(data);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Create response wrapper
|
|
321
|
-
*/
|
|
322
|
-
function makeResponse(res) {
|
|
323
|
-
return {
|
|
324
|
-
status(code) {
|
|
325
|
-
res.statusCode = code;
|
|
326
|
-
return this;
|
|
327
|
-
},
|
|
328
|
-
setHeader(key, value) {
|
|
329
|
-
res.setHeader(key, value);
|
|
330
|
-
},
|
|
331
|
-
json(data) {
|
|
332
|
-
if (!res.headersSent) {
|
|
333
|
-
res.writeHead(res.statusCode || 200, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
334
|
-
}
|
|
335
|
-
res.end(JSON.stringify(data));
|
|
336
|
-
},
|
|
337
|
-
send(data) {
|
|
338
|
-
if (!res.headersSent) {
|
|
339
|
-
res.writeHead(res.statusCode || 200, corsHeaders());
|
|
340
|
-
}
|
|
341
|
-
res.end(data);
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Content type helper
|
|
347
|
-
*/
|
|
348
|
-
function getContentType(filePath) {
|
|
349
|
-
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
350
|
-
const types = {
|
|
351
|
-
'.html': 'text/html',
|
|
352
|
-
'.js': 'application/javascript',
|
|
353
|
-
'.css': 'text/css',
|
|
354
|
-
'.json': 'application/json',
|
|
355
|
-
'.png': 'image/png',
|
|
356
|
-
'.jpg': 'image/jpeg',
|
|
357
|
-
'.jpeg': 'image/jpeg',
|
|
358
|
-
'.svg': 'image/svg+xml',
|
|
359
|
-
'.woff': 'font/woff',
|
|
360
|
-
'.woff2': 'font/woff2',
|
|
361
|
-
'.ttf': 'font/ttf'
|
|
362
|
-
};
|
|
363
|
-
return types[ext] || 'application/octet-stream';
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Serve React SPA
|
|
367
|
-
*/
|
|
368
|
-
function serveSpa(ui, req, res, ctx) {
|
|
369
|
-
const url = new URL(req.url, 'http://localhost');
|
|
370
|
-
const rel = url.pathname.substring(ui.base.length) || '/';
|
|
371
|
-
const distPath = path_1.default.join(ctx.pluginRoot, ui.appDir);
|
|
372
|
-
let filePath = path_1.default.join(distPath, rel);
|
|
373
|
-
if (filePath.endsWith('/')) {
|
|
374
|
-
filePath = path_1.default.join(filePath, 'index.html');
|
|
375
|
-
}
|
|
376
|
-
// Security: prevent path traversal
|
|
377
|
-
const normalizedPath = path_1.default.resolve(filePath);
|
|
378
|
-
if (!normalizedPath.startsWith(path_1.default.resolve(distPath))) {
|
|
379
|
-
res.writeHead(403, corsHeaders({ 'Content-Type': 'text/plain' }));
|
|
380
|
-
res.end('Forbidden: Path traversal detected');
|
|
381
|
-
ctx.logger.warn(`Path traversal attempt blocked: ${rel}`);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const exists = fs_1.default.existsSync(filePath) && fs_1.default.statSync(filePath).isFile();
|
|
385
|
-
const targetFile = exists ? filePath : path_1.default.join(distPath, 'index.html');
|
|
386
|
-
if (!fs_1.default.existsSync(targetFile)) {
|
|
387
|
-
res.writeHead(404, corsHeaders({ 'Content-Type': 'text/plain' }));
|
|
388
|
-
res.end('Not found');
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
let content = fs_1.default.readFileSync(targetFile);
|
|
392
|
-
// Inject base URL for React apps and MCP DOM Agent
|
|
393
|
-
if (path_1.default.basename(targetFile) === 'index.html') {
|
|
394
|
-
const html = content.toString('utf-8');
|
|
395
|
-
// Inject configuration
|
|
396
|
-
const configInject = `<script>` +
|
|
397
|
-
`window.__MAJK_BASE_URL__=${JSON.stringify(ctx.http.baseUrl)};` +
|
|
398
|
-
`window.__MAJK_IFRAME_BASE__=${JSON.stringify(ui.base)};` +
|
|
399
|
-
`window.__MAJK_PLUGIN_ID__=${JSON.stringify(ctx.pluginId)};` +
|
|
400
|
-
`</script>`;
|
|
401
|
-
// Inject html2canvas library (inline from node_modules)
|
|
402
|
-
const html2canvasScript = BuiltPlugin.html2canvasScript
|
|
403
|
-
? `<script>${BuiltPlugin.html2canvasScript}</script>`
|
|
404
|
-
: '';
|
|
405
|
-
// Inject MCP DOM Agent script
|
|
406
|
-
const mcpScript = BuiltPlugin.mcpDomAgentScript
|
|
407
|
-
? `<script>${BuiltPlugin.mcpDomAgentScript}</script>`
|
|
408
|
-
: '';
|
|
409
|
-
// Debug logging
|
|
410
|
-
if (html2canvasScript) {
|
|
411
|
-
ctx.logger.debug(`[React SPA] Injecting html2canvas (${(BuiltPlugin.html2canvasScript?.length || 0) / 1024} KB)`);
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
ctx.logger.warn('[React SPA] html2canvas NOT injected - script not loaded');
|
|
415
|
-
}
|
|
416
|
-
if (mcpScript) {
|
|
417
|
-
ctx.logger.debug('[React SPA] Injecting MCP DOM Agent');
|
|
418
|
-
}
|
|
419
|
-
const allInjections = configInject + html2canvasScript + mcpScript;
|
|
420
|
-
const injected = html.replace('</head>', `${allInjections}</head>`);
|
|
421
|
-
content = Buffer.from(injected, 'utf-8');
|
|
422
|
-
}
|
|
423
|
-
res.writeHead(200, corsHeaders({ 'Content-Type': getContentType(targetFile) }));
|
|
424
|
-
res.end(content);
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Load html2canvas library from node_modules
|
|
428
|
-
*/
|
|
429
|
-
function loadHtml2CanvasScript() {
|
|
430
|
-
try {
|
|
431
|
-
// Try multiple possible paths for html2canvas
|
|
432
|
-
const possiblePaths = [
|
|
433
|
-
// When running from dist/ (after build)
|
|
434
|
-
path_1.default.join(__dirname, '..', 'node_modules', 'html2canvas', 'dist', 'html2canvas.min.js'),
|
|
435
|
-
// When running from src/ (development)
|
|
436
|
-
path_1.default.join(__dirname, '..', '..', 'node_modules', 'html2canvas', 'dist', 'html2canvas.min.js'),
|
|
437
|
-
// When installed as a dependency in user's project
|
|
438
|
-
path_1.default.join(__dirname, '..', '..', '..', 'html2canvas', 'dist', 'html2canvas.min.js'),
|
|
439
|
-
];
|
|
440
|
-
for (const scriptPath of possiblePaths) {
|
|
441
|
-
if (fs_1.default.existsSync(scriptPath)) {
|
|
442
|
-
log(`[html2canvas] Loaded from: ${scriptPath}`);
|
|
443
|
-
return fs_1.default.readFileSync(scriptPath, 'utf-8');
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
log('[html2canvas] Script file not found in any expected location');
|
|
447
|
-
return '';
|
|
448
|
-
}
|
|
449
|
-
catch (error) {
|
|
450
|
-
log(`[html2canvas] Failed to load script: ${error.message}`);
|
|
451
|
-
return '';
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* Load MCP DOM Agent script
|
|
456
|
-
*/
|
|
457
|
-
function loadMcpDomAgentScript() {
|
|
458
|
-
try {
|
|
459
|
-
const scriptPath = path_1.default.join(__dirname, 'mcp-dom-agent.js');
|
|
460
|
-
if (fs_1.default.existsSync(scriptPath)) {
|
|
461
|
-
return fs_1.default.readFileSync(scriptPath, 'utf-8');
|
|
462
|
-
}
|
|
463
|
-
// Fallback: return minimal inline version if file not found
|
|
464
|
-
log('[MCP DOM Agent] Script file not found, using minimal fallback');
|
|
465
|
-
return `(function(){if(window.__mcpDomAgent)return;window.__mcpDomAgent=true;console.log('[MCP DOM Agent] Minimal fallback loaded');})();`;
|
|
466
|
-
}
|
|
467
|
-
catch (error) {
|
|
468
|
-
log(`[MCP DOM Agent] Failed to load script: ${error.message}`);
|
|
469
|
-
return '';
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
153
|
/**
|
|
473
154
|
* Main plugin class built by the fluent API
|
|
474
155
|
*/
|
|
475
156
|
class BuiltPlugin {
|
|
476
157
|
constructor(id, name, version, capabilities, tools, apiRoutes, uiConfig, reactScreens, htmlScreens, wizard, settings, onReadyFn, healthCheckFn, userProvidedPluginRoot,
|
|
477
|
-
//
|
|
478
|
-
functionRegistry,
|
|
479
|
-
//
|
|
158
|
+
// Function registry
|
|
159
|
+
functionRegistry,
|
|
160
|
+
// Configurable entities
|
|
480
161
|
configurableEntities,
|
|
481
|
-
//
|
|
162
|
+
// Secret providers
|
|
482
163
|
secretProviders) {
|
|
483
164
|
this.capabilities = capabilities;
|
|
484
165
|
this.tools = tools;
|
|
@@ -494,13 +175,10 @@ class BuiltPlugin {
|
|
|
494
175
|
this.configurableEntities = configurableEntities;
|
|
495
176
|
this.secretProviders = secretProviders;
|
|
496
177
|
this.cleanups = [];
|
|
497
|
-
this.router = [];
|
|
498
|
-
this.transports = [];
|
|
499
178
|
this.id = id;
|
|
500
179
|
this.name = name;
|
|
501
180
|
this.version = version;
|
|
502
181
|
this.functionRegistry = functionRegistry;
|
|
503
|
-
this.transports = transports || [];
|
|
504
182
|
this.configurableEntities = configurableEntities || [];
|
|
505
183
|
this.secretProviders = secretProviders || [];
|
|
506
184
|
}
|
|
@@ -568,6 +246,19 @@ class BuiltPlugin {
|
|
|
568
246
|
}
|
|
569
247
|
return false;
|
|
570
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Get function provider (new async Plugin interface)
|
|
251
|
+
* Returns null if plugin has no functions
|
|
252
|
+
*/
|
|
253
|
+
async getFunctions() {
|
|
254
|
+
if (!this.functionRegistry) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
if (!this._functionProvider) {
|
|
258
|
+
this._functionProvider = new registry_1.FunctionProviderImpl(this.functionRegistry, this.context);
|
|
259
|
+
}
|
|
260
|
+
return this._functionProvider;
|
|
261
|
+
}
|
|
571
262
|
async createTool(toolName) {
|
|
572
263
|
const handler = this.tools.get(toolName);
|
|
573
264
|
if (!handler) {
|
|
@@ -605,77 +296,17 @@ class BuiltPlugin {
|
|
|
605
296
|
}
|
|
606
297
|
}
|
|
607
298
|
async onLoad(context) {
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
const effectivePluginRoot = this.userProvidedPluginRoot || context.pluginRoot;
|
|
612
|
-
this.context = {
|
|
613
|
-
...context,
|
|
614
|
-
pluginRoot: effectivePluginRoot
|
|
615
|
-
};
|
|
299
|
+
// NOTE: context.resources is now provided by MAJK (ResourceProvider abstraction)
|
|
300
|
+
// Plugin-kit no longer manages HTTP servers or file paths directly
|
|
301
|
+
this.context = context;
|
|
616
302
|
context.logger.info('═══════════════════════════════════════════════════════');
|
|
617
303
|
context.logger.info(`🚀 Loading Plugin: ${this.name} (${this.id}) v${this.version}`);
|
|
618
|
-
context.logger.info(`🔧 Plugin Kit Version: @majkapp/plugin-kit@
|
|
619
|
-
context.logger.info(`📍 Plugin Root: ${effectivePluginRoot}`);
|
|
620
|
-
if (this.userProvidedPluginRoot) {
|
|
621
|
-
context.logger.info(` (user-provided via .pluginRoot(__dirname))`);
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
context.logger.info(` (from MAJK context - consider using .pluginRoot(__dirname) for better reliability)`);
|
|
625
|
-
}
|
|
626
|
-
context.logger.info(`🌐 HTTP Port: ${context.http.port}`);
|
|
304
|
+
context.logger.info(`🔧 Plugin Kit Version: @majkapp/plugin-kit@2.0.0`);
|
|
627
305
|
context.logger.info(`🔗 Base URL: ${context.http.baseUrl}`);
|
|
628
306
|
context.logger.info('═══════════════════════════════════════════════════════');
|
|
629
|
-
// Load html2canvas and MCP DOM Agent scripts once for all plugins
|
|
630
|
-
if (!BuiltPlugin.html2canvasScript) {
|
|
631
|
-
BuiltPlugin.html2canvasScript = loadHtml2CanvasScript();
|
|
632
|
-
if (BuiltPlugin.html2canvasScript) {
|
|
633
|
-
const sizeKB = (BuiltPlugin.html2canvasScript.length / 1024).toFixed(2);
|
|
634
|
-
context.logger.info(`✅ html2canvas library loaded (${sizeKB} KB) - screenshot support enabled`);
|
|
635
|
-
}
|
|
636
|
-
else {
|
|
637
|
-
context.logger.warn('⚠️ html2canvas not found - screenshots will use fallback mode');
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (!BuiltPlugin.mcpDomAgentScript) {
|
|
641
|
-
BuiltPlugin.mcpDomAgentScript = loadMcpDomAgentScript();
|
|
642
|
-
if (BuiltPlugin.mcpDomAgentScript) {
|
|
643
|
-
context.logger.info('✅ MCP DOM Agent script loaded - will be injected into all screens');
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
// Validate file paths now that we have pluginRoot context
|
|
647
|
-
if (this.reactScreens.length > 0 && this.uiConfig) {
|
|
648
|
-
const indexPath = path_1.default.join(this.context.pluginRoot, this.uiConfig.appDir || '', 'index.html');
|
|
649
|
-
if (!fs_1.default.existsSync(indexPath)) {
|
|
650
|
-
throw new PluginRuntimeError(`React app not built: ${indexPath} does not exist. Run "npm run build" in your UI directory to build the React app.`, 'onLoad', { appDir: this.uiConfig.appDir, indexPath, pluginRoot: this.context.pluginRoot });
|
|
651
|
-
}
|
|
652
|
-
context.logger.info(`✅ React UI found at: ${indexPath}`);
|
|
653
|
-
}
|
|
654
|
-
// Validate HTML screen files
|
|
655
|
-
for (const screen of this.htmlScreens) {
|
|
656
|
-
if ('htmlFile' in screen) {
|
|
657
|
-
const filePath = path_1.default.join(this.context.pluginRoot, screen.htmlFile);
|
|
658
|
-
if (!fs_1.default.existsSync(filePath)) {
|
|
659
|
-
throw new PluginRuntimeError(`HTML screen file not found: ${screen.htmlFile}. Create the HTML file at ${filePath} or fix the path.`, 'onLoad', { screen: screen.id, file: screen.htmlFile, resolvedPath: filePath, pluginRoot: this.context.pluginRoot });
|
|
660
|
-
}
|
|
661
|
-
context.logger.info(`✅ HTML screen file found: ${screen.htmlFile}`);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
307
|
try {
|
|
665
|
-
|
|
666
|
-
// Initialize transports for function-first architecture
|
|
308
|
+
// Log registered functions
|
|
667
309
|
if (this.functionRegistry) {
|
|
668
|
-
context.logger.info('');
|
|
669
|
-
context.logger.info('🔌 Initializing Transports...');
|
|
670
|
-
for (const transport of this.transports) {
|
|
671
|
-
await transport.initialize(this.functionRegistry, this.context);
|
|
672
|
-
await transport.start();
|
|
673
|
-
const metadata = transport.getMetadata();
|
|
674
|
-
context.logger.info(` ✅ ${transport.name} transport initialized`);
|
|
675
|
-
context.logger.info(` • Base Path: ${metadata.endpoint}`);
|
|
676
|
-
context.logger.info(` • Discovery: ${metadata.discovery}`);
|
|
677
|
-
}
|
|
678
|
-
// Log registered functions
|
|
679
310
|
const functionNames = this.functionRegistry.getFunctionNames();
|
|
680
311
|
if (functionNames.length > 0) {
|
|
681
312
|
context.logger.info('');
|
|
@@ -691,136 +322,21 @@ class BuiltPlugin {
|
|
|
691
322
|
}
|
|
692
323
|
}
|
|
693
324
|
}
|
|
694
|
-
// Register API routes (legacy)
|
|
695
|
-
for (const route of this.apiRoutes) {
|
|
696
|
-
const { regex, keys } = pathToRegex(route.path);
|
|
697
|
-
this.router.push({
|
|
698
|
-
method: route.method,
|
|
699
|
-
regex,
|
|
700
|
-
keys,
|
|
701
|
-
name: route.name,
|
|
702
|
-
description: route.description,
|
|
703
|
-
handler: route.handler,
|
|
704
|
-
requestSchema: route.requestSchema,
|
|
705
|
-
responseSchema: route.responseSchema,
|
|
706
|
-
tags: route.tags,
|
|
707
|
-
deprecated: route.deprecated
|
|
708
|
-
});
|
|
709
|
-
context.logger.info(`📝 Registered route: ${route.method} ${route.path} - ${route.name}`);
|
|
710
|
-
}
|
|
711
|
-
// Register the discovery endpoint at /majk/plugin/api
|
|
712
|
-
const discoveryPath = '/majk/plugin/api';
|
|
713
|
-
const { regex: discoveryRegex, keys: discoveryKeys } = pathToRegex(discoveryPath);
|
|
714
|
-
this.router.push({
|
|
715
|
-
method: 'GET',
|
|
716
|
-
regex: discoveryRegex,
|
|
717
|
-
keys: discoveryKeys,
|
|
718
|
-
name: 'API Discovery',
|
|
719
|
-
description: 'Get OpenAPI specification for all plugin API routes',
|
|
720
|
-
handler: async (_req, _res, _ctx) => {
|
|
721
|
-
// If using function-first architecture, generate spec from function registry
|
|
722
|
-
if (this.functionRegistry && this.functionRegistry.getFunctionNames().length > 0) {
|
|
723
|
-
return this.functionRegistry.toOpenAPISpec();
|
|
724
|
-
}
|
|
725
|
-
// Otherwise fall back to legacy API routes
|
|
726
|
-
const openApiSpec = generateOpenApiSpec(this.id, this.name, this.version, this.apiRoutes, this.context.http.baseUrl);
|
|
727
|
-
return openApiSpec;
|
|
728
|
-
}
|
|
729
|
-
});
|
|
730
|
-
context.logger.info(`📝 Registered discovery route: GET ${discoveryPath}`);
|
|
731
|
-
// Also register a simpler JSON format at /majk/plugin/routes for quick access
|
|
732
|
-
const routesPath = '/majk/plugin/routes';
|
|
733
|
-
const { regex: routesRegex, keys: routesKeys } = pathToRegex(routesPath);
|
|
734
|
-
this.router.push({
|
|
735
|
-
method: 'GET',
|
|
736
|
-
regex: routesRegex,
|
|
737
|
-
keys: routesKeys,
|
|
738
|
-
name: 'Routes List',
|
|
739
|
-
description: 'Get a simple JSON list of all available API routes',
|
|
740
|
-
handler: async (_req, _res, _ctx) => {
|
|
741
|
-
return {
|
|
742
|
-
plugin: {
|
|
743
|
-
id: this.id,
|
|
744
|
-
name: this.name,
|
|
745
|
-
version: this.version
|
|
746
|
-
},
|
|
747
|
-
routes: this.apiRoutes.map(route => ({
|
|
748
|
-
method: route.method,
|
|
749
|
-
path: route.path,
|
|
750
|
-
name: route.name,
|
|
751
|
-
description: route.description,
|
|
752
|
-
deprecated: route.deprecated,
|
|
753
|
-
tags: route.tags,
|
|
754
|
-
requestSchema: route.requestSchema,
|
|
755
|
-
responseSchema: route.responseSchema
|
|
756
|
-
})),
|
|
757
|
-
discoveryUrl: `${this.context.http.baseUrl}/majk/plugin/api`
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
context.logger.info(`📝 Registered routes list: GET ${routesPath}`);
|
|
762
325
|
// Call onReady hook
|
|
763
326
|
if (this.onReadyFn) {
|
|
327
|
+
context.logger.info('');
|
|
764
328
|
context.logger.info('⚙️ Calling onReady hook...');
|
|
765
329
|
await this.onReadyFn(this.context, (fn) => this.cleanups.push(fn));
|
|
766
330
|
context.logger.info('✅ onReady hook completed');
|
|
767
331
|
}
|
|
768
|
-
// Print
|
|
332
|
+
// Print summary
|
|
769
333
|
context.logger.info('');
|
|
770
334
|
context.logger.info('╔══════════════════════════════════════════════════════════╗');
|
|
771
335
|
context.logger.info('║ 🎉 PLUGIN LOADED SUCCESSFULLY ║');
|
|
772
|
-
context.logger.info('╟──────────────────────────────────────────────────────────╢');
|
|
773
|
-
context.logger.info('║ 🌐 API ENDPOINTS ║');
|
|
774
|
-
context.logger.info('╟──────────────────────────────────────────────────────────╢');
|
|
775
|
-
// API Discovery endpoints
|
|
776
|
-
context.logger.info(`║ 🔍 Discovery: ║`);
|
|
777
|
-
context.logger.info(`║ ${context.http.baseUrl}/majk/plugin/api`);
|
|
778
|
-
context.logger.info(`║ ${context.http.baseUrl}/majk/plugin/routes`);
|
|
779
|
-
// Function endpoints
|
|
780
|
-
if (this.functionRegistry && this.functionRegistry.getFunctionNames().length > 0) {
|
|
781
|
-
context.logger.info(`║ ║`);
|
|
782
|
-
context.logger.info(`║ 🚀 Function Endpoints: ║`);
|
|
783
|
-
for (const fname of this.functionRegistry.getFunctionNames()) {
|
|
784
|
-
context.logger.info(`║ POST ${context.http.baseUrl}/api/fn/${fname}`);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
// Legacy API routes
|
|
788
|
-
if (this.apiRoutes.length > 0) {
|
|
789
|
-
context.logger.info(`║ ║`);
|
|
790
|
-
context.logger.info(`║ 📋 Legacy API Routes: ║`);
|
|
791
|
-
for (const route of this.apiRoutes) {
|
|
792
|
-
context.logger.info(`║ ${route.method} ${context.http.baseUrl}${route.path}`);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
// UI Screens
|
|
796
|
-
const allScreens = [...this.reactScreens, ...this.htmlScreens];
|
|
797
|
-
if (allScreens.length > 0 || this.uiConfig) {
|
|
798
|
-
context.logger.info('╟──────────────────────────────────────────────────────────╢');
|
|
799
|
-
context.logger.info('║ 🕹 UI SCREENS ║');
|
|
800
|
-
context.logger.info('╟──────────────────────────────────────────────────────────╢');
|
|
801
|
-
for (const screen of allScreens) {
|
|
802
|
-
context.logger.info(`║ • ${screen.name || screen.id}`);
|
|
803
|
-
context.logger.info(`║ ${context.http.baseUrl}${screen.route}`);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
// Config Wizard
|
|
807
|
-
if (this.wizard) {
|
|
808
|
-
context.logger.info('╟──────────────────────────────────────────────────────────╢');
|
|
809
|
-
context.logger.info('║ 🧙 CONFIG WIZARD ║');
|
|
810
|
-
context.logger.info('╟──────────────────────────────────────────────────────────╢');
|
|
811
|
-
context.logger.info(`║ ${this.wizard.title}`);
|
|
812
|
-
context.logger.info(`║ ${context.http.baseUrl}${this.wizard.path}`);
|
|
813
|
-
}
|
|
814
|
-
// Settings
|
|
815
|
-
if (this.settings) {
|
|
816
|
-
context.logger.info('╟──────────────────────────────────────────────────────────╢');
|
|
817
|
-
context.logger.info('║ ⚙️ SETTINGS ║');
|
|
818
|
-
context.logger.info('╟──────────────────────────────────────────────────────────╢');
|
|
819
|
-
context.logger.info(`║ ${this.settings.title}`);
|
|
820
|
-
context.logger.info(`║ ${context.http.baseUrl}${this.settings.path}`);
|
|
821
|
-
}
|
|
822
336
|
context.logger.info('╚══════════════════════════════════════════════════════════╝');
|
|
823
337
|
context.logger.info('');
|
|
338
|
+
context.logger.info('ℹ️ Note: HTTP serving is managed by MAJK, not plugin-kit');
|
|
339
|
+
context.logger.info('');
|
|
824
340
|
}
|
|
825
341
|
catch (error) {
|
|
826
342
|
context.logger.error(`❌ Failed to load plugin "${this.name}": ${error.message}`);
|
|
@@ -841,17 +357,10 @@ class BuiltPlugin {
|
|
|
841
357
|
this.context.logger.error(`Error during cleanup: ${error.message}`);
|
|
842
358
|
}
|
|
843
359
|
}
|
|
844
|
-
// Stop HTTP server
|
|
845
|
-
if (this.server) {
|
|
846
|
-
await new Promise((resolve) => {
|
|
847
|
-
this.server.close(() => {
|
|
848
|
-
this.context.logger.info('✅ HTTP server stopped');
|
|
849
|
-
resolve();
|
|
850
|
-
});
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
360
|
this.context.logger.info(`✅ Plugin "${this.name}" unloaded`);
|
|
854
361
|
}
|
|
362
|
+
// isHealthy() - implements both old and new interface
|
|
363
|
+
// Returns compatible format for both interfaces
|
|
855
364
|
async isHealthy() {
|
|
856
365
|
if (this.healthCheckFn) {
|
|
857
366
|
try {
|
|
@@ -860,325 +369,25 @@ class BuiltPlugin {
|
|
|
860
369
|
storage: this.context.storage,
|
|
861
370
|
logger: this.context.logger
|
|
862
371
|
});
|
|
863
|
-
return {
|
|
372
|
+
return {
|
|
373
|
+
healthy: result.healthy,
|
|
374
|
+
checks: result.details,
|
|
375
|
+
errors: [],
|
|
376
|
+
warnings: []
|
|
377
|
+
};
|
|
864
378
|
}
|
|
865
379
|
catch (error) {
|
|
866
380
|
return {
|
|
867
381
|
healthy: false,
|
|
868
|
-
|
|
382
|
+
error: `Health check failed: ${error.message}`,
|
|
383
|
+
errors: [`Health check failed: ${error.message}`],
|
|
384
|
+
warnings: []
|
|
869
385
|
};
|
|
870
386
|
}
|
|
871
387
|
}
|
|
872
|
-
return { healthy: true };
|
|
873
|
-
}
|
|
874
|
-
async startServer() {
|
|
875
|
-
const { port } = this.context.http;
|
|
876
|
-
this.server = http_1.default.createServer(async (req, res) => {
|
|
877
|
-
const startTime = Date.now();
|
|
878
|
-
const method = req.method || 'UNKNOWN';
|
|
879
|
-
const url = req.url || '/';
|
|
880
|
-
try {
|
|
881
|
-
// Log incoming request
|
|
882
|
-
this.context.logger.debug(`→ ${method} ${url}`);
|
|
883
|
-
// CORS preflight
|
|
884
|
-
if (req.method === 'OPTIONS') {
|
|
885
|
-
res.writeHead(204, corsHeaders());
|
|
886
|
-
res.end();
|
|
887
|
-
const duration = Date.now() - startTime;
|
|
888
|
-
this.context.logger.debug(`← ${method} ${url} 204 (${duration}ms)`);
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
// Health check
|
|
892
|
-
if (req.method === 'GET' && req.url === '/health') {
|
|
893
|
-
const health = await this.isHealthy();
|
|
894
|
-
res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
895
|
-
res.end(JSON.stringify({ ...health, plugin: this.name, version: this.version }));
|
|
896
|
-
const duration = Date.now() - startTime;
|
|
897
|
-
this.context.logger.debug(`← ${method} ${url} 200 (${duration}ms)`);
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
// MCP Screenshot endpoint
|
|
901
|
-
if (req.method === 'POST' && req.url === '/api/mcp/screenshot') {
|
|
902
|
-
try {
|
|
903
|
-
const body = await readBody(req);
|
|
904
|
-
if (!body || !body.data) {
|
|
905
|
-
res.writeHead(400, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
906
|
-
res.end(JSON.stringify({ error: 'Missing screenshot data' }));
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
// Create screenshots directory if it doesn't exist
|
|
910
|
-
const screenshotsDir = path_1.default.join(this.context.pluginRoot, 'screenshots');
|
|
911
|
-
if (!fs_1.default.existsSync(screenshotsDir)) {
|
|
912
|
-
fs_1.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
913
|
-
}
|
|
914
|
-
// Generate filename
|
|
915
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
916
|
-
const metadata = body.metadata || {};
|
|
917
|
-
const customName = metadata.filename || `screenshot-${timestamp}`;
|
|
918
|
-
const filename = `${customName}.png`;
|
|
919
|
-
const filepath = path_1.default.join(screenshotsDir, filename);
|
|
920
|
-
// Extract base64 data (remove data:image/png;base64, prefix if present)
|
|
921
|
-
const base64Data = body.data.replace(/^data:image\/\w+;base64,/, '');
|
|
922
|
-
const buffer = Buffer.from(base64Data, 'base64');
|
|
923
|
-
// Write file
|
|
924
|
-
fs_1.default.writeFileSync(filepath, buffer);
|
|
925
|
-
// Also save metadata
|
|
926
|
-
const metadataPath = path_1.default.join(screenshotsDir, `${customName}.json`);
|
|
927
|
-
fs_1.default.writeFileSync(metadataPath, JSON.stringify({
|
|
928
|
-
...metadata,
|
|
929
|
-
filename,
|
|
930
|
-
filepath,
|
|
931
|
-
timestamp: new Date().toISOString(),
|
|
932
|
-
size: buffer.length
|
|
933
|
-
}, null, 2));
|
|
934
|
-
// Log with full details
|
|
935
|
-
const absolutePath = path_1.default.resolve(filepath);
|
|
936
|
-
const absoluteMetadataPath = path_1.default.resolve(metadataPath);
|
|
937
|
-
this.context.logger.info(`📸 Screenshot saved successfully:`);
|
|
938
|
-
this.context.logger.info(` Image: ${absolutePath}`);
|
|
939
|
-
this.context.logger.info(` Metadata: ${absoluteMetadataPath}`);
|
|
940
|
-
this.context.logger.info(` Size: ${(buffer.length / 1024).toFixed(2)} KB`);
|
|
941
|
-
res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
942
|
-
res.end(JSON.stringify({
|
|
943
|
-
success: true,
|
|
944
|
-
filename,
|
|
945
|
-
filepath,
|
|
946
|
-
size: buffer.length,
|
|
947
|
-
message: 'Screenshot saved successfully'
|
|
948
|
-
}));
|
|
949
|
-
const duration = Date.now() - startTime;
|
|
950
|
-
this.context.logger.debug(`← ${method} ${url} 200 (${duration}ms)`);
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
catch (error) {
|
|
954
|
-
this.context.logger.error(`Failed to save screenshot: ${error.message}`);
|
|
955
|
-
res.writeHead(500, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
956
|
-
res.end(JSON.stringify({
|
|
957
|
-
success: false,
|
|
958
|
-
error: error.message || 'Failed to save screenshot'
|
|
959
|
-
}));
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
// HTML screens virtual route
|
|
964
|
-
if (req.method === 'GET' && req.url?.startsWith('/__html/')) {
|
|
965
|
-
const id = decodeURIComponent(req.url.substring('/__html/'.length)).split('?')[0];
|
|
966
|
-
const screen = this.htmlScreens.find(s => s.id === id);
|
|
967
|
-
if (!screen) {
|
|
968
|
-
res.writeHead(404, corsHeaders({ 'Content-Type': 'text/plain' }));
|
|
969
|
-
res.end('HTML screen not found');
|
|
970
|
-
const duration = Date.now() - startTime;
|
|
971
|
-
this.context.logger.debug(`← ${method} ${url} 404 (${duration}ms)`);
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
res.writeHead(200, corsHeaders({ 'Content-Type': 'text/html; charset=utf-8' }));
|
|
975
|
-
let htmlContent;
|
|
976
|
-
if ('html' in screen) {
|
|
977
|
-
htmlContent = screen.html;
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
const filePath = path_1.default.join(this.context.pluginRoot, screen.htmlFile);
|
|
981
|
-
// Security check
|
|
982
|
-
const normalized = path_1.default.resolve(filePath);
|
|
983
|
-
const pluginRoot = path_1.default.resolve(this.context.pluginRoot);
|
|
984
|
-
if (!normalized.startsWith(pluginRoot)) {
|
|
985
|
-
res.writeHead(403, corsHeaders({ 'Content-Type': 'text/plain' }));
|
|
986
|
-
res.end('Forbidden');
|
|
987
|
-
this.context.logger.warn(`Path traversal attempt: ${screen.htmlFile}`);
|
|
988
|
-
const duration = Date.now() - startTime;
|
|
989
|
-
this.context.logger.debug(`← ${method} ${url} 403 (${duration}ms)`);
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
try {
|
|
993
|
-
htmlContent = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
994
|
-
}
|
|
995
|
-
catch (error) {
|
|
996
|
-
this.context.logger.error(`Failed to read HTML file: ${error.message}`);
|
|
997
|
-
res.writeHead(500, corsHeaders({ 'Content-Type': 'text/html' }));
|
|
998
|
-
res.end('<!doctype html><html><body><h1>Error loading screen</h1></body></html>');
|
|
999
|
-
const duration = Date.now() - startTime;
|
|
1000
|
-
this.context.logger.debug(`← ${method} ${url} 500 (${duration}ms)`);
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
// Inject html2canvas and MCP DOM Agent script into HTML screens
|
|
1005
|
-
if (BuiltPlugin.mcpDomAgentScript) {
|
|
1006
|
-
const html2canvasScript = BuiltPlugin.html2canvasScript
|
|
1007
|
-
? `<script>${BuiltPlugin.html2canvasScript}</script>`
|
|
1008
|
-
: '';
|
|
1009
|
-
const mcpScript = `<script>${BuiltPlugin.mcpDomAgentScript}</script>`;
|
|
1010
|
-
const allScripts = html2canvasScript + mcpScript;
|
|
1011
|
-
// Debug logging
|
|
1012
|
-
if (html2canvasScript) {
|
|
1013
|
-
this.context.logger.debug(`[HTML Screen] Injecting html2canvas (${(BuiltPlugin.html2canvasScript?.length || 0) / 1024} KB)`);
|
|
1014
|
-
}
|
|
1015
|
-
else {
|
|
1016
|
-
this.context.logger.warn('[HTML Screen] html2canvas NOT injected - script not loaded');
|
|
1017
|
-
}
|
|
1018
|
-
this.context.logger.debug('[HTML Screen] Injecting MCP DOM Agent');
|
|
1019
|
-
// Try to inject before </body>, fallback to </head>, fallback to end of HTML
|
|
1020
|
-
if (htmlContent.includes('</body>')) {
|
|
1021
|
-
htmlContent = htmlContent.replace('</body>', `${allScripts}</body>`);
|
|
1022
|
-
}
|
|
1023
|
-
else if (htmlContent.includes('</head>')) {
|
|
1024
|
-
htmlContent = htmlContent.replace('</head>', `${allScripts}</head>`);
|
|
1025
|
-
}
|
|
1026
|
-
else {
|
|
1027
|
-
htmlContent += allScripts;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
res.end(htmlContent);
|
|
1031
|
-
const duration = Date.now() - startTime;
|
|
1032
|
-
this.context.logger.debug(`← ${method} ${url} 200 (${duration}ms)`);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
// React SPA
|
|
1036
|
-
if (req.method === 'GET' && this.uiConfig && req.url?.startsWith(this.uiConfig.base)) {
|
|
1037
|
-
serveSpa(this.uiConfig, req, res, this.context);
|
|
1038
|
-
const duration = Date.now() - startTime;
|
|
1039
|
-
this.context.logger.debug(`← ${method} ${url} ${res.statusCode || 200} (${duration}ms)`);
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
// Function calls (new function-first API)
|
|
1043
|
-
if (req.method === 'POST' && req.url?.startsWith('/api/fn/')) {
|
|
1044
|
-
const functionName = req.url.substring('/api/fn/'.length).split('?')[0];
|
|
1045
|
-
if (this.functionRegistry && this.transports.length > 0) {
|
|
1046
|
-
// Use the first HTTP transport to handle the function call
|
|
1047
|
-
const httpTransport = this.transports.find(t => t.name === 'http');
|
|
1048
|
-
if (httpTransport) {
|
|
1049
|
-
const body = await readBody(req);
|
|
1050
|
-
const request = {
|
|
1051
|
-
method: 'POST',
|
|
1052
|
-
url: req.url,
|
|
1053
|
-
pathname: req.url.split('?')[0],
|
|
1054
|
-
query: new URL(req.url, `http://localhost:${port}`).searchParams,
|
|
1055
|
-
params: {},
|
|
1056
|
-
body,
|
|
1057
|
-
headers: req.headers
|
|
1058
|
-
};
|
|
1059
|
-
const response = makeResponse(res);
|
|
1060
|
-
await httpTransport.handleFunctionCall(functionName, request, response, {
|
|
1061
|
-
majk: this.context.majk,
|
|
1062
|
-
storage: this.context.storage,
|
|
1063
|
-
logger: this.context.logger,
|
|
1064
|
-
http: this.context.http
|
|
1065
|
-
});
|
|
1066
|
-
const duration = Date.now() - startTime;
|
|
1067
|
-
this.context.logger.debug(`← POST /api/fn/${functionName} ${res.statusCode || 200} (${duration}ms)`);
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
// Function discovery endpoint
|
|
1073
|
-
if (req.method === 'GET' && req.url === '/api/fn/discovery') {
|
|
1074
|
-
if (this.functionRegistry && this.transports.length > 0) {
|
|
1075
|
-
const httpTransport = this.transports.find(t => t.name === 'http');
|
|
1076
|
-
if (httpTransport) {
|
|
1077
|
-
const discovery = httpTransport.getDiscovery();
|
|
1078
|
-
res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
1079
|
-
res.end(JSON.stringify(discovery));
|
|
1080
|
-
const duration = Date.now() - startTime;
|
|
1081
|
-
this.context.logger.debug(`← GET /api/fn/discovery 200 (${duration}ms)`);
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
// API routes (including discovery endpoints) - legacy
|
|
1087
|
-
if (req.url?.startsWith('/api/') || req.url?.startsWith('/majk/plugin/')) {
|
|
1088
|
-
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1089
|
-
const pathname = url.pathname;
|
|
1090
|
-
const method = req.method;
|
|
1091
|
-
this.context.logger.info(`[ROUTE MATCHING] Incoming: ${method} ${pathname}`);
|
|
1092
|
-
this.context.logger.info(`[ROUTE MATCHING] Available routes: ${this.router.length}`);
|
|
1093
|
-
this.router.forEach((r, idx) => {
|
|
1094
|
-
const matches = r.regex.test(pathname);
|
|
1095
|
-
this.context.logger.info(`[ROUTE MATCHING] Route ${idx}: ${r.method} ${r.name} - regex: ${r.regex} - keys: [${r.keys.join(', ')}] - matches: ${matches}`);
|
|
1096
|
-
});
|
|
1097
|
-
const route = this.router.find(r => r.method === method && r.regex.test(pathname));
|
|
1098
|
-
if (!route) {
|
|
1099
|
-
res.writeHead(404, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
1100
|
-
res.end(JSON.stringify({
|
|
1101
|
-
error: 'Route not found',
|
|
1102
|
-
path: pathname,
|
|
1103
|
-
method,
|
|
1104
|
-
hint: `Available routes: ${this.router.map(r => `${r.method} ${r.name}`).join(', ')}`
|
|
1105
|
-
}));
|
|
1106
|
-
const duration = Date.now() - startTime;
|
|
1107
|
-
this.context.logger.debug(`← ${method} ${pathname} 404 (${duration}ms)`);
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
const body = await readBody(req);
|
|
1111
|
-
const match = pathname.match(route.regex);
|
|
1112
|
-
const params = {};
|
|
1113
|
-
route.keys.forEach((key, i) => {
|
|
1114
|
-
params[key] = decodeURIComponent(match[i + 1]);
|
|
1115
|
-
});
|
|
1116
|
-
const request = {
|
|
1117
|
-
method,
|
|
1118
|
-
url: req.url,
|
|
1119
|
-
pathname,
|
|
1120
|
-
query: url.searchParams,
|
|
1121
|
-
params,
|
|
1122
|
-
body,
|
|
1123
|
-
headers: req.headers
|
|
1124
|
-
};
|
|
1125
|
-
const response = makeResponse(res);
|
|
1126
|
-
const routeContext = {
|
|
1127
|
-
majk: this.context.majk,
|
|
1128
|
-
storage: this.context.storage,
|
|
1129
|
-
logger: this.context.logger,
|
|
1130
|
-
http: this.context.http
|
|
1131
|
-
};
|
|
1132
|
-
try {
|
|
1133
|
-
const result = await route.handler(request, response, routeContext);
|
|
1134
|
-
if (!res.headersSent) {
|
|
1135
|
-
res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
1136
|
-
res.end(JSON.stringify(result ?? { success: true }));
|
|
1137
|
-
}
|
|
1138
|
-
const duration = Date.now() - startTime;
|
|
1139
|
-
this.context.logger.debug(`← ${method} ${pathname} ${res.statusCode || 200} (${duration}ms)`);
|
|
1140
|
-
}
|
|
1141
|
-
catch (error) {
|
|
1142
|
-
this.context.logger.error(`API route error: ${method} ${pathname} - ${error.message}`);
|
|
1143
|
-
if (error.stack) {
|
|
1144
|
-
this.context.logger.error(error.stack);
|
|
1145
|
-
}
|
|
1146
|
-
if (!res.headersSent) {
|
|
1147
|
-
res.writeHead(500, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
1148
|
-
res.end(JSON.stringify({
|
|
1149
|
-
error: error.message || 'Internal server error',
|
|
1150
|
-
route: route.name,
|
|
1151
|
-
path: pathname
|
|
1152
|
-
}));
|
|
1153
|
-
}
|
|
1154
|
-
const duration = Date.now() - startTime;
|
|
1155
|
-
this.context.logger.debug(`← ${method} ${pathname} 500 (${duration}ms)`);
|
|
1156
|
-
}
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
// 404
|
|
1160
|
-
res.writeHead(404, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
1161
|
-
res.end(JSON.stringify({ error: 'Not found', path: req.url }));
|
|
1162
|
-
}
|
|
1163
|
-
catch (error) {
|
|
1164
|
-
this.context.logger.error(`Unhandled server error: ${error.message}`);
|
|
1165
|
-
if (!res.headersSent) {
|
|
1166
|
-
res.writeHead(500, corsHeaders({ 'Content-Type': 'text/plain' }));
|
|
1167
|
-
res.end('Internal server error');
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
});
|
|
1171
|
-
await new Promise((resolve) => {
|
|
1172
|
-
this.server.listen(port, () => {
|
|
1173
|
-
this.context.logger.info(`✅ HTTP server listening on port ${port}`);
|
|
1174
|
-
resolve();
|
|
1175
|
-
});
|
|
1176
|
-
});
|
|
388
|
+
return { healthy: true, errors: [], warnings: [] };
|
|
1177
389
|
}
|
|
1178
390
|
}
|
|
1179
|
-
// MCP DOM Agent script (loaded once at startup, accessible to serveSpa function)
|
|
1180
|
-
BuiltPlugin.mcpDomAgentScript = null;
|
|
1181
|
-
BuiltPlugin.html2canvasScript = null;
|
|
1182
391
|
/**
|
|
1183
392
|
* Group tools by scope
|
|
1184
393
|
*/
|
|
@@ -1205,12 +414,12 @@ function definePlugin(id, name, version) {
|
|
|
1205
414
|
const _reactScreens = [];
|
|
1206
415
|
const _htmlScreens = [];
|
|
1207
416
|
const _topbars = [];
|
|
417
|
+
const _navpaneActions = [];
|
|
1208
418
|
const _entities = [];
|
|
1209
419
|
const _configurableEntities = [];
|
|
1210
420
|
const _secretProviders = [];
|
|
1211
421
|
// Function-first architecture state
|
|
1212
422
|
const _functionRegistry = new registry_1.FunctionRegistryImpl(id, name, version);
|
|
1213
|
-
const _transports = [];
|
|
1214
423
|
let _clientConfig = null;
|
|
1215
424
|
let _ui = { appDir: 'ui/dist', base: '/', history: 'browser' };
|
|
1216
425
|
let _uiConfigured = false;
|
|
@@ -1248,6 +457,57 @@ function definePlugin(id, name, version) {
|
|
|
1248
457
|
});
|
|
1249
458
|
return this;
|
|
1250
459
|
},
|
|
460
|
+
navpaneAction(action) {
|
|
461
|
+
// Validate required fields
|
|
462
|
+
if (!action.id) {
|
|
463
|
+
throw new PluginBuildError('NavPane action must have an id', 'Provide a unique identifier for this action', { action });
|
|
464
|
+
}
|
|
465
|
+
if (!action.label) {
|
|
466
|
+
throw new PluginBuildError('NavPane action must have a label', 'Provide a display label for the button', { action });
|
|
467
|
+
}
|
|
468
|
+
// Validate that at least one of event or route is specified
|
|
469
|
+
if (!action.event && !action.route) {
|
|
470
|
+
throw new PluginBuildError('NavPane action must specify at least one of: event, route', 'Add an event to dispatch, a route to navigate to, or both', { action });
|
|
471
|
+
}
|
|
472
|
+
// Validate event structure if present
|
|
473
|
+
if (action.event) {
|
|
474
|
+
if (!action.event.name) {
|
|
475
|
+
throw new PluginBuildError('NavPane action event must have a name', 'Provide an event name (e.g., "my-plugin:action")', { action });
|
|
476
|
+
}
|
|
477
|
+
if (action.event.payload !== undefined && typeof action.event.payload !== 'object') {
|
|
478
|
+
throw new PluginBuildError('NavPane action event payload must be an object', 'Provide a JSON-serializable object as payload', { action });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Validate context array if present
|
|
482
|
+
if (action.context) {
|
|
483
|
+
if (!Array.isArray(action.context)) {
|
|
484
|
+
throw new PluginBuildError('NavPane action context must be an array', 'Provide an array of context types', { action });
|
|
485
|
+
}
|
|
486
|
+
const validContextTypes = ['conversation', 'project', 'teammate', 'filter', 'search'];
|
|
487
|
+
for (const ctx of action.context) {
|
|
488
|
+
if (!validContextTypes.includes(ctx)) {
|
|
489
|
+
throw new PluginBuildError(`Invalid context type: ${ctx}`, `Must be one of: ${validContextTypes.join(', ')}`, { action });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Validate color if present
|
|
494
|
+
if (action.color) {
|
|
495
|
+
const validColors = ['primary', 'success', 'warning', 'danger', 'default'];
|
|
496
|
+
if (!validColors.includes(action.color)) {
|
|
497
|
+
throw new PluginBuildError(`Invalid color: ${action.color}`, `Must be one of: ${validColors.join(', ')}`, { action });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Validate variant if present
|
|
501
|
+
if (action.variant) {
|
|
502
|
+
const validVariants = ['flat', 'solid', 'bordered', 'light'];
|
|
503
|
+
if (!validVariants.includes(action.variant)) {
|
|
504
|
+
throw new PluginBuildError(`Invalid variant: ${action.variant}`, `Must be one of: ${validVariants.join(', ')}`, { action });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
_navpaneActions.push(action);
|
|
508
|
+
log(` ⚡ Registered NavPane action: ${action.label} (${action.id})`);
|
|
509
|
+
return this;
|
|
510
|
+
},
|
|
1251
511
|
ui(config) {
|
|
1252
512
|
_uiConfigured = true;
|
|
1253
513
|
if (config?.appDir)
|
|
@@ -1356,7 +616,7 @@ function definePlugin(id, name, version) {
|
|
|
1356
616
|
return this;
|
|
1357
617
|
},
|
|
1358
618
|
transport(transport) {
|
|
1359
|
-
|
|
619
|
+
log('⚠️ WARNING: .transport() is deprecated in v2.0.0. HTTP serving is now managed by MAJK. This call does nothing.');
|
|
1360
620
|
return this;
|
|
1361
621
|
},
|
|
1362
622
|
generateClient(config) {
|
|
@@ -1588,6 +848,22 @@ function definePlugin(id, name, version) {
|
|
|
1588
848
|
route: topbar.route
|
|
1589
849
|
});
|
|
1590
850
|
}
|
|
851
|
+
// Add navpane actions
|
|
852
|
+
for (const action of _navpaneActions) {
|
|
853
|
+
_capabilities.push({
|
|
854
|
+
type: 'navpane-action',
|
|
855
|
+
id: action.id,
|
|
856
|
+
label: action.label,
|
|
857
|
+
icon: action.icon,
|
|
858
|
+
event: action.event,
|
|
859
|
+
route: action.route,
|
|
860
|
+
context: action.context,
|
|
861
|
+
order: action.order ?? 500,
|
|
862
|
+
color: action.color ?? 'primary',
|
|
863
|
+
variant: action.variant ?? 'flat',
|
|
864
|
+
tooltip: action.tooltip
|
|
865
|
+
});
|
|
866
|
+
}
|
|
1591
867
|
// Add tools (grouped by scope)
|
|
1592
868
|
const toolGroups = groupByScope(_toolSpecs);
|
|
1593
869
|
for (const group of toolGroups) {
|
|
@@ -1695,10 +971,6 @@ function definePlugin(id, name, version) {
|
|
|
1695
971
|
// ========== Build Plugin Class ==========
|
|
1696
972
|
// MAJK expects a class/constructor, not an instance
|
|
1697
973
|
// Return a class that instantiates BuiltPlugin
|
|
1698
|
-
// If no transports specified but functions were defined, add default HTTP transport
|
|
1699
|
-
if (_functionRegistry.functions.size > 0 && _transports.length === 0) {
|
|
1700
|
-
_transports.push(new transports_1.HttpTransport());
|
|
1701
|
-
}
|
|
1702
974
|
// TODO: Implement client generation if configured
|
|
1703
975
|
if (_clientConfig) {
|
|
1704
976
|
// This would generate the client files based on the function registry
|
|
@@ -1747,8 +1019,8 @@ function definePlugin(id, name, version) {
|
|
|
1747
1019
|
return class extends BuiltPlugin {
|
|
1748
1020
|
constructor() {
|
|
1749
1021
|
super(id, name, version, _capabilities, _tools, _apiRoutes, _uiConfigured ? _ui : null, _reactScreens, _htmlScreens, _wizard, _settings, _onReady, _healthCheck, _pluginRoot,
|
|
1750
|
-
// Pass function registry
|
|
1751
|
-
_functionRegistry.functions.size > 0 ? _functionRegistry : undefined,
|
|
1022
|
+
// Pass function registry if functions were defined
|
|
1023
|
+
_functionRegistry.functions.size > 0 ? _functionRegistry : undefined,
|
|
1752
1024
|
// Pass configurable entities
|
|
1753
1025
|
_configurableEntities.length > 0 ? _configurableEntities : undefined,
|
|
1754
1026
|
// Pass secret providers
|