@majkapp/plugin-kit 2.1.0 → 2.3.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,9 @@ 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
8
  const path_1 = __importDefault(require("path"));
10
9
  const registry_1 = require("./registry");
11
- const transports_1 = require("./transports");
12
10
  /**
13
11
  * Conditional logging - suppressed in extract mode
14
12
  */
@@ -153,333 +151,19 @@ function validateEntity(entityType, entity) {
153
151
  }
154
152
  return errors;
155
153
  }
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
154
  /**
473
155
  * Main plugin class built by the fluent API
474
156
  */
475
157
  class BuiltPlugin {
476
158
  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
159
+ // Function registry
160
+ functionRegistry,
161
+ // Configurable entities
480
162
  configurableEntities,
481
- // New parameter for secret providers
482
- secretProviders) {
163
+ // Secret providers
164
+ secretProviders,
165
+ // Package name for RPC service registration
166
+ packageName) {
483
167
  this.capabilities = capabilities;
484
168
  this.tools = tools;
485
169
  this.apiRoutes = apiRoutes;
@@ -493,16 +177,15 @@ class BuiltPlugin {
493
177
  this.userProvidedPluginRoot = userProvidedPluginRoot;
494
178
  this.configurableEntities = configurableEntities;
495
179
  this.secretProviders = secretProviders;
180
+ this.packageName = packageName;
496
181
  this.cleanups = [];
497
- this.router = [];
498
- this.transports = [];
499
182
  this.id = id;
500
183
  this.name = name;
501
184
  this.version = version;
502
185
  this.functionRegistry = functionRegistry;
503
- this.transports = transports || [];
504
186
  this.configurableEntities = configurableEntities || [];
505
187
  this.secretProviders = secretProviders || [];
188
+ this.packageName = packageName;
506
189
  }
507
190
  async getCapabilities() {
508
191
  // Start with static capabilities
@@ -568,6 +251,19 @@ class BuiltPlugin {
568
251
  }
569
252
  return false;
570
253
  }
254
+ /**
255
+ * Get function provider (new async Plugin interface)
256
+ * Returns null if plugin has no functions
257
+ */
258
+ async getFunctions() {
259
+ if (!this.functionRegistry) {
260
+ return null;
261
+ }
262
+ if (!this._functionProvider) {
263
+ this._functionProvider = new registry_1.FunctionProviderImpl(this.functionRegistry, this.context);
264
+ }
265
+ return this._functionProvider;
266
+ }
571
267
  async createTool(toolName) {
572
268
  const handler = this.tools.get(toolName);
573
269
  if (!handler) {
@@ -605,77 +301,17 @@ class BuiltPlugin {
605
301
  }
606
302
  }
607
303
  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
- };
304
+ // NOTE: context.resources is now provided by MAJK (ResourceProvider abstraction)
305
+ // Plugin-kit no longer manages HTTP servers or file paths directly
306
+ this.context = context;
616
307
  context.logger.info('═══════════════════════════════════════════════════════');
617
308
  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}`);
309
+ context.logger.info(`🔧 Plugin Kit Version: @majkapp/plugin-kit@2.0.0`);
627
310
  context.logger.info(`🔗 Base URL: ${context.http.baseUrl}`);
628
311
  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
312
  try {
665
- await this.startServer();
666
- // Initialize transports for function-first architecture
313
+ // Log registered functions
667
314
  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
315
  const functionNames = this.functionRegistry.getFunctionNames();
680
316
  if (functionNames.length > 0) {
681
317
  context.logger.info('');
@@ -691,136 +327,70 @@ class BuiltPlugin {
691
327
  }
692
328
  }
693
329
  }
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
330
  // Call onReady hook
763
331
  if (this.onReadyFn) {
332
+ context.logger.info('');
764
333
  context.logger.info('⚙️ Calling onReady hook...');
765
334
  await this.onReadyFn(this.context, (fn) => this.cleanups.push(fn));
766
335
  context.logger.info('✅ onReady hook completed');
767
336
  }
768
- // Print comprehensive summary
769
- context.logger.info('');
770
- context.logger.info('╔══════════════════════════════════════════════════════════╗');
771
- 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}`);
337
+ // Register RPC service
338
+ if (this.packageName && this.functionRegistry && context.rpc) {
339
+ const functionNames = this.functionRegistry.getFunctionNames();
340
+ context.logger.info('');
341
+ context.logger.info('═══════════════════════════════════════════════════════');
342
+ context.logger.info('📡 Registering RPC Service');
343
+ context.logger.info(` Service Name: ${this.packageName}`);
344
+ context.logger.info(` Methods: ${functionNames.length}`);
345
+ for (const fname of functionNames) {
346
+ const func = this.functionRegistry.functions.get(fname);
347
+ context.logger.info(` • ${fname}${func?.deprecated ? ' (DEPRECATED)' : ''}`);
785
348
  }
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}`);
349
+ context.logger.info('═══════════════════════════════════════════════════════');
350
+ const serviceMethods = {};
351
+ for (const [fname, func] of this.functionRegistry.functions) {
352
+ serviceMethods[fname] = async (input) => {
353
+ const callId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
354
+ const startTime = Date.now();
355
+ context.logger.info(`📡 [RPC IN] ${this.packageName}.${fname} (call: ${callId})`);
356
+ context.logger.debug(` Input: ${JSON.stringify(input).substring(0, 200)}`);
357
+ try {
358
+ const result = await func.handler(input, context);
359
+ const duration = Date.now() - startTime;
360
+ context.logger.info(`📡 [RPC OUT] ${this.packageName}.${fname} (call: ${callId}, ${duration}ms) ✅`);
361
+ context.logger.debug(` Output: ${JSON.stringify(result).substring(0, 200)}`);
362
+ return result;
363
+ }
364
+ catch (error) {
365
+ const duration = Date.now() - startTime;
366
+ context.logger.error(`📡 [RPC ERR] ${this.packageName}.${fname} (call: ${callId}, ${duration}ms) ❌`);
367
+ context.logger.error(` Error: ${error.message}`);
368
+ if (error.stack) {
369
+ context.logger.debug(` Stack: ${error.stack}`);
370
+ }
371
+ throw error;
372
+ }
373
+ };
793
374
  }
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}`);
375
+ try {
376
+ context.rpc.registerService(this.packageName, serviceMethods);
377
+ context.logger.info('✅ RPC service registered successfully');
378
+ context.logger.info('');
379
+ }
380
+ catch (error) {
381
+ context.logger.error(`❌ Failed to register RPC service: ${error.message}`);
382
+ context.logger.error(' Functions will still be available via direct invocation');
383
+ context.logger.info('');
804
384
  }
805
385
  }
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
- }
386
+ // Print summary
387
+ context.logger.info('');
388
+ context.logger.info('╔══════════════════════════════════════════════════════════╗');
389
+ context.logger.info('║ 🎉 PLUGIN LOADED SUCCESSFULLY ║');
822
390
  context.logger.info('╚══════════════════════════════════════════════════════════╝');
823
391
  context.logger.info('');
392
+ context.logger.info('ℹ️ Note: HTTP serving is managed by MAJK, not plugin-kit');
393
+ context.logger.info('');
824
394
  }
825
395
  catch (error) {
826
396
  context.logger.error(`❌ Failed to load plugin "${this.name}": ${error.message}`);
@@ -832,6 +402,18 @@ class BuiltPlugin {
832
402
  }
833
403
  async onUnload() {
834
404
  this.context.logger.info(`🛑 Unloading plugin: ${this.name}`);
405
+ // Unregister RPC service
406
+ if (this.packageName && this.context.rpc) {
407
+ try {
408
+ if (typeof this.context.rpc.unregisterService === 'function') {
409
+ this.context.rpc.unregisterService(this.packageName);
410
+ this.context.logger.info(`📡 RPC service unregistered: ${this.packageName}`);
411
+ }
412
+ }
413
+ catch (error) {
414
+ this.context.logger.error(`Failed to unregister RPC service: ${error.message}`);
415
+ }
416
+ }
835
417
  // Run cleanup functions
836
418
  for (const cleanup of this.cleanups.splice(0)) {
837
419
  try {
@@ -841,17 +423,10 @@ class BuiltPlugin {
841
423
  this.context.logger.error(`Error during cleanup: ${error.message}`);
842
424
  }
843
425
  }
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
426
  this.context.logger.info(`✅ Plugin "${this.name}" unloaded`);
854
427
  }
428
+ // isHealthy() - implements both old and new interface
429
+ // Returns compatible format for both interfaces
855
430
  async isHealthy() {
856
431
  if (this.healthCheckFn) {
857
432
  try {
@@ -860,325 +435,25 @@ class BuiltPlugin {
860
435
  storage: this.context.storage,
861
436
  logger: this.context.logger
862
437
  });
863
- return { ...result, errors: [], warnings: [] };
438
+ return {
439
+ healthy: result.healthy,
440
+ checks: result.details,
441
+ errors: [],
442
+ warnings: []
443
+ };
864
444
  }
865
445
  catch (error) {
866
446
  return {
867
447
  healthy: false,
868
- errors: [`Health check failed: ${error.message}`]
448
+ error: `Health check failed: ${error.message}`,
449
+ errors: [`Health check failed: ${error.message}`],
450
+ warnings: []
869
451
  };
870
452
  }
871
453
  }
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
- });
454
+ return { healthy: true, errors: [], warnings: [] };
1177
455
  }
1178
456
  }
1179
- // MCP DOM Agent script (loaded once at startup, accessible to serveSpa function)
1180
- BuiltPlugin.mcpDomAgentScript = null;
1181
- BuiltPlugin.html2canvasScript = null;
1182
457
  /**
1183
458
  * Group tools by scope
1184
459
  */
@@ -1211,7 +486,6 @@ function definePlugin(id, name, version) {
1211
486
  const _secretProviders = [];
1212
487
  // Function-first architecture state
1213
488
  const _functionRegistry = new registry_1.FunctionRegistryImpl(id, name, version);
1214
- const _transports = [];
1215
489
  let _clientConfig = null;
1216
490
  let _ui = { appDir: 'ui/dist', base: '/', history: 'browser' };
1217
491
  let _uiConfigured = false;
@@ -1315,17 +589,32 @@ function definePlugin(id, name, version) {
1315
589
  if (!screen.route.startsWith(`/plugin-screens/${id}/`)) {
1316
590
  throw new PluginBuildError(`React screen route must start with "/plugin-screens/${id}/"`, `Change route from "${screen.route}" to "/plugin-screens/${id}/your-route"`, { screen: screen.id, route: screen.route });
1317
591
  }
1318
- const iframeUrl = `http://localhost:{port}${_ui.base}${screen.reactPath}`;
592
+ // Validate that at least one of the path properties is provided
593
+ if (!screen.pluginPath && !screen.reactPath) {
594
+ throw new PluginBuildError(`React screen "${screen.name}" must provide either pluginPath or reactPath (deprecated)`, `Add pluginPath: '/ui/index.html' to your screen definition`, { screen: screen.id });
595
+ }
596
+ // Warn if using deprecated properties
597
+ if (screen.reactPath) {
598
+ log(` ⚠️ Warning: reactPath is deprecated for screen "${screen.name}". Use pluginPath instead.`);
599
+ }
600
+ if (screen.hash) {
601
+ log(` ⚠️ Warning: hash is deprecated for screen "${screen.name}". Use pluginPathHash instead.`);
602
+ }
1319
603
  _reactScreens.push(screen);
1320
604
  log(` 🕹 Registered React screen: ${screen.name} at ${screen.route}`);
1321
605
  const capability = {
1322
606
  type: 'screen',
1323
607
  id: screen.id,
1324
- path: screen.route,
1325
- iframeUrl
608
+ path: screen.route
1326
609
  };
1327
- if (screen.hash) {
1328
- capability.hash = screen.hash;
610
+ // Prefer new properties, fallback to deprecated ones
611
+ const effectivePluginPath = screen.pluginPath || screen.reactPath;
612
+ const effectivePluginPathHash = screen.pluginPathHash || screen.hash;
613
+ if (effectivePluginPath) {
614
+ capability.pluginPath = effectivePluginPath;
615
+ }
616
+ if (effectivePluginPathHash) {
617
+ capability.pluginPathHash = effectivePluginPathHash;
1329
618
  }
1330
619
  _capabilities.push(capability);
1331
620
  return this;
@@ -1335,15 +624,22 @@ function definePlugin(id, name, version) {
1335
624
  if (!screen.route.startsWith(`/plugin-screens/${id}/`)) {
1336
625
  throw new PluginBuildError(`HTML screen route must start with "/plugin-screens/${id}/"`, `Change route from "${screen.route}" to "/plugin-screens/${id}/your-route"`, { screen: screen.id, route: screen.route });
1337
626
  }
1338
- const iframeUrl = `http://localhost:{port}/__html/${encodeURIComponent(screen.id)}`;
1339
627
  _htmlScreens.push(screen);
1340
628
  log(` 📋 Registered HTML screen: ${screen.name} at ${screen.route}`);
1341
- _capabilities.push({
629
+ const capability = {
1342
630
  type: 'screen',
1343
631
  id: screen.id,
1344
632
  path: screen.route,
1345
- iframeUrl
1346
- });
633
+ pluginRoute: `/__html/${encodeURIComponent(screen.id)}`
634
+ };
635
+ // Add pluginPath and pluginPathHash if present
636
+ if (screen.pluginPath) {
637
+ capability.pluginPath = screen.pluginPath;
638
+ }
639
+ if (screen.pluginPathHash) {
640
+ capability.pluginPathHash = screen.pluginPathHash;
641
+ }
642
+ _capabilities.push(capability);
1347
643
  return this;
1348
644
  },
1349
645
  apiRoute(route) {
@@ -1408,7 +704,7 @@ function definePlugin(id, name, version) {
1408
704
  return this;
1409
705
  },
1410
706
  transport(transport) {
1411
- _transports.push(transport);
707
+ log('⚠️ WARNING: .transport() is deprecated in v2.0.0. HTTP serving is now managed by MAJK. This call does nothing.');
1412
708
  return this;
1413
709
  },
1414
710
  generateClient(config) {
@@ -1579,6 +875,25 @@ function definePlugin(id, name, version) {
1579
875
  return this;
1580
876
  },
1581
877
  build() {
878
+ // ========== Load Package Info ==========
879
+ let packageName;
880
+ let packageVersion;
881
+ if (_pluginRoot) {
882
+ try {
883
+ const packageJsonPath = path_1.default.resolve(_pluginRoot, 'package.json');
884
+ if (fs_1.default.existsSync(packageJsonPath)) {
885
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
886
+ packageName = packageJson.name;
887
+ packageVersion = packageJson.version;
888
+ if (packageName) {
889
+ log(` 📦 Package: ${packageName}@${packageVersion}`);
890
+ }
891
+ }
892
+ }
893
+ catch (error) {
894
+ log(` ⚠️ Could not read package.json: ${error.message}`);
895
+ }
896
+ }
1582
897
  // ========== Build-Time Validation ==========
1583
898
  // Validate configurable entities require configWizard with schema
1584
899
  if (_configurableEntities.length > 0) {
@@ -1723,19 +1038,34 @@ function definePlugin(id, name, version) {
1723
1038
  }
1724
1039
  // Add wizard
1725
1040
  if (_wizard) {
1041
+ // Validate that at least one path property is provided
1042
+ if (!_wizard.pluginPath && !_wizard.path) {
1043
+ throw new PluginBuildError('Config wizard must provide either pluginPath or path (deprecated)', 'Add pluginPath: "/main" to your configWizard definition', { wizard: _wizard.title });
1044
+ }
1045
+ // Warn if using deprecated properties
1046
+ if (_wizard.path) {
1047
+ log(` ⚠️ Warning: path is deprecated in configWizard. Use pluginPath instead.`);
1048
+ }
1049
+ if (_wizard.hash) {
1050
+ log(` ⚠️ Warning: hash is deprecated in configWizard. Use pluginPathHash instead.`);
1051
+ }
1726
1052
  const wizardCap = {
1727
1053
  type: 'config-wizard',
1728
1054
  screen: {
1729
- path: _wizard.path,
1730
1055
  title: _wizard.title,
1731
1056
  width: _wizard.width,
1732
1057
  height: _wizard.height
1733
1058
  },
1734
1059
  required: true
1735
1060
  };
1736
- // Add hash if provided
1737
- if (_wizard.hash) {
1738
- wizardCap.screen.hash = _wizard.hash;
1061
+ // Prefer new properties, fallback to deprecated ones
1062
+ const effectivePluginPath = _wizard.pluginPath || _wizard.path;
1063
+ const effectivePluginPathHash = _wizard.pluginPathHash || _wizard.hash;
1064
+ if (effectivePluginPath) {
1065
+ wizardCap.screen.pluginPath = effectivePluginPath;
1066
+ }
1067
+ if (effectivePluginPathHash) {
1068
+ wizardCap.screen.pluginPathHash = effectivePluginPathHash;
1739
1069
  }
1740
1070
  // Add shouldShow as method name reference if provided
1741
1071
  if (_wizard.shouldShow) {
@@ -1745,28 +1075,56 @@ function definePlugin(id, name, version) {
1745
1075
  }
1746
1076
  // Add settings
1747
1077
  if (_settings) {
1078
+ // Validate that at least one path property is provided
1079
+ if (!_settings.pluginPath && !_settings.path) {
1080
+ throw new PluginBuildError('Settings must provide either pluginPath or path (deprecated)', 'Add pluginPath: "/settings" to your settings definition', { settings: _settings.title });
1081
+ }
1082
+ // Warn if using deprecated properties
1083
+ if (_settings.path) {
1084
+ log(` ⚠️ Warning: path is deprecated in settings. Use pluginPath instead.`);
1085
+ }
1086
+ if (_settings.hash) {
1087
+ log(` ⚠️ Warning: hash is deprecated in settings. Use pluginPathHash instead.`);
1088
+ }
1748
1089
  const settingsCap = {
1749
1090
  type: 'settings-screen',
1750
1091
  path: `/plugins/${id}/settings`,
1751
1092
  name: _settings.title,
1752
1093
  screen: {
1753
- path: _settings.path,
1754
1094
  title: _settings.title
1755
1095
  }
1756
1096
  };
1757
- // Add hash if provided
1758
- if (_settings.hash) {
1759
- settingsCap.screen.hash = _settings.hash;
1097
+ // Prefer new properties, fallback to deprecated ones
1098
+ const effectivePluginPath = _settings.pluginPath || _settings.path;
1099
+ const effectivePluginPathHash = _settings.pluginPathHash || _settings.hash;
1100
+ if (effectivePluginPath) {
1101
+ settingsCap.screen.pluginPath = effectivePluginPath;
1102
+ }
1103
+ if (effectivePluginPathHash) {
1104
+ settingsCap.screen.pluginPathHash = effectivePluginPathHash;
1760
1105
  }
1761
1106
  _capabilities.push(settingsCap);
1762
1107
  }
1108
+ // Add functions
1109
+ for (const [fname, func] of _functionRegistry.functions) {
1110
+ const functionCap = {
1111
+ type: 'function',
1112
+ name: fname,
1113
+ description: func.description,
1114
+ input: func.input,
1115
+ output: func.output
1116
+ };
1117
+ if (func.tags && func.tags.length > 0) {
1118
+ functionCap.tags = func.tags;
1119
+ }
1120
+ if (packageName) {
1121
+ functionCap.serviceName = packageName;
1122
+ }
1123
+ _capabilities.push(functionCap);
1124
+ }
1763
1125
  // ========== Build Plugin Class ==========
1764
1126
  // MAJK expects a class/constructor, not an instance
1765
1127
  // 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
1128
  // TODO: Implement client generation if configured
1771
1129
  if (_clientConfig) {
1772
1130
  // This would generate the client files based on the function registry
@@ -1815,12 +1173,14 @@ function definePlugin(id, name, version) {
1815
1173
  return class extends BuiltPlugin {
1816
1174
  constructor() {
1817
1175
  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,
1176
+ // Pass function registry if functions were defined
1177
+ _functionRegistry.functions.size > 0 ? _functionRegistry : undefined,
1820
1178
  // Pass configurable entities
1821
1179
  _configurableEntities.length > 0 ? _configurableEntities : undefined,
1822
1180
  // Pass secret providers
1823
- _secretProviders.length > 0 ? _secretProviders : undefined);
1181
+ _secretProviders.length > 0 ? _secretProviders : undefined,
1182
+ // Pass package name for RPC service registration
1183
+ packageName);
1824
1184
  }
1825
1185
  };
1826
1186
  }