@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.
@@ -0,0 +1,500 @@
1
+ # Context API
2
+
3
+ Every function handler receives a `ctx` parameter with access to MAJK APIs, storage, and logging.
4
+
5
+ ```typescript
6
+ .function('myFunction', {
7
+ description: 'Example function',
8
+ input: { /* ... */ },
9
+ output: { /* ... */ },
10
+ handler: async (input, ctx) => {
11
+ // ctx.majk - Access MAJK data
12
+ // ctx.storage - Plugin-scoped key-value store
13
+ // ctx.logger - Scoped logging
14
+ return { /* ... */ };
15
+ }
16
+ })
17
+ ```
18
+
19
+ ## ctx.logger
20
+
21
+ Scoped logging automatically includes your plugin name.
22
+
23
+ ```typescript
24
+ handler: async (input, ctx) => {
25
+ // Debug - verbose information for development
26
+ ctx.logger.debug('Processing request', { input });
27
+
28
+ // Info - normal operational messages
29
+ ctx.logger.info('Operation started', { userId: input.userId });
30
+
31
+ // Warn - warning conditions, non-critical issues
32
+ ctx.logger.warn('Rate limit approaching', { count: 95, limit: 100 });
33
+
34
+ // Error - error conditions, failures
35
+ ctx.logger.error('Operation failed', {
36
+ error: error.message,
37
+ stack: error.stack,
38
+ input
39
+ });
40
+
41
+ return { /* ... */ };
42
+ }
43
+ ```
44
+
45
+ **Best practice:** Always log errors with context:
46
+
47
+ ```typescript
48
+ handler: async (input, ctx) => {
49
+ try {
50
+ const result = await performOperation(input);
51
+ ctx.logger.info('Operation succeeded', { result });
52
+ return result;
53
+ } catch (error) {
54
+ ctx.logger.error('Operation failed', {
55
+ error: error.message,
56
+ stack: error.stack,
57
+ input, // Include input for debugging
58
+ timestamp: new Date().toISOString()
59
+ });
60
+ throw error; // Re-throw or return error response
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## ctx.storage
66
+
67
+ Plugin-scoped key-value storage. Data persists between function calls.
68
+
69
+ ```typescript
70
+ // Set a value
71
+ await ctx.storage.set('counter', 42);
72
+ await ctx.storage.set('user-settings', { theme: 'dark', notifications: true });
73
+
74
+ // Get a value (returns undefined if not found)
75
+ const counter = await ctx.storage.get('counter'); // 42
76
+ const settings = await ctx.storage.get('user-settings'); // { theme: 'dark', ... }
77
+
78
+ // Check if key exists
79
+ const exists = await ctx.storage.has('counter'); // true
80
+
81
+ // Delete a key
82
+ await ctx.storage.delete('counter');
83
+
84
+ // Clear all storage (use with caution!)
85
+ await ctx.storage.clear();
86
+ ```
87
+
88
+ ### Example: Counter
89
+
90
+ ```typescript
91
+ .function('incrementCounter', {
92
+ description: 'Increment counter and return new value',
93
+ input: {
94
+ type: 'object',
95
+ properties: {},
96
+ additionalProperties: false
97
+ },
98
+ output: {
99
+ type: 'object',
100
+ properties: {
101
+ counter: { type: 'number' }
102
+ }
103
+ },
104
+ handler: async (input, ctx) => {
105
+ // Get current counter (default to 0)
106
+ const current = await ctx.storage.get('counter') || 0;
107
+
108
+ // Increment
109
+ const newValue = current + 1;
110
+
111
+ // Save
112
+ await ctx.storage.set('counter', newValue);
113
+
114
+ ctx.logger.info(`Counter incremented to ${newValue}`);
115
+
116
+ return { counter: newValue };
117
+ }
118
+ })
119
+ ```
120
+
121
+ ### Example: Session Tracking
122
+
123
+ ```typescript
124
+ .function('trackSession', {
125
+ description: 'Track user session',
126
+ input: {
127
+ type: 'object',
128
+ properties: {
129
+ userId: { type: 'string' },
130
+ duration: { type: 'number' }
131
+ },
132
+ required: ['userId', 'duration']
133
+ },
134
+ output: {
135
+ type: 'object',
136
+ properties: {
137
+ totalSessions: { type: 'number' }
138
+ }
139
+ },
140
+ handler: async (input, ctx) => {
141
+ // Get existing sessions
142
+ const sessions = await ctx.storage.get('sessions') || [];
143
+
144
+ // Add new session
145
+ sessions.push({
146
+ userId: input.userId,
147
+ duration: input.duration,
148
+ timestamp: Date.now()
149
+ });
150
+
151
+ // Save updated sessions
152
+ await ctx.storage.set('sessions', sessions);
153
+
154
+ ctx.logger.info(`Session tracked for user ${input.userId}`, {
155
+ duration: input.duration,
156
+ totalSessions: sessions.length
157
+ });
158
+
159
+ return { totalSessions: sessions.length };
160
+ }
161
+ })
162
+ ```
163
+
164
+ ## ctx.majk.conversations
165
+
166
+ Access MAJK conversations.
167
+
168
+ ```typescript
169
+ handler: async (input, ctx) => {
170
+ // List all conversations
171
+ const conversations = await ctx.majk.conversations.list();
172
+ // Returns: Array<{ id, title, createdAt, updatedAt, ... }>
173
+
174
+ // Get specific conversation
175
+ const conversation = await ctx.majk.conversations.get('conv-id');
176
+ // Returns: { id, title, messages, ... } or null if not found
177
+
178
+ ctx.logger.info(`Found ${conversations.length} conversations`);
179
+
180
+ return { count: conversations.length };
181
+ }
182
+ ```
183
+
184
+ ### Example: Conversation Summary
185
+
186
+ ```typescript
187
+ .function('getConversationStats', {
188
+ description: 'Get statistics across all conversations',
189
+ input: {
190
+ type: 'object',
191
+ properties: {},
192
+ additionalProperties: false
193
+ },
194
+ output: {
195
+ type: 'object',
196
+ properties: {
197
+ totalConversations: { type: 'number' },
198
+ totalMessages: { type: 'number' },
199
+ mostActive: {
200
+ type: 'object',
201
+ properties: {
202
+ id: { type: 'string' },
203
+ title: { type: 'string' },
204
+ messageCount: { type: 'number' }
205
+ }
206
+ }
207
+ }
208
+ },
209
+ handler: async (input, ctx) => {
210
+ const conversations = await ctx.majk.conversations.list();
211
+
212
+ let totalMessages = 0;
213
+ let mostActive = null;
214
+ let maxMessages = 0;
215
+
216
+ for (const conv of conversations) {
217
+ const messageCount = conv.messages?.length || 0;
218
+ totalMessages += messageCount;
219
+
220
+ if (messageCount > maxMessages) {
221
+ maxMessages = messageCount;
222
+ mostActive = {
223
+ id: conv.id,
224
+ title: conv.title || 'Untitled',
225
+ messageCount
226
+ };
227
+ }
228
+ }
229
+
230
+ ctx.logger.info('Conversation stats calculated', {
231
+ totalConversations: conversations.length,
232
+ totalMessages
233
+ });
234
+
235
+ return {
236
+ totalConversations: conversations.length,
237
+ totalMessages,
238
+ mostActive: mostActive || { id: '', title: '', messageCount: 0 }
239
+ };
240
+ }
241
+ })
242
+ ```
243
+
244
+ ## ctx.majk.todos
245
+
246
+ Access MAJK todos.
247
+
248
+ ```typescript
249
+ handler: async (input, ctx) => {
250
+ // List all todos
251
+ const todos = await ctx.majk.todos.list();
252
+ // Returns: Array<{ id, title, status, ... }>
253
+
254
+ // Get specific todo
255
+ const todo = await ctx.majk.todos.get('todo-id');
256
+ // Returns: { id, title, status, ... } or null
257
+
258
+ // Filter by status
259
+ const pending = todos.filter(t => t.status === 'pending');
260
+ const completed = todos.filter(t => t.status === 'completed');
261
+
262
+ return { pending: pending.length, completed: completed.length };
263
+ }
264
+ ```
265
+
266
+ ## ctx.majk.projects
267
+
268
+ Access MAJK projects.
269
+
270
+ ```typescript
271
+ handler: async (input, ctx) => {
272
+ // List all projects
273
+ const projects = await ctx.majk.projects.list();
274
+ // Returns: Array<{ id, name, ... }>
275
+
276
+ // Get specific project
277
+ const project = await ctx.majk.projects.get('project-id');
278
+ // Returns: { id, name, ... } or null
279
+
280
+ return { projectCount: projects.length };
281
+ }
282
+ ```
283
+
284
+ ## ctx.majk.teammates
285
+
286
+ Access MAJK teammates (AI assistants).
287
+
288
+ ```typescript
289
+ handler: async (input, ctx) => {
290
+ // List all teammates
291
+ const teammates = await ctx.majk.teammates.list();
292
+ // Returns: Array<{ id, name, expertise, ... }>
293
+
294
+ // Get specific teammate
295
+ const teammate = await ctx.majk.teammates.get('teammate-id');
296
+ // Returns: { id, name, expertise, skills, ... } or null
297
+
298
+ ctx.logger.info(`Found ${teammates.length} teammates`);
299
+
300
+ return { teammates: teammates.map(t => ({ id: t.id, name: t.name })) };
301
+ }
302
+ ```
303
+
304
+ ## ctx.majk.mcpServers
305
+
306
+ Access MCP (Model Context Protocol) servers.
307
+
308
+ ```typescript
309
+ handler: async (input, ctx) => {
310
+ // List all MCP servers
311
+ const servers = await ctx.majk.mcpServers.list();
312
+ // Returns: Array<{ id, name, config, ... }>
313
+
314
+ // Get specific server
315
+ const server = await ctx.majk.mcpServers.get('server-id');
316
+ // Returns: { id, name, config, ... } or null
317
+
318
+ return { serverCount: servers.length };
319
+ }
320
+ ```
321
+
322
+ ## ctx.majk.eventBus
323
+
324
+ Subscribe to MAJK events (conversations, todos, projects, etc.).
325
+
326
+ **CRITICAL:** Use in `.onReady()` to register cleanup.
327
+
328
+ ```typescript
329
+ .onReady(async (ctx, cleanup) => {
330
+ ctx.logger.info('Subscribing to event bus');
331
+
332
+ // Subscribe to ALL events
333
+ const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
334
+ ctx.logger.info('Event received', {
335
+ entityType: event.entityType, // 'conversation', 'todo', 'project', etc.
336
+ eventType: event.type, // 'created', 'updated', 'deleted'
337
+ entityId: event.entity?.id
338
+ });
339
+
340
+ // Store event for later display
341
+ // ... (see LIFECYCLE.md for full example)
342
+ });
343
+
344
+ // ALWAYS register cleanup to unsubscribe
345
+ cleanup(() => {
346
+ if (subscription && typeof subscription.unsubscribe === 'function') {
347
+ subscription.unsubscribe();
348
+ ctx.logger.info('Unsubscribed from event bus');
349
+ }
350
+ });
351
+ })
352
+ ```
353
+
354
+ ## Complete Example: Dashboard Data
355
+
356
+ ```typescript
357
+ .function('getDashboardData', {
358
+ description: 'Get comprehensive dashboard data from MAJK APIs',
359
+
360
+ input: {
361
+ type: 'object',
362
+ properties: {},
363
+ additionalProperties: false
364
+ },
365
+
366
+ output: {
367
+ type: 'object',
368
+ properties: {
369
+ conversations: {
370
+ type: 'object',
371
+ properties: {
372
+ total: { type: 'number' },
373
+ recent: { type: 'array' }
374
+ }
375
+ },
376
+ todos: {
377
+ type: 'object',
378
+ properties: {
379
+ total: { type: 'number' },
380
+ pending: { type: 'number' },
381
+ completed: { type: 'number' }
382
+ }
383
+ },
384
+ projects: {
385
+ type: 'object',
386
+ properties: {
387
+ total: { type: 'number' }
388
+ }
389
+ },
390
+ timestamp: { type: 'string' }
391
+ }
392
+ },
393
+
394
+ handler: async (input, ctx) => {
395
+ ctx.logger.info('Fetching dashboard data');
396
+
397
+ try {
398
+ // Fetch all data in parallel for better performance
399
+ const [conversations, todos, projects] = await Promise.all([
400
+ ctx.majk.conversations.list(),
401
+ ctx.majk.todos.list(),
402
+ ctx.majk.projects.list()
403
+ ]);
404
+
405
+ // Process todos
406
+ const pendingTodos = todos.filter(t => t.status === 'pending');
407
+ const completedTodos = todos.filter(t => t.status === 'completed');
408
+
409
+ // Get recent conversations (last 5)
410
+ const recentConversations = conversations
411
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
412
+ .slice(0, 5)
413
+ .map(c => ({
414
+ id: c.id,
415
+ title: c.title || 'Untitled',
416
+ updatedAt: c.updatedAt
417
+ }));
418
+
419
+ const result = {
420
+ conversations: {
421
+ total: conversations.length,
422
+ recent: recentConversations
423
+ },
424
+ todos: {
425
+ total: todos.length,
426
+ pending: pendingTodos.length,
427
+ completed: completedTodos.length
428
+ },
429
+ projects: {
430
+ total: projects.length
431
+ },
432
+ timestamp: new Date().toISOString()
433
+ };
434
+
435
+ ctx.logger.info('Dashboard data fetched successfully', {
436
+ conversationCount: conversations.length,
437
+ todoCount: todos.length,
438
+ projectCount: projects.length
439
+ });
440
+
441
+ return result;
442
+
443
+ } catch (error) {
444
+ ctx.logger.error('Failed to fetch dashboard data', {
445
+ error: error.message,
446
+ stack: error.stack
447
+ });
448
+ throw new Error(`Dashboard data fetch failed: ${error.message}`);
449
+ }
450
+ },
451
+
452
+ tags: ['dashboard', 'aggregation']
453
+ })
454
+ ```
455
+
456
+ ## Context in Tests
457
+
458
+ Mock context for testing:
459
+
460
+ ```typescript
461
+ // tests/plugin/functions/unit/dashboard.test.js
462
+ import { test, invoke, mock } from '@majkapp/plugin-test';
463
+ import assert from 'assert';
464
+
465
+ test('getDashboardData aggregates MAJK data', async () => {
466
+ // Create mock context with storage and MAJK data
467
+ const context = mock()
468
+ .storage({
469
+ 'user-settings': { theme: 'dark' }
470
+ })
471
+ .withMajkData({
472
+ conversations: [
473
+ { id: 'conv1', title: 'Test 1', updatedAt: '2024-01-01T00:00:00Z', messages: [] },
474
+ { id: 'conv2', title: 'Test 2', updatedAt: '2024-01-02T00:00:00Z', messages: [] }
475
+ ],
476
+ todos: [
477
+ { id: 'todo1', status: 'pending' },
478
+ { id: 'todo2', status: 'completed' }
479
+ ],
480
+ projects: [
481
+ { id: 'proj1', name: 'Project 1' }
482
+ ]
483
+ })
484
+ .build();
485
+
486
+ const result = await invoke('getDashboardData', {}, { context });
487
+
488
+ assert.strictEqual(result.conversations.total, 2);
489
+ assert.strictEqual(result.todos.total, 2);
490
+ assert.strictEqual(result.todos.pending, 1);
491
+ assert.strictEqual(result.todos.completed, 1);
492
+ assert.strictEqual(result.projects.total, 1);
493
+ });
494
+ ```
495
+
496
+ ## Next Steps
497
+
498
+ Run `npx @majkapp/plugin-kit --lifecycle` - onReady and event subscriptions
499
+ Run `npx @majkapp/plugin-kit --services` - Group functions into services
500
+ Run `npx @majkapp/plugin-kit --testing` - Test functions with mock context