@majkapp/plugin-kit 1.3.2 → 1.3.4

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.
@@ -0,0 +1,404 @@
1
+ // mcp-dom-agent.js
2
+ // Automatically injected into all MAJK plugin screens for remote control and screenshots
3
+
4
+ (function () {
5
+ if (window.__mcpDomAgent) return;
6
+ window.__mcpDomAgent = true;
7
+
8
+ // ---- Configuration ----
9
+ const SCREENSHOT_ENDPOINT = '/api/mcp/screenshot';
10
+ let replyId = 0;
11
+
12
+ // ---- Helpers ----
13
+
14
+ function getById(id) {
15
+ return document.querySelector('[data-mcp-id="' + CSS.escape(id) + '"]');
16
+ }
17
+
18
+ function centerScroll(el) {
19
+ el.scrollIntoView({
20
+ behavior: 'instant',
21
+ block: 'center',
22
+ inline: 'center'
23
+ });
24
+ }
25
+
26
+ function elementCenter(el) {
27
+ const r = el.getBoundingClientRect();
28
+ return {
29
+ x: r.left + r.width / 2,
30
+ y: r.top + r.height / 2,
31
+ rect: { x: r.left, y: r.top, width: r.width, height: r.height }
32
+ };
33
+ }
34
+
35
+ function dispatchClick(el) {
36
+ const { x, y } = elementCenter(el);
37
+ ['mousedown', 'mouseup', 'click'].forEach((type) => {
38
+ el.dispatchEvent(new MouseEvent(type, {
39
+ bubbles: true,
40
+ cancelable: true,
41
+ composed: true,
42
+ clientX: x,
43
+ clientY: y,
44
+ button: 0
45
+ }));
46
+ });
47
+ }
48
+
49
+ function dispatchHover(el) {
50
+ const { x, y } = elementCenter(el);
51
+ el.dispatchEvent(new MouseEvent('mousemove', {
52
+ bubbles: true,
53
+ cancelable: true,
54
+ composed: true,
55
+ clientX: x,
56
+ clientY: y
57
+ }));
58
+ }
59
+
60
+ function reply(ev, id, result, error) {
61
+ if (ev.source) {
62
+ ev.source.postMessage(
63
+ {
64
+ __mcpReplyId: id,
65
+ result: result,
66
+ error: error ? String(error) : undefined
67
+ },
68
+ '*'
69
+ );
70
+ }
71
+ }
72
+
73
+ // ---- Screenshot Capture ----
74
+
75
+ async function captureScreenshot(options = {}) {
76
+ try {
77
+ // Use html2canvas if available, otherwise use basic canvas approach
78
+ if (typeof html2canvas !== 'undefined') {
79
+ const canvas = await html2canvas(document.body, {
80
+ allowTaint: true,
81
+ useCORS: true,
82
+ logging: false,
83
+ ...options
84
+ });
85
+ return canvas.toDataURL('image/png');
86
+ } else {
87
+ // Fallback: Use basic canvas API to capture visible viewport
88
+ return captureViewportScreenshot(options);
89
+ }
90
+ } catch (error) {
91
+ console.error('[MCP DOM Agent] Screenshot capture failed:', error);
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ function captureViewportScreenshot(options = {}) {
97
+ return new Promise((resolve, reject) => {
98
+ try {
99
+ const canvas = document.createElement('canvas');
100
+ const ctx = canvas.getContext('2d');
101
+
102
+ canvas.width = window.innerWidth;
103
+ canvas.height = window.innerHeight;
104
+
105
+ // This is a basic fallback - it won't capture actual content
106
+ // but provides a placeholder implementation
107
+ ctx.fillStyle = '#ffffff';
108
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
109
+
110
+ ctx.fillStyle = '#333333';
111
+ ctx.font = '20px Arial';
112
+ ctx.fillText('Screenshot captured at ' + new Date().toISOString(), 50, 50);
113
+ ctx.fillText('(Install html2canvas for full screenshot support)', 50, 80);
114
+
115
+ resolve(canvas.toDataURL('image/png'));
116
+ } catch (error) {
117
+ reject(error);
118
+ }
119
+ });
120
+ }
121
+
122
+ async function sendScreenshotToBackend(dataUrl, metadata = {}) {
123
+ try {
124
+ const baseUrl = window.__MAJK_BASE_URL__ || '';
125
+ const response = await fetch(baseUrl + SCREENSHOT_ENDPOINT, {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/json'
129
+ },
130
+ body: JSON.stringify({
131
+ data: dataUrl,
132
+ metadata: {
133
+ url: window.location.href,
134
+ timestamp: new Date().toISOString(),
135
+ userAgent: navigator.userAgent,
136
+ viewport: {
137
+ width: window.innerWidth,
138
+ height: window.innerHeight
139
+ },
140
+ ...metadata
141
+ }
142
+ })
143
+ });
144
+
145
+ if (!response.ok) {
146
+ throw new Error(`Screenshot upload failed: ${response.status} ${response.statusText}`);
147
+ }
148
+
149
+ const result = await response.json();
150
+ return result;
151
+ } catch (error) {
152
+ console.error('[MCP DOM Agent] Failed to send screenshot to backend:', error);
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ // ---- Main Listener ----
158
+
159
+ window.addEventListener('message', async function (ev) {
160
+ const msg = ev.data;
161
+ if (!msg || !msg.__mcpCmdId) return;
162
+
163
+ const cmdId = msg.__mcpCmdId;
164
+ const type = msg.type;
165
+ const id = msg.id;
166
+
167
+ let el = null;
168
+ if (typeof id === 'string') {
169
+ el = getById(id);
170
+ }
171
+
172
+ try {
173
+ switch (type) {
174
+
175
+ case 'find': {
176
+ reply(ev, cmdId, { exists: !!el });
177
+ break;
178
+ }
179
+
180
+ case 'scrollIntoView': {
181
+ if (!el) throw new Error('not found: ' + id);
182
+ centerScroll(el);
183
+ requestAnimationFrame(function () {
184
+ reply(ev, cmdId, { ok: true });
185
+ });
186
+ break;
187
+ }
188
+
189
+ case 'click': {
190
+ if (!el) throw new Error('not found: ' + id);
191
+ centerScroll(el);
192
+ requestAnimationFrame(function () {
193
+ dispatchClick(el);
194
+ reply(ev, cmdId, { ok: true });
195
+ });
196
+ break;
197
+ }
198
+
199
+ case 'hover': {
200
+ if (!el) throw new Error('not found: ' + id);
201
+ centerScroll(el);
202
+ requestAnimationFrame(function () {
203
+ dispatchHover(el);
204
+ reply(ev, cmdId, { ok: true });
205
+ });
206
+ break;
207
+ }
208
+
209
+ case 'getRect': {
210
+ if (!el) throw new Error('not found: ' + id);
211
+ const r = el.getBoundingClientRect();
212
+ reply(ev, cmdId, {
213
+ x: r.left,
214
+ y: r.top,
215
+ width: r.width,
216
+ height: r.height
217
+ });
218
+ break;
219
+ }
220
+
221
+ case 'captureScreenshot': {
222
+ const options = msg.options || {};
223
+ const metadata = msg.metadata || {};
224
+ const sendToBackend = msg.sendToBackend !== false; // Default true
225
+
226
+ // Capture screenshot
227
+ const dataUrl = await captureScreenshot(options);
228
+
229
+ // Send to backend if requested
230
+ let backendResult = null;
231
+ if (sendToBackend) {
232
+ backendResult = await sendScreenshotToBackend(dataUrl, metadata);
233
+ }
234
+
235
+ reply(ev, cmdId, {
236
+ success: true,
237
+ dataUrl: sendToBackend ? undefined : dataUrl, // Don't send back if already sent to backend
238
+ backend: backendResult
239
+ });
240
+ break;
241
+ }
242
+
243
+ default: {
244
+ reply(ev, cmdId, null, 'unknown type: ' + type);
245
+ }
246
+ }
247
+ } catch (err) {
248
+ reply(ev, cmdId, null, err && err.message ? err.message : String(err));
249
+ }
250
+ });
251
+
252
+ // ---- File Picker Bridge ----
253
+ // Since plugins run in cross-origin iframes, they can't directly use File System Access APIs
254
+ // This bridge sends requests to the parent window which executes the APIs and returns results
255
+
256
+ let filePickerRequestId = 0;
257
+ const pendingFilePickerRequests = new Map();
258
+
259
+ // Listen for file picker results from parent window
260
+ window.addEventListener('message', function (ev) {
261
+ const msg = ev.data;
262
+ if (!msg) return;
263
+
264
+ // Handle directory picker result
265
+ if (msg.type === 'majk:file-picker:directory-picker-result') {
266
+ const pending = pendingFilePickerRequests.get(msg.requestId);
267
+ if (pending) {
268
+ pendingFilePickerRequests.delete(msg.requestId);
269
+ if (msg.success) {
270
+ pending.resolve(msg.result);
271
+ } else {
272
+ pending.reject(new Error(msg.error?.message || 'Directory picker failed'));
273
+ }
274
+ }
275
+ }
276
+
277
+ // Handle open file picker result
278
+ if (msg.type === 'majk:file-picker:open-file-picker-result') {
279
+ const pending = pendingFilePickerRequests.get(msg.requestId);
280
+ if (pending) {
281
+ pendingFilePickerRequests.delete(msg.requestId);
282
+ if (msg.success) {
283
+ pending.resolve(msg.results);
284
+ } else {
285
+ pending.reject(new Error(msg.error?.message || 'File picker failed'));
286
+ }
287
+ }
288
+ }
289
+
290
+ // Handle save file picker result
291
+ if (msg.type === 'majk:file-picker:save-file-picker-result') {
292
+ const pending = pendingFilePickerRequests.get(msg.requestId);
293
+ if (pending) {
294
+ pendingFilePickerRequests.delete(msg.requestId);
295
+ if (msg.success) {
296
+ pending.resolve(msg.result);
297
+ } else {
298
+ pending.reject(new Error(msg.error?.message || 'Save file picker failed'));
299
+ }
300
+ }
301
+ }
302
+ });
303
+
304
+ // Polyfill File System Access APIs to use the bridge
305
+ async function showDirectoryPickerBridge(options) {
306
+ const requestId = 'file-picker-' + (filePickerRequestId++);
307
+
308
+ return new Promise((resolve, reject) => {
309
+ pendingFilePickerRequests.set(requestId, { resolve, reject });
310
+
311
+ // Send request to parent window
312
+ window.parent.postMessage({
313
+ type: 'majk:file-picker:show-directory-picker',
314
+ requestId,
315
+ options
316
+ }, '*');
317
+
318
+ // Timeout after 5 minutes
319
+ setTimeout(() => {
320
+ if (pendingFilePickerRequests.has(requestId)) {
321
+ pendingFilePickerRequests.delete(requestId);
322
+ reject(new Error('Directory picker request timed out'));
323
+ }
324
+ }, 300000);
325
+ });
326
+ }
327
+
328
+ async function showOpenFilePickerBridge(options) {
329
+ const requestId = 'file-picker-' + (filePickerRequestId++);
330
+
331
+ return new Promise((resolve, reject) => {
332
+ pendingFilePickerRequests.set(requestId, { resolve, reject });
333
+
334
+ // Send request to parent window
335
+ window.parent.postMessage({
336
+ type: 'majk:file-picker:show-open-file-picker',
337
+ requestId,
338
+ options
339
+ }, '*');
340
+
341
+ // Timeout after 5 minutes
342
+ setTimeout(() => {
343
+ if (pendingFilePickerRequests.has(requestId)) {
344
+ pendingFilePickerRequests.delete(requestId);
345
+ reject(new Error('File picker request timed out'));
346
+ }
347
+ }, 300000);
348
+ });
349
+ }
350
+
351
+ async function showSaveFilePickerBridge(options) {
352
+ const requestId = 'file-picker-' + (filePickerRequestId++);
353
+
354
+ return new Promise((resolve, reject) => {
355
+ pendingFilePickerRequests.set(requestId, { resolve, reject });
356
+
357
+ // Send request to parent window
358
+ window.parent.postMessage({
359
+ type: 'majk:file-picker:show-save-file-picker',
360
+ requestId,
361
+ options
362
+ }, '*');
363
+
364
+ // Timeout after 5 minutes
365
+ setTimeout(() => {
366
+ if (pendingFilePickerRequests.has(requestId)) {
367
+ pendingFilePickerRequests.delete(requestId);
368
+ reject(new Error('Save file picker request timed out'));
369
+ }
370
+ }, 300000);
371
+ });
372
+ }
373
+
374
+ // ---- Global API for direct access ----
375
+ window.mcpDomAgent = {
376
+ version: '1.1.0',
377
+ captureScreenshot: async (options) => {
378
+ const dataUrl = await captureScreenshot(options);
379
+ return dataUrl;
380
+ },
381
+ captureAndSend: async (metadata) => {
382
+ const dataUrl = await captureScreenshot();
383
+ const result = await sendScreenshotToBackend(dataUrl, metadata);
384
+ return result;
385
+ },
386
+ // File picker APIs - use these instead of native File System Access APIs in plugin iframes
387
+ showDirectoryPicker: showDirectoryPickerBridge,
388
+ showOpenFilePicker: showOpenFilePickerBridge,
389
+ showSaveFilePicker: showSaveFilePickerBridge
390
+ };
391
+
392
+ // Also expose file pickers as global functions for convenience (matching native API)
393
+ if (!window.showDirectoryPicker) {
394
+ window.showDirectoryPicker = showDirectoryPickerBridge;
395
+ }
396
+ if (!window.showOpenFilePicker) {
397
+ window.showOpenFilePicker = showOpenFilePickerBridge;
398
+ }
399
+ if (!window.showSaveFilePicker) {
400
+ window.showSaveFilePicker = showSaveFilePickerBridge;
401
+ }
402
+
403
+ console.log('[MCP DOM Agent] Initialized v1.1.0 with File Picker Bridge');
404
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@majkapp/plugin-kit",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Fluent builder framework for creating robust MAJK plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,7 +21,7 @@
21
21
  "README.md"
22
22
  ],
23
23
  "scripts": {
24
- "build": "tsc",
24
+ "build": "tsc && cp src/mcp-dom-agent.js dist/mcp-dom-agent.js",
25
25
  "watch": "tsc --watch",
26
26
  "clean": "rm -rf dist"
27
27
  },