@majkapp/plugin-kit 1.3.9 → 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.
@@ -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
- // New parameters for function-first architecture
478
- functionRegistry, transports,
479
- // New parameter for configurable entities
158
+ // Function registry
159
+ functionRegistry,
160
+ // Configurable entities
480
161
  configurableEntities,
481
- // New parameter for secret providers
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
- // Use user-provided pluginRoot (from __dirname) if available, otherwise fall back to context.pluginRoot
609
- // The user-provided path comes from Node's __dirname which automatically resolves to the real directory
610
- // (following symlinks), making it work correctly in both local development and npm installed scenarios
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@1.0.19`);
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
- await this.startServer();
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 comprehensive summary
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 { ...result, errors: [], warnings: [] };
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
- errors: [`Health check failed: ${error.message}`]
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
  */
@@ -1211,7 +420,6 @@ function definePlugin(id, name, version) {
1211
420
  const _secretProviders = [];
1212
421
  // Function-first architecture state
1213
422
  const _functionRegistry = new registry_1.FunctionRegistryImpl(id, name, version);
1214
- const _transports = [];
1215
423
  let _clientConfig = null;
1216
424
  let _ui = { appDir: 'ui/dist', base: '/', history: 'browser' };
1217
425
  let _uiConfigured = false;
@@ -1408,7 +616,7 @@ function definePlugin(id, name, version) {
1408
616
  return this;
1409
617
  },
1410
618
  transport(transport) {
1411
- _transports.push(transport);
619
+ log('⚠️ WARNING: .transport() is deprecated in v2.0.0. HTTP serving is now managed by MAJK. This call does nothing.');
1412
620
  return this;
1413
621
  },
1414
622
  generateClient(config) {
@@ -1763,10 +971,6 @@ function definePlugin(id, name, version) {
1763
971
  // ========== Build Plugin Class ==========
1764
972
  // MAJK expects a class/constructor, not an instance
1765
973
  // Return a class that instantiates BuiltPlugin
1766
- // If no transports specified but functions were defined, add default HTTP transport
1767
- if (_functionRegistry.functions.size > 0 && _transports.length === 0) {
1768
- _transports.push(new transports_1.HttpTransport());
1769
- }
1770
974
  // TODO: Implement client generation if configured
1771
975
  if (_clientConfig) {
1772
976
  // This would generate the client files based on the function registry
@@ -1815,8 +1019,8 @@ function definePlugin(id, name, version) {
1815
1019
  return class extends BuiltPlugin {
1816
1020
  constructor() {
1817
1021
  super(id, name, version, _capabilities, _tools, _apiRoutes, _uiConfigured ? _ui : null, _reactScreens, _htmlScreens, _wizard, _settings, _onReady, _healthCheck, _pluginRoot,
1818
- // Pass function registry and transports if functions were defined
1819
- _functionRegistry.functions.size > 0 ? _functionRegistry : undefined, _transports.length > 0 ? _transports : undefined,
1022
+ // Pass function registry if functions were defined
1023
+ _functionRegistry.functions.size > 0 ? _functionRegistry : undefined,
1820
1024
  // Pass configurable entities
1821
1025
  _configurableEntities.length > 0 ? _configurableEntities : undefined,
1822
1026
  // Pass secret providers