@majkapp/plugin-kit 3.2.0 → 3.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.
- package/bin/promptable-cli.js +35 -0
- package/dist/generator/generator.js +12 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/plugin-kit.d.ts +34 -1
- package/dist/plugin-kit.d.ts.map +1 -1
- package/dist/plugin-kit.js +87 -1
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/API.md +394 -0
- package/docs/CONFIG.md +428 -0
- package/docs/CONTEXT.md +500 -0
- package/docs/FULL.md +848 -0
- package/docs/FUNCTIONS.md +623 -0
- package/docs/HOOKS.md +532 -0
- package/docs/INDEX.md +486 -0
- package/docs/LIFECYCLE.md +490 -0
- package/docs/SCREENS.md +547 -0
- package/docs/SERVICES.md +350 -0
- package/docs/TESTING.md +593 -0
- package/docs/mcp-execution-api.md +490 -0
- package/package.json +18 -3
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
Plugins have a lifecycle with setup and teardown phases. Use `.onReady()` for initialization.
|
|
4
|
+
|
|
5
|
+
## onReady
|
|
6
|
+
|
|
7
|
+
Called once when the plugin is loaded. Use for subscriptions, timers, and async setup.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// src/index.ts
|
|
11
|
+
import { definePlugin } from '@majkapp/plugin-kit';
|
|
12
|
+
|
|
13
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
14
|
+
.pluginRoot(__dirname)
|
|
15
|
+
|
|
16
|
+
// Define functions first
|
|
17
|
+
.function('getEvents', { /* ... */ })
|
|
18
|
+
.function('clearEvents', { /* ... */ })
|
|
19
|
+
|
|
20
|
+
// Lifecycle hook - called when plugin loads
|
|
21
|
+
.onReady(async (ctx, cleanup) => {
|
|
22
|
+
ctx.logger.info('Plugin initializing...');
|
|
23
|
+
|
|
24
|
+
// Perform async setup
|
|
25
|
+
await initializePluginData(ctx);
|
|
26
|
+
|
|
27
|
+
// Register cleanup (called when plugin unloads/reloads)
|
|
28
|
+
cleanup(() => {
|
|
29
|
+
ctx.logger.info('Plugin cleaning up');
|
|
30
|
+
// Cleanup code here
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
ctx.logger.info('Plugin ready');
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
.build();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Event Bus Subscription (Real Example)
|
|
40
|
+
|
|
41
|
+
From `samples/full-featured/src/index.ts`:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// In-memory event log (scoped to plugin instance)
|
|
45
|
+
const eventLog: any[] = [];
|
|
46
|
+
const maxEvents = 1000;
|
|
47
|
+
|
|
48
|
+
const plugin = definePlugin('full-featured', 'Full Featured', '1.0.0')
|
|
49
|
+
.pluginRoot(__dirname)
|
|
50
|
+
|
|
51
|
+
// Functions that access eventLog
|
|
52
|
+
.function('getEvents', {
|
|
53
|
+
description: 'Get all logged events from the event bus',
|
|
54
|
+
input: { /* ... */ },
|
|
55
|
+
output: { /* ... */ },
|
|
56
|
+
handler: async (input, ctx) => {
|
|
57
|
+
return {
|
|
58
|
+
events: eventLog,
|
|
59
|
+
count: eventLog.length
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
.function('clearEvents', {
|
|
65
|
+
description: 'Clear the event log',
|
|
66
|
+
input: { /* ... */ },
|
|
67
|
+
output: { /* ... */ },
|
|
68
|
+
handler: async (input, ctx) => {
|
|
69
|
+
const count = eventLog.length;
|
|
70
|
+
eventLog.length = 0; // Clear array
|
|
71
|
+
ctx.logger.info(`Cleared ${count} events`);
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
clearedCount: count
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Subscribe to events when plugin loads
|
|
80
|
+
.onReady(async (ctx, cleanup) => {
|
|
81
|
+
ctx.logger.info('Full Featured plugin ready - subscribing to all events');
|
|
82
|
+
|
|
83
|
+
// Subscribe to ALL events from MAJK event bus
|
|
84
|
+
// CRITICAL: Must await the subscription (returns Promise<Subscription>)
|
|
85
|
+
const subscription = await ctx.majk.eventBus.subscribeAll((event: any) => {
|
|
86
|
+
// Build a log entry for each event
|
|
87
|
+
const logEntry = {
|
|
88
|
+
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
entityType: event.entityType, // 'conversation', 'todo', 'project', etc.
|
|
91
|
+
eventType: event.type, // 'created', 'updated', 'deleted'
|
|
92
|
+
entityId: event.entity?.id || 'unknown',
|
|
93
|
+
entitySummary: event.entity ? {
|
|
94
|
+
...( event.entity.name && { name: event.entity.name }),
|
|
95
|
+
...( event.entity.title && { title: event.entity.title }),
|
|
96
|
+
...( event.entity.status && { status: event.entity.status })
|
|
97
|
+
} : {},
|
|
98
|
+
metadata: event.metadata || {},
|
|
99
|
+
fullEntity: event.entity
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Add to front of array (newest first), enforce max size
|
|
103
|
+
eventLog.unshift(logEntry);
|
|
104
|
+
if (eventLog.length > maxEvents) {
|
|
105
|
+
eventLog.length = maxEvents; // Truncate old events
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ctx.logger.info(`Event logged: ${event.entityType}.${event.type} - Total: ${eventLog.length}`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ALWAYS register cleanup for subscriptions, timers, etc.
|
|
112
|
+
// This runs when plugin is disabled or reloaded
|
|
113
|
+
cleanup(() => {
|
|
114
|
+
if (subscription && typeof subscription.unsubscribe === 'function') {
|
|
115
|
+
subscription.unsubscribe();
|
|
116
|
+
ctx.logger.info('Unsubscribed from EventBus');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
.build();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Timer Example
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
.onReady(async (ctx, cleanup) => {
|
|
128
|
+
ctx.logger.info('Starting periodic task');
|
|
129
|
+
|
|
130
|
+
// Start interval timer
|
|
131
|
+
const intervalId = setInterval(async () => {
|
|
132
|
+
try {
|
|
133
|
+
// Perform periodic task
|
|
134
|
+
const stats = await calculateStats(ctx);
|
|
135
|
+
await ctx.storage.set('last-stats', stats);
|
|
136
|
+
ctx.logger.info('Stats updated', stats);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
ctx.logger.error('Periodic task failed', { error: error.message });
|
|
139
|
+
}
|
|
140
|
+
}, 60000); // Every 60 seconds
|
|
141
|
+
|
|
142
|
+
// Clean up timer on unload
|
|
143
|
+
cleanup(() => {
|
|
144
|
+
clearInterval(intervalId);
|
|
145
|
+
ctx.logger.info('Stopped periodic task');
|
|
146
|
+
});
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Initialize Storage
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
.onReady(async (ctx, cleanup) => {
|
|
154
|
+
ctx.logger.info('Initializing plugin storage');
|
|
155
|
+
|
|
156
|
+
// Check if first run
|
|
157
|
+
const initialized = await ctx.storage.get('initialized');
|
|
158
|
+
|
|
159
|
+
if (!initialized) {
|
|
160
|
+
// First-time setup
|
|
161
|
+
await ctx.storage.set('initialized', true);
|
|
162
|
+
await ctx.storage.set('sessions', []);
|
|
163
|
+
await ctx.storage.set('settings', {
|
|
164
|
+
theme: 'light',
|
|
165
|
+
notifications: true
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
ctx.logger.info('Plugin storage initialized');
|
|
169
|
+
} else {
|
|
170
|
+
ctx.logger.info('Plugin storage already initialized');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// No cleanup needed for storage initialization
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Multiple Cleanups
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
.onReady(async (ctx, cleanup) => {
|
|
181
|
+
// Setup 1: Event subscription
|
|
182
|
+
const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
|
|
183
|
+
// Handle event
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
cleanup(() => {
|
|
187
|
+
subscription.unsubscribe();
|
|
188
|
+
ctx.logger.info('Unsubscribed from events');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Setup 2: Timer
|
|
192
|
+
const timerId = setInterval(() => {
|
|
193
|
+
// Periodic task
|
|
194
|
+
}, 30000);
|
|
195
|
+
|
|
196
|
+
cleanup(() => {
|
|
197
|
+
clearInterval(timerId);
|
|
198
|
+
ctx.logger.info('Stopped timer');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Setup 3: External connection
|
|
202
|
+
const connection = await connectToExternalService();
|
|
203
|
+
|
|
204
|
+
cleanup(async () => {
|
|
205
|
+
await connection.disconnect();
|
|
206
|
+
ctx.logger.info('Disconnected from external service');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
ctx.logger.info('Plugin fully initialized');
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Error Handling in onReady
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
.onReady(async (ctx, cleanup) => {
|
|
217
|
+
try {
|
|
218
|
+
ctx.logger.info('Plugin initializing...');
|
|
219
|
+
|
|
220
|
+
// Attempt initialization
|
|
221
|
+
const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
|
|
222
|
+
// Handle event
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
cleanup(() => {
|
|
226
|
+
subscription.unsubscribe();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
ctx.logger.info('Plugin initialized successfully');
|
|
230
|
+
|
|
231
|
+
} catch (error) {
|
|
232
|
+
ctx.logger.error('Plugin initialization failed', {
|
|
233
|
+
error: error.message,
|
|
234
|
+
stack: error.stack
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Plugin will still load, but initialization failed
|
|
238
|
+
// Consider storing error state for functions to check
|
|
239
|
+
await ctx.storage.set('init-error', error.message);
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## When to Use onReady
|
|
245
|
+
|
|
246
|
+
**Use onReady for:**
|
|
247
|
+
- Event bus subscriptions
|
|
248
|
+
- Timers and intervals
|
|
249
|
+
- External service connections
|
|
250
|
+
- Background workers
|
|
251
|
+
- Initial data loading
|
|
252
|
+
- Storage initialization
|
|
253
|
+
|
|
254
|
+
**Don't use onReady for:**
|
|
255
|
+
- Simple function definitions (no setup needed)
|
|
256
|
+
- Synchronous operations (use functions instead)
|
|
257
|
+
- Operations that should run per-request (use function handlers)
|
|
258
|
+
|
|
259
|
+
## Cleanup Patterns
|
|
260
|
+
|
|
261
|
+
### Pattern 1: Subscription Cleanup
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
cleanup(() => {
|
|
265
|
+
if (subscription && typeof subscription.unsubscribe === 'function') {
|
|
266
|
+
subscription.unsubscribe();
|
|
267
|
+
ctx.logger.info('Unsubscribed');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Pattern 2: Timer Cleanup
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
cleanup(() => {
|
|
276
|
+
clearInterval(intervalId);
|
|
277
|
+
clearTimeout(timeoutId);
|
|
278
|
+
ctx.logger.info('Timers cleared');
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Pattern 3: Async Cleanup
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
cleanup(async () => {
|
|
286
|
+
await connection.close();
|
|
287
|
+
await saveState(ctx);
|
|
288
|
+
ctx.logger.info('Async cleanup complete');
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Pattern 4: Resource Cleanup
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
cleanup(() => {
|
|
296
|
+
try {
|
|
297
|
+
resource.release();
|
|
298
|
+
ctx.logger.info('Resource released');
|
|
299
|
+
} catch (error) {
|
|
300
|
+
ctx.logger.error('Cleanup failed', { error: error.message });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Testing with onReady
|
|
306
|
+
|
|
307
|
+
onReady is tested through integration tests:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// tests/plugin/integration/lifecycle.test.js
|
|
311
|
+
import { test } from '@majkapp/plugin-test';
|
|
312
|
+
import { loadPlugin, unloadPlugin } from '@majkapp/plugin-test';
|
|
313
|
+
import assert from 'assert';
|
|
314
|
+
|
|
315
|
+
test('plugin initializes event subscription', async () => {
|
|
316
|
+
const plugin = await loadPlugin();
|
|
317
|
+
|
|
318
|
+
// Trigger an event
|
|
319
|
+
await triggerTestEvent({ type: 'test', data: {} });
|
|
320
|
+
|
|
321
|
+
// Wait briefly for event to be processed
|
|
322
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
323
|
+
|
|
324
|
+
// Verify event was logged
|
|
325
|
+
const result = await invoke('getEvents', {}, { context: plugin.context });
|
|
326
|
+
assert.ok(result.events.length > 0);
|
|
327
|
+
|
|
328
|
+
// Cleanup
|
|
329
|
+
await unloadPlugin(plugin);
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Best Practices
|
|
334
|
+
|
|
335
|
+
### 1. Always Register Cleanup
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// Good: Cleanup registered
|
|
339
|
+
.onReady(async (ctx, cleanup) => {
|
|
340
|
+
const subscription = await ctx.majk.eventBus.subscribeAll(handler);
|
|
341
|
+
|
|
342
|
+
cleanup(() => {
|
|
343
|
+
subscription.unsubscribe();
|
|
344
|
+
});
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
// Bad: No cleanup - memory leak!
|
|
348
|
+
.onReady(async (ctx, cleanup) => {
|
|
349
|
+
const subscription = await ctx.majk.eventBus.subscribeAll(handler);
|
|
350
|
+
// Missing cleanup!
|
|
351
|
+
})
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### 2. Log Initialization Steps
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
.onReady(async (ctx, cleanup) => {
|
|
358
|
+
ctx.logger.info('Plugin initializing...');
|
|
359
|
+
|
|
360
|
+
const subscription = await ctx.majk.eventBus.subscribeAll(handler);
|
|
361
|
+
ctx.logger.info('Event subscription created');
|
|
362
|
+
|
|
363
|
+
cleanup(() => {
|
|
364
|
+
subscription.unsubscribe();
|
|
365
|
+
ctx.logger.info('Event subscription cleaned up');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
ctx.logger.info('Plugin initialization complete');
|
|
369
|
+
})
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### 3. Handle Errors Gracefully
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
.onReady(async (ctx, cleanup) => {
|
|
376
|
+
try {
|
|
377
|
+
const subscription = await ctx.majk.eventBus.subscribeAll(handler);
|
|
378
|
+
|
|
379
|
+
cleanup(() => {
|
|
380
|
+
try {
|
|
381
|
+
subscription.unsubscribe();
|
|
382
|
+
} catch (error) {
|
|
383
|
+
ctx.logger.error('Cleanup error', { error: error.message });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
ctx.logger.error('Initialization error', { error: error.message });
|
|
388
|
+
// Store error state for functions to check
|
|
389
|
+
await ctx.storage.set('init-error', error.message);
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Complete Example
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// src/index.ts
|
|
398
|
+
import { definePlugin } from '@majkapp/plugin-kit';
|
|
399
|
+
|
|
400
|
+
const eventLog: any[] = [];
|
|
401
|
+
const maxEvents = 500;
|
|
402
|
+
let statsTimer: NodeJS.Timeout;
|
|
403
|
+
|
|
404
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
405
|
+
.pluginRoot(__dirname)
|
|
406
|
+
|
|
407
|
+
.function('getEvents', {
|
|
408
|
+
description: 'Get logged events',
|
|
409
|
+
input: { /* ... */ },
|
|
410
|
+
output: { /* ... */ },
|
|
411
|
+
handler: async () => ({ events: eventLog })
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
.function('getStats', {
|
|
415
|
+
description: 'Get cached statistics',
|
|
416
|
+
input: { /* ... */ },
|
|
417
|
+
output: { /* ... */ },
|
|
418
|
+
handler: async (input, ctx) => {
|
|
419
|
+
return await ctx.storage.get('cached-stats') || { count: 0 };
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
.onReady(async (ctx, cleanup) => {
|
|
424
|
+
ctx.logger.info('My Plugin initializing...');
|
|
425
|
+
|
|
426
|
+
// 1. Initialize storage
|
|
427
|
+
const initialized = await ctx.storage.get('initialized');
|
|
428
|
+
if (!initialized) {
|
|
429
|
+
await ctx.storage.set('initialized', true);
|
|
430
|
+
await ctx.storage.set('cached-stats', { count: 0, timestamp: Date.now() });
|
|
431
|
+
ctx.logger.info('Storage initialized');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 2. Subscribe to events
|
|
435
|
+
try {
|
|
436
|
+
const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
|
|
437
|
+
eventLog.unshift({
|
|
438
|
+
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
439
|
+
timestamp: new Date().toISOString(),
|
|
440
|
+
type: event.type,
|
|
441
|
+
entityType: event.entityType
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (eventLog.length > maxEvents) {
|
|
445
|
+
eventLog.length = maxEvents;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
cleanup(() => {
|
|
450
|
+
subscription.unsubscribe();
|
|
451
|
+
ctx.logger.info('Event subscription cleaned up');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
ctx.logger.info('Event subscription active');
|
|
455
|
+
} catch (error) {
|
|
456
|
+
ctx.logger.error('Event subscription failed', { error: error.message });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 3. Start periodic stats update
|
|
460
|
+
statsTimer = setInterval(async () => {
|
|
461
|
+
try {
|
|
462
|
+
const stats = {
|
|
463
|
+
count: eventLog.length,
|
|
464
|
+
timestamp: Date.now()
|
|
465
|
+
};
|
|
466
|
+
await ctx.storage.set('cached-stats', stats);
|
|
467
|
+
ctx.logger.debug('Stats updated', stats);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
ctx.logger.error('Stats update failed', { error: error.message });
|
|
470
|
+
}
|
|
471
|
+
}, 60000); // Every 60 seconds
|
|
472
|
+
|
|
473
|
+
cleanup(() => {
|
|
474
|
+
clearInterval(statsTimer);
|
|
475
|
+
ctx.logger.info('Stats timer stopped');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
ctx.logger.info('My Plugin ready');
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
.build();
|
|
482
|
+
|
|
483
|
+
export = plugin;
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Next Steps
|
|
487
|
+
|
|
488
|
+
Run `npx @majkapp/plugin-kit --context` - ctx API for onReady
|
|
489
|
+
Run `npx @majkapp/plugin-kit --testing` - Test plugin lifecycle
|
|
490
|
+
Run `npx @majkapp/plugin-kit --services` - Organize functions into services
|