@majkapp/plugin-kit 3.2.1 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/FULL.md ADDED
@@ -0,0 +1,848 @@
1
+ # Complete Reference
2
+
3
+ Comprehensive reference for @majkapp/plugin-kit covering all features and patterns.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Quick Start](#quick-start)
8
+ 2. [Plugin Definition](#plugin-definition)
9
+ 3. [Functions](#functions)
10
+ 4. [Screens](#screens)
11
+ 5. [Generated Hooks](#generated-hooks)
12
+ 6. [Context API](#context-api)
13
+ 7. [Services](#services)
14
+ 8. [Lifecycle](#lifecycle)
15
+ 9. [Testing](#testing)
16
+ 10. [Configuration](#configuration)
17
+ 11. [Best Practices](#best-practices)
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ // src/index.ts
23
+ import { definePlugin } from '@majkapp/plugin-kit';
24
+
25
+ const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
26
+ .pluginRoot(__dirname)
27
+ .function('health', {
28
+ description: 'Check plugin health',
29
+ input: { type: 'object', properties: {}, additionalProperties: false },
30
+ output: {
31
+ type: 'object',
32
+ properties: {
33
+ status: { type: 'string' },
34
+ timestamp: { type: 'string' }
35
+ }
36
+ },
37
+ handler: async (_, ctx) => ({
38
+ status: 'ok',
39
+ timestamp: new Date().toISOString()
40
+ })
41
+ })
42
+ .screenReact({
43
+ id: 'my-plugin-main',
44
+ name: 'My Plugin',
45
+ description: 'Main screen',
46
+ route: '/plugin-screens/my-plugin/main',
47
+ pluginPath: '/index.html'
48
+ })
49
+ .build();
50
+
51
+ export = plugin;
52
+ ```
53
+
54
+ ## Plugin Definition
55
+
56
+ ### definePlugin()
57
+
58
+ ```typescript
59
+ definePlugin(id: string, name: string, version: string)
60
+ ```
61
+
62
+ - **id**: Unique identifier (lowercase, hyphens)
63
+ - **name**: Display name
64
+ - **version**: Semantic version
65
+
66
+ ### .pluginRoot()
67
+
68
+ **REQUIRED.** Enables resource loading.
69
+
70
+ ```typescript
71
+ .pluginRoot(__dirname)
72
+ ```
73
+
74
+ ### .build()
75
+
76
+ **REQUIRED.** Finalizes plugin definition.
77
+
78
+ ```typescript
79
+ .build()
80
+ ```
81
+
82
+ Always at the end.
83
+
84
+ ## Functions
85
+
86
+ Define callable operations with typed inputs/outputs.
87
+
88
+ ### Basic Function
89
+
90
+ ```typescript
91
+ .function('functionName', {
92
+ description: string, // REQUIRED: 2-3 sentences
93
+ input: JsonSchema, // REQUIRED: Input schema
94
+ output: JsonSchema, // REQUIRED: Output schema
95
+ handler: HandlerFn, // REQUIRED: Implementation
96
+ tags?: string[], // Optional: Organization tags
97
+ deprecated?: boolean // Optional: Mark deprecated
98
+ })
99
+ ```
100
+
101
+ ### Input/Output Schemas
102
+
103
+ JSON Schema for validation:
104
+
105
+ ```typescript
106
+ // Simple types
107
+ { type: 'string' }
108
+ { type: 'number' }
109
+ { type: 'boolean' }
110
+
111
+ // Enums
112
+ { type: 'string', enum: ['value1', 'value2'] }
113
+
114
+ // Objects
115
+ {
116
+ type: 'object',
117
+ properties: {
118
+ name: { type: 'string' },
119
+ age: { type: 'number' }
120
+ },
121
+ required: ['name'],
122
+ additionalProperties: false
123
+ }
124
+
125
+ // Arrays
126
+ {
127
+ type: 'array',
128
+ items: { type: 'string' }
129
+ }
130
+ ```
131
+
132
+ ### Handler Function
133
+
134
+ ```typescript
135
+ handler: async (input, ctx) => {
136
+ // ctx.logger - Logging
137
+ // ctx.storage - Key-value store
138
+ // ctx.majk - MAJK APIs
139
+
140
+ return output; // Must match output schema
141
+ }
142
+ ```
143
+
144
+ ### Example: Analytics Function
145
+
146
+ ```typescript
147
+ .function('getAnalytics', {
148
+ description: 'Get analytics for specified time period',
149
+ input: {
150
+ type: 'object',
151
+ properties: {
152
+ period: { type: 'string', enum: ['24h', '7d', '30d'] }
153
+ },
154
+ required: ['period']
155
+ },
156
+ output: {
157
+ type: 'object',
158
+ properties: {
159
+ period: { type: 'string' },
160
+ metrics: {
161
+ type: 'object',
162
+ properties: {
163
+ activeUsers: { type: 'number' },
164
+ totalSessions: { type: 'number' }
165
+ }
166
+ }
167
+ }
168
+ },
169
+ handler: async (input, ctx) => {
170
+ const sessions = await ctx.storage.get('sessions') || [];
171
+ // Calculate metrics...
172
+ return { period: input.period, metrics: { /* ... */ } };
173
+ }
174
+ })
175
+ ```
176
+
177
+ ## Screens
178
+
179
+ Define UI screens loaded in MAJK.
180
+
181
+ ### React Screen
182
+
183
+ ```typescript
184
+ .screenReact({
185
+ id: string, // Unique ID
186
+ name: string, // Display name
187
+ description: string, // 2-3 sentences
188
+ route: `/plugin-screens/${id}/path`, // Must start with prefix
189
+ pluginPath: string, // Path to built app
190
+ pluginPathHash?: string // Optional hash route
191
+ })
192
+ ```
193
+
194
+ ### Example: Multiple Screens
195
+
196
+ ```typescript
197
+ .screenReact({
198
+ id: 'my-plugin-dashboard',
199
+ name: 'Dashboard',
200
+ description: 'Main dashboard',
201
+ route: '/plugin-screens/my-plugin/dashboard',
202
+ pluginPath: '/index.html',
203
+ pluginPathHash: '#/'
204
+ })
205
+
206
+ .screenReact({
207
+ id: 'my-plugin-settings',
208
+ name: 'Settings',
209
+ description: 'Plugin settings',
210
+ route: '/plugin-screens/my-plugin/settings',
211
+ pluginPath: '/index.html',
212
+ pluginPathHash: '#/settings'
213
+ })
214
+ ```
215
+
216
+ ### React App Setup
217
+
218
+ ```typescript
219
+ // ui/src/App.tsx
220
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
221
+
222
+ export function App() {
223
+ return (
224
+ <BrowserRouter basename={window.__MAJK_IFRAME_BASE__}>
225
+ <Routes>
226
+ <Route path="/" element={<DashboardPage />} />
227
+ <Route path="/settings" element={<SettingsPage />} />
228
+ </Routes>
229
+ </BrowserRouter>
230
+ );
231
+ }
232
+ ```
233
+
234
+ ## Generated Hooks
235
+
236
+ Auto-generated React hooks from functions.
237
+
238
+ ### Query Hooks
239
+
240
+ Auto-fetch on mount:
241
+
242
+ ```typescript
243
+ const { data, error, loading, refetch } = useHealth(
244
+ input?,
245
+ options?: {
246
+ enabled?: boolean,
247
+ refetchInterval?: number
248
+ }
249
+ );
250
+ ```
251
+
252
+ ### Mutation Hooks
253
+
254
+ Call manually:
255
+
256
+ ```typescript
257
+ const { mutate, data, error, loading } = useClearEvents();
258
+
259
+ await mutate(input);
260
+ ```
261
+
262
+ ### Example: Using Hooks
263
+
264
+ ```typescript
265
+ import { useHealth, useGetAnalytics, useClearEvents } from './generated/hooks';
266
+
267
+ export function DashboardPage() {
268
+ // Query - auto-fetches
269
+ const { data: health } = useHealth();
270
+ const { data: analytics } = useGetAnalytics({ period: '7d' });
271
+
272
+ // Mutation - call manually
273
+ const { mutate: clearEvents } = useClearEvents();
274
+
275
+ return (
276
+ <div>
277
+ <div data-majk-id="healthStatus">{health?.status}</div>
278
+ <button onClick={() => clearEvents({})}>Clear</button>
279
+ </div>
280
+ );
281
+ }
282
+ ```
283
+
284
+ ## Context API
285
+
286
+ Handler context provides access to MAJK.
287
+
288
+ ### ctx.logger
289
+
290
+ ```typescript
291
+ ctx.logger.debug(message, data);
292
+ ctx.logger.info(message, data);
293
+ ctx.logger.warn(message, data);
294
+ ctx.logger.error(message, data);
295
+ ```
296
+
297
+ ### ctx.storage
298
+
299
+ Plugin-scoped key-value store:
300
+
301
+ ```typescript
302
+ await ctx.storage.get(key);
303
+ await ctx.storage.set(key, value);
304
+ await ctx.storage.has(key);
305
+ await ctx.storage.delete(key);
306
+ await ctx.storage.clear();
307
+ ```
308
+
309
+ ### ctx.majk APIs
310
+
311
+ ```typescript
312
+ // Conversations
313
+ await ctx.majk.conversations.list();
314
+ await ctx.majk.conversations.get(id);
315
+
316
+ // Todos
317
+ await ctx.majk.todos.list();
318
+ await ctx.majk.todos.get(id);
319
+
320
+ // Projects
321
+ await ctx.majk.projects.list();
322
+ await ctx.majk.projects.get(id);
323
+
324
+ // Teammates
325
+ await ctx.majk.teammates.list();
326
+ await ctx.majk.teammates.get(id);
327
+
328
+ // MCP Servers
329
+ await ctx.majk.mcpServers.list();
330
+ await ctx.majk.mcpServers.get(id);
331
+
332
+ // Event Bus (use in .onReady())
333
+ const subscription = await ctx.majk.eventBus.subscribeAll(handler);
334
+ ```
335
+
336
+ ## Services
337
+
338
+ Group related functions together.
339
+
340
+ ### Service Definition
341
+
342
+ ```typescript
343
+ .service(serviceName, {
344
+ type: string,
345
+ metadata: {
346
+ name: string,
347
+ description: string,
348
+ version: string
349
+ },
350
+ discoverable?: boolean
351
+ })
352
+ .withFunction(functionName, {
353
+ examples?: string[],
354
+ tags?: string[]
355
+ })
356
+ .endService()
357
+ ```
358
+
359
+ ### Example
360
+
361
+ ```typescript
362
+ .function('health', { /* ... */, tags: ['monitoring'] })
363
+ .function('getStats', { /* ... */, tags: ['monitoring'] })
364
+
365
+ .service('plugin:my-plugin:monitoring', {
366
+ type: '@my-plugin/monitoring',
367
+ metadata: {
368
+ name: 'Monitoring Service',
369
+ description: 'Health checks and statistics',
370
+ version: '1.0.0'
371
+ }
372
+ })
373
+ .withFunction('health', {
374
+ examples: ['Check plugin health']
375
+ })
376
+ .withFunction('getStats', {
377
+ examples: ['Get statistics']
378
+ })
379
+ .endService()
380
+ ```
381
+
382
+ ## Lifecycle
383
+
384
+ Initialize and clean up resources.
385
+
386
+ ### .onReady()
387
+
388
+ ```typescript
389
+ .onReady(async (ctx, cleanup) => {
390
+ // Initialization
391
+ const subscription = await ctx.majk.eventBus.subscribeAll(handler);
392
+
393
+ // Register cleanup
394
+ cleanup(() => {
395
+ subscription.unsubscribe();
396
+ });
397
+ })
398
+ ```
399
+
400
+ ### Example: Event Subscription
401
+
402
+ ```typescript
403
+ const eventLog = [];
404
+
405
+ .function('getEvents', {
406
+ /* ... */
407
+ handler: async () => ({ events: eventLog })
408
+ })
409
+
410
+ .onReady(async (ctx, cleanup) => {
411
+ const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
412
+ eventLog.unshift({
413
+ id: `${Date.now()}`,
414
+ type: event.type,
415
+ entityType: event.entityType,
416
+ timestamp: new Date().toISOString()
417
+ });
418
+
419
+ if (eventLog.length > 1000) {
420
+ eventLog.length = 1000;
421
+ }
422
+ });
423
+
424
+ cleanup(() => {
425
+ subscription.unsubscribe();
426
+ });
427
+ })
428
+ ```
429
+
430
+ ## Testing
431
+
432
+ Test functions and UI with @majkapp/plugin-test.
433
+
434
+ ### Function Tests
435
+
436
+ ```typescript
437
+ import { test, invoke, mock } from '@majkapp/plugin-test';
438
+ import assert from 'assert';
439
+
440
+ test('function test', async () => {
441
+ const context = mock()
442
+ .storage({ key: 'value' })
443
+ .withMajkData({
444
+ conversations: [{ id: 'c1', title: 'Test' }]
445
+ })
446
+ .build();
447
+
448
+ const result = await invoke('functionName', { input }, { context });
449
+
450
+ assert.strictEqual(result.expected, 'value');
451
+ });
452
+ ```
453
+
454
+ ### UI Tests
455
+
456
+ ```typescript
457
+ import { test, bot, mock } from '@majkapp/plugin-test';
458
+ import assert from 'assert';
459
+
460
+ test('ui test', async () => {
461
+ const context = mock()
462
+ .withMockHandler('health', async () => ({
463
+ status: 'ok',
464
+ timestamp: new Date().toISOString()
465
+ }))
466
+ .build();
467
+
468
+ const b = await bot(context);
469
+ await b.goto('/plugin-screens/my-plugin/main');
470
+
471
+ await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');
472
+
473
+ const text = await b.getText('[data-majk-id="healthStatus"]');
474
+ assert.ok(text.includes('ok'));
475
+
476
+ await b.close();
477
+ });
478
+ ```
479
+
480
+ ### data-majk-* Attributes
481
+
482
+ ```typescript
483
+ // Identify elements
484
+ <button data-majk-id="saveButton">Save</button>
485
+
486
+ // Track state
487
+ <div data-majk-id="container" data-majk-state="populated">
488
+ {/* content */}
489
+ </div>
490
+ ```
491
+
492
+ ## Configuration
493
+
494
+ ### vite.config.js (EXACT FORMAT)
495
+
496
+ ```javascript
497
+ import { defineConfig } from 'vite';
498
+ import react from '@vitejs/plugin-react';
499
+
500
+ export default defineConfig({
501
+ plugins: [react()],
502
+ root: 'ui',
503
+ base: '', // IMPORTANT: Empty string
504
+ server: {
505
+ port: 3000,
506
+ strictPort: false,
507
+ },
508
+ build: {
509
+ outDir: '../dist',
510
+ assetsDir: 'assets',
511
+ emptyOutDir: true,
512
+ },
513
+ });
514
+ ```
515
+
516
+ ### Project Structure
517
+
518
+ ```
519
+ my-plugin/
520
+ ├── src/
521
+ │ ├── index.ts
522
+ │ └── core/
523
+ ├── ui/
524
+ │ ├── src/
525
+ │ │ ├── App.tsx
526
+ │ │ ├── components/
527
+ │ │ └── generated/
528
+ │ └── vite.config.js
529
+ ├── tests/
530
+ │ └── plugin/
531
+ │ ├── functions/unit/
532
+ │ └── ui/unit/
533
+ ├── dist/
534
+ ├── package.json
535
+ └── tsconfig.json
536
+ ```
537
+
538
+ ## Best Practices
539
+
540
+ ### 1. Decouple Business Logic
541
+
542
+ ```typescript
543
+ // src/core/analytics.ts - Pure logic
544
+ export function calculateAnalytics(sessions, period) {
545
+ // Pure computation, no plugin dependencies
546
+ }
547
+
548
+ // src/index.ts - Plugin wrapper
549
+ .function('getAnalytics', {
550
+ /* ... */
551
+ handler: async (input, ctx) => {
552
+ const sessions = await ctx.storage.get('sessions') || [];
553
+ return calculateAnalytics(sessions, input.period);
554
+ }
555
+ })
556
+ ```
557
+
558
+ ### 2. Always Use data-majk-* Attributes
559
+
560
+ ```typescript
561
+ <button
562
+ data-majk-id="saveButton"
563
+ data-majk-state={saving ? 'saving' : 'idle'}
564
+ onClick={handleSave}
565
+ >
566
+ Save
567
+ </button>
568
+ ```
569
+
570
+ ### 3. Create uiLog Function
571
+
572
+ ```typescript
573
+ .function('uiLog', {
574
+ description: 'Log from UI',
575
+ input: {
576
+ type: 'object',
577
+ properties: {
578
+ level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] },
579
+ component: { type: 'string' },
580
+ message: { type: 'string' },
581
+ data: { type: 'object' }
582
+ },
583
+ required: ['level', 'component', 'message']
584
+ },
585
+ output: {
586
+ type: 'object',
587
+ properties: { success: { type: 'boolean' } }
588
+ },
589
+ handler: async (input, ctx) => {
590
+ ctx.logger[input.level](`[UI:${input.component}] ${input.message}`, input.data);
591
+ return { success: true };
592
+ }
593
+ })
594
+ ```
595
+
596
+ ### 4. Register Cleanup in onReady
597
+
598
+ ```typescript
599
+ .onReady(async (ctx, cleanup) => {
600
+ const subscription = await ctx.majk.eventBus.subscribeAll(handler);
601
+
602
+ // ALWAYS register cleanup
603
+ cleanup(() => {
604
+ subscription.unsubscribe();
605
+ });
606
+ })
607
+ ```
608
+
609
+ ### 5. Service Interfaces for External Dependencies
610
+
611
+ ```typescript
612
+ // src/core/storage-service.ts
613
+ export interface StorageService {
614
+ save(key: string, data: any): Promise<void>;
615
+ load(key: string): Promise<any>;
616
+ }
617
+
618
+ // src/core/aws-storage.ts - Real implementation
619
+ export class AWSStorageService implements StorageService {
620
+ // AWS SDK implementation
621
+ }
622
+
623
+ // src/core/mock-storage.ts - Mock for testing
624
+ export class MockStorageService implements StorageService {
625
+ // In-memory implementation for tests
626
+ }
627
+ ```
628
+
629
+ ## Complete Example
630
+
631
+ ```typescript
632
+ // src/index.ts
633
+ import { definePlugin } from '@majkapp/plugin-kit';
634
+
635
+ const eventLog: any[] = [];
636
+ const maxEvents = 500;
637
+
638
+ const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
639
+ .pluginRoot(__dirname)
640
+
641
+ // Health function
642
+ .function('health', {
643
+ description: 'Check plugin health',
644
+ input: {
645
+ type: 'object',
646
+ properties: {},
647
+ additionalProperties: false
648
+ },
649
+ output: {
650
+ type: 'object',
651
+ properties: {
652
+ status: { type: 'string' },
653
+ timestamp: { type: 'string' }
654
+ }
655
+ },
656
+ handler: async (_, ctx) => ({
657
+ status: 'ok',
658
+ timestamp: new Date().toISOString()
659
+ }),
660
+ tags: ['monitoring']
661
+ })
662
+
663
+ // Analytics function
664
+ .function('getAnalytics', {
665
+ description: 'Get analytics',
666
+ input: {
667
+ type: 'object',
668
+ properties: {
669
+ period: { type: 'string', enum: ['24h', '7d', '30d'] }
670
+ },
671
+ required: ['period']
672
+ },
673
+ output: {
674
+ type: 'object',
675
+ properties: {
676
+ period: { type: 'string' },
677
+ metrics: {
678
+ type: 'object',
679
+ properties: {
680
+ activeUsers: { type: 'number' },
681
+ totalSessions: { type: 'number' }
682
+ }
683
+ }
684
+ }
685
+ },
686
+ handler: async (input, ctx) => {
687
+ const sessions = await ctx.storage.get('sessions') || [];
688
+ // Calculate...
689
+ return { period: input.period, metrics: { /* ... */ } };
690
+ },
691
+ tags: ['analytics']
692
+ })
693
+
694
+ // Events functions
695
+ .function('getEvents', {
696
+ description: 'Get logged events',
697
+ input: {
698
+ type: 'object',
699
+ properties: {},
700
+ additionalProperties: false
701
+ },
702
+ output: {
703
+ type: 'object',
704
+ properties: {
705
+ events: { type: 'array' },
706
+ count: { type: 'number' }
707
+ }
708
+ },
709
+ handler: async () => ({
710
+ events: eventLog,
711
+ count: eventLog.length
712
+ }),
713
+ tags: ['events']
714
+ })
715
+
716
+ .function('clearEvents', {
717
+ description: 'Clear event log',
718
+ input: {
719
+ type: 'object',
720
+ properties: {},
721
+ additionalProperties: false
722
+ },
723
+ output: {
724
+ type: 'object',
725
+ properties: {
726
+ success: { type: 'boolean' },
727
+ clearedCount: { type: 'number' }
728
+ }
729
+ },
730
+ handler: async (_, ctx) => {
731
+ const count = eventLog.length;
732
+ eventLog.length = 0;
733
+ ctx.logger.info(`Cleared ${count} events`);
734
+ return { success: true, clearedCount: count };
735
+ },
736
+ tags: ['events']
737
+ })
738
+
739
+ // UI logging
740
+ .function('uiLog', {
741
+ description: 'Log from UI',
742
+ input: {
743
+ type: 'object',
744
+ properties: {
745
+ level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] },
746
+ component: { type: 'string' },
747
+ message: { type: 'string' },
748
+ data: { type: 'object' }
749
+ },
750
+ required: ['level', 'component', 'message']
751
+ },
752
+ output: {
753
+ type: 'object',
754
+ properties: { success: { type: 'boolean' } }
755
+ },
756
+ handler: async (input, ctx) => {
757
+ ctx.logger[input.level](`[UI:${input.component}] ${input.message}`, input.data);
758
+ return { success: true };
759
+ },
760
+ tags: ['debugging']
761
+ })
762
+
763
+ // Screens
764
+ .screenReact({
765
+ id: 'my-plugin-dashboard',
766
+ name: 'Dashboard',
767
+ description: 'Main dashboard',
768
+ route: '/plugin-screens/my-plugin/dashboard',
769
+ pluginPath: '/index.html',
770
+ pluginPathHash: '#/'
771
+ })
772
+
773
+ .screenReact({
774
+ id: 'my-plugin-events',
775
+ name: 'Events',
776
+ description: 'Event monitor',
777
+ route: '/plugin-screens/my-plugin/events',
778
+ pluginPath: '/index.html',
779
+ pluginPathHash: '#/events'
780
+ })
781
+
782
+ // Services
783
+ .service('plugin:my-plugin:monitoring', {
784
+ type: '@my-plugin/monitoring',
785
+ metadata: {
786
+ name: 'Monitoring Service',
787
+ description: 'Health checks and analytics',
788
+ version: '1.0.0'
789
+ }
790
+ })
791
+ .withFunction('health')
792
+ .withFunction('getAnalytics')
793
+ .endService()
794
+
795
+ .service('plugin:my-plugin:events', {
796
+ type: '@my-plugin/events',
797
+ metadata: {
798
+ name: 'Event Management',
799
+ description: 'Event logging and monitoring',
800
+ version: '1.0.0'
801
+ }
802
+ })
803
+ .withFunction('getEvents')
804
+ .withFunction('clearEvents')
805
+ .endService()
806
+
807
+ // Lifecycle
808
+ .onReady(async (ctx, cleanup) => {
809
+ ctx.logger.info('My Plugin initializing');
810
+
811
+ // Subscribe to events
812
+ const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
813
+ eventLog.unshift({
814
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
815
+ timestamp: new Date().toISOString(),
816
+ entityType: event.entityType,
817
+ eventType: event.type
818
+ });
819
+
820
+ if (eventLog.length > maxEvents) {
821
+ eventLog.length = maxEvents;
822
+ }
823
+ });
824
+
825
+ cleanup(() => {
826
+ subscription.unsubscribe();
827
+ ctx.logger.info('Event subscription cleaned up');
828
+ });
829
+
830
+ ctx.logger.info('My Plugin ready');
831
+ })
832
+
833
+ .build();
834
+
835
+ export = plugin;
836
+ ```
837
+
838
+ ## Next Steps
839
+
840
+ Run `npx @majkapp/plugin-kit` - Quick start
841
+ Run `npx @majkapp/plugin-kit --functions` - Function patterns
842
+ Run `npx @majkapp/plugin-kit --screens` - Screen configuration
843
+ Run `npx @majkapp/plugin-kit --hooks` - Generated hooks
844
+ Run `npx @majkapp/plugin-kit --context` - Context API
845
+ Run `npx @majkapp/plugin-kit --services` - Service grouping
846
+ Run `npx @majkapp/plugin-kit --lifecycle` - Lifecycle hooks
847
+ Run `npx @majkapp/plugin-kit --testing` - Testing guide
848
+ Run `npx @majkapp/plugin-kit --config` - Project configuration