@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/bin/promptable-cli.js +91 -0
- 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 +605 -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
package/docs/CONTEXT.md
ADDED
|
@@ -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
|