@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
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
# Functions
|
|
2
|
+
|
|
3
|
+
Functions are the core of your plugin. They define callable operations with strongly-typed inputs and outputs.
|
|
4
|
+
|
|
5
|
+
## Basic Function
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// src/index.ts
|
|
9
|
+
import { definePlugin } from '@majkapp/plugin-kit';
|
|
10
|
+
|
|
11
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
12
|
+
.pluginRoot(__dirname)
|
|
13
|
+
|
|
14
|
+
.function('health', {
|
|
15
|
+
// REQUIRED: 2-3 sentence description for LLM context
|
|
16
|
+
description: 'Check plugin health status. Returns current status and timestamp.',
|
|
17
|
+
|
|
18
|
+
// REQUIRED: JSON Schema for input validation
|
|
19
|
+
input: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {},
|
|
22
|
+
additionalProperties: false // Reject unknown properties
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// REQUIRED: JSON Schema for output validation
|
|
26
|
+
output: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
status: { type: 'string', enum: ['ok', 'error'] },
|
|
30
|
+
timestamp: { type: 'string', format: 'date-time' }
|
|
31
|
+
},
|
|
32
|
+
required: ['status', 'timestamp']
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// REQUIRED: Handler function
|
|
36
|
+
handler: async (input, ctx) => {
|
|
37
|
+
// ctx.logger - scoped logging (automatically includes plugin name)
|
|
38
|
+
ctx.logger.info('Health check requested');
|
|
39
|
+
|
|
40
|
+
// Return MUST match output schema or validation fails
|
|
41
|
+
return {
|
|
42
|
+
status: 'ok',
|
|
43
|
+
timestamp: new Date().toISOString()
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// OPTIONAL: Tags for organization and discovery
|
|
48
|
+
tags: ['monitoring', 'health']
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Test it:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// tests/plugin/functions/unit/health.test.js
|
|
56
|
+
import { test, invoke, mock } from '@majkapp/plugin-test';
|
|
57
|
+
import assert from 'assert';
|
|
58
|
+
|
|
59
|
+
test('health check returns ok status', async () => {
|
|
60
|
+
const context = mock().build();
|
|
61
|
+
const result = await invoke('health', {}, { context });
|
|
62
|
+
|
|
63
|
+
assert.strictEqual(result.status, 'ok');
|
|
64
|
+
assert.ok(result.timestamp);
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Function with Input Parameters
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// src/index.ts
|
|
72
|
+
.function('getAnalytics', {
|
|
73
|
+
description: 'Get analytics for specified time period. Returns user activity metrics.',
|
|
74
|
+
|
|
75
|
+
input: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
period: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
enum: ['24h', '7d', '30d'],
|
|
81
|
+
description: 'Time period for analytics'
|
|
82
|
+
},
|
|
83
|
+
includeDetails: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
default: false,
|
|
86
|
+
description: 'Include detailed breakdown'
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
required: ['period'], // period is required, includeDetails is optional
|
|
90
|
+
additionalProperties: false
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
output: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
period: { type: 'string' },
|
|
97
|
+
metrics: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
activeUsers: { type: 'number' },
|
|
101
|
+
totalSessions: { type: 'number' },
|
|
102
|
+
avgDuration: { type: 'number' }
|
|
103
|
+
},
|
|
104
|
+
required: ['activeUsers', 'totalSessions', 'avgDuration']
|
|
105
|
+
},
|
|
106
|
+
details: {
|
|
107
|
+
type: 'array',
|
|
108
|
+
items: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
date: { type: 'string' },
|
|
112
|
+
count: { type: 'number' }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
required: ['period', 'metrics']
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
handler: async (input, ctx) => {
|
|
121
|
+
ctx.logger.info(`Analytics requested for ${input.period}`);
|
|
122
|
+
|
|
123
|
+
// Access plugin storage (key-value store scoped to your plugin)
|
|
124
|
+
const sessions = await ctx.storage.get('sessions') || [];
|
|
125
|
+
|
|
126
|
+
// Calculate metrics (best practice: move to separate file)
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const periodMs = input.period === '24h' ? 86400000 :
|
|
129
|
+
input.period === '7d' ? 604800000 : 2592000000;
|
|
130
|
+
|
|
131
|
+
const recentSessions = sessions.filter(s =>
|
|
132
|
+
now - s.timestamp < periodMs
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const uniqueUsers = new Set(recentSessions.map(s => s.userId)).size;
|
|
136
|
+
|
|
137
|
+
const result = {
|
|
138
|
+
period: input.period,
|
|
139
|
+
metrics: {
|
|
140
|
+
activeUsers: uniqueUsers,
|
|
141
|
+
totalSessions: recentSessions.length,
|
|
142
|
+
avgDuration: recentSessions.length > 0
|
|
143
|
+
? Math.round(recentSessions.reduce((sum, s) => sum + s.duration, 0) / recentSessions.length)
|
|
144
|
+
: 0
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Conditionally include details based on input
|
|
149
|
+
if (input.includeDetails) {
|
|
150
|
+
result.details = calculateDailyBreakdown(recentSessions);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
tags: ['analytics', 'reporting']
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Test with different inputs:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// tests/plugin/functions/unit/analytics.test.js
|
|
164
|
+
import { test, invoke, mock } from '@majkapp/plugin-test';
|
|
165
|
+
import assert from 'assert';
|
|
166
|
+
|
|
167
|
+
test('getAnalytics returns metrics for 7d period', async () => {
|
|
168
|
+
const context = mock()
|
|
169
|
+
.storage({
|
|
170
|
+
sessions: [
|
|
171
|
+
{ userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 }, // 1 day ago
|
|
172
|
+
{ userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 }, // 2 days ago
|
|
173
|
+
{ userId: 'user1', duration: 600, timestamp: Date.now() - 259200000 } // 3 days ago
|
|
174
|
+
]
|
|
175
|
+
})
|
|
176
|
+
.build();
|
|
177
|
+
|
|
178
|
+
const result = await invoke('getAnalytics', { period: '7d' }, { context });
|
|
179
|
+
|
|
180
|
+
assert.strictEqual(result.period, '7d');
|
|
181
|
+
assert.strictEqual(result.metrics.activeUsers, 2); // user1 and user2
|
|
182
|
+
assert.strictEqual(result.metrics.totalSessions, 3);
|
|
183
|
+
assert.strictEqual(result.metrics.avgDuration, 450); // (300+450+600)/3
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('getAnalytics includes details when requested', async () => {
|
|
187
|
+
const context = mock()
|
|
188
|
+
.storage({ sessions: [] })
|
|
189
|
+
.build();
|
|
190
|
+
|
|
191
|
+
const result = await invoke('getAnalytics', {
|
|
192
|
+
period: '24h',
|
|
193
|
+
includeDetails: true
|
|
194
|
+
}, { context });
|
|
195
|
+
|
|
196
|
+
assert.ok(result.details);
|
|
197
|
+
assert.ok(Array.isArray(result.details));
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Best Practice: Decouple Business Logic
|
|
202
|
+
|
|
203
|
+
**ALWAYS separate core logic from plugin code.** Makes testing easier and logic reusable.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// src/core/analytics.ts - Pure business logic, NO plugin dependencies
|
|
207
|
+
export interface Session {
|
|
208
|
+
userId: string;
|
|
209
|
+
duration: number;
|
|
210
|
+
timestamp: number;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface AnalyticsResult {
|
|
214
|
+
period: string;
|
|
215
|
+
metrics: {
|
|
216
|
+
activeUsers: number;
|
|
217
|
+
totalSessions: number;
|
|
218
|
+
avgDuration: number;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Pure function - testable with plain Jest/Vitest
|
|
223
|
+
export function calculateAnalytics(
|
|
224
|
+
sessions: Session[],
|
|
225
|
+
period: '24h' | '7d' | '30d'
|
|
226
|
+
): AnalyticsResult {
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
const periodMs = {
|
|
229
|
+
'24h': 86400000,
|
|
230
|
+
'7d': 604800000,
|
|
231
|
+
'30d': 2592000000
|
|
232
|
+
}[period];
|
|
233
|
+
|
|
234
|
+
const recentSessions = sessions.filter(s =>
|
|
235
|
+
now - s.timestamp < periodMs
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const uniqueUsers = new Set(recentSessions.map(s => s.userId)).size;
|
|
239
|
+
const totalDuration = recentSessions.reduce((sum, s) => sum + s.duration, 0);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
period,
|
|
243
|
+
metrics: {
|
|
244
|
+
activeUsers: uniqueUsers,
|
|
245
|
+
totalSessions: recentSessions.length,
|
|
246
|
+
avgDuration: recentSessions.length > 0
|
|
247
|
+
? Math.round(totalDuration / recentSessions.length)
|
|
248
|
+
: 0
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// tests/core/analytics.test.js - Test with plain Jest (faster, no plugin overhead)
|
|
256
|
+
import { calculateAnalytics } from '../src/core/analytics';
|
|
257
|
+
import { describe, it, expect } from 'vitest';
|
|
258
|
+
|
|
259
|
+
describe('calculateAnalytics', () => {
|
|
260
|
+
it('calculates metrics for 7d period', () => {
|
|
261
|
+
const sessions = [
|
|
262
|
+
{ userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 },
|
|
263
|
+
{ userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 },
|
|
264
|
+
{ userId: 'user1', duration: 600, timestamp: Date.now() - 259200000 }
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const result = calculateAnalytics(sessions, '7d');
|
|
268
|
+
|
|
269
|
+
expect(result.metrics.activeUsers).toBe(2);
|
|
270
|
+
expect(result.metrics.totalSessions).toBe(3);
|
|
271
|
+
expect(result.metrics.avgDuration).toBe(450);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('filters out old sessions', () => {
|
|
275
|
+
const sessions = [
|
|
276
|
+
{ userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 }, // 1 day ago - included
|
|
277
|
+
{ userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 }, // 2 days ago - excluded
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const result = calculateAnalytics(sessions, '24h');
|
|
281
|
+
|
|
282
|
+
expect(result.metrics.activeUsers).toBe(1);
|
|
283
|
+
expect(result.metrics.totalSessions).toBe(1);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// src/index.ts - Plugin function is thin wrapper
|
|
290
|
+
import { calculateAnalytics } from './core/analytics';
|
|
291
|
+
|
|
292
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
293
|
+
.pluginRoot(__dirname)
|
|
294
|
+
|
|
295
|
+
.function('getAnalytics', {
|
|
296
|
+
description: 'Get analytics for specified time period',
|
|
297
|
+
input: { /* schema */ },
|
|
298
|
+
output: { /* schema */ },
|
|
299
|
+
handler: async (input, ctx) => {
|
|
300
|
+
// Plugin layer: Get data from storage
|
|
301
|
+
const sessions = await ctx.storage.get('sessions') || [];
|
|
302
|
+
|
|
303
|
+
// Business logic: Pure computation (easy to test)
|
|
304
|
+
const result = calculateAnalytics(sessions, input.period);
|
|
305
|
+
|
|
306
|
+
// Plugin layer: Log and return
|
|
307
|
+
ctx.logger.info(`Analytics calculated for ${input.period}`, result);
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Accessing MAJK APIs
|
|
314
|
+
|
|
315
|
+
Use `ctx.majk` to access MAJK's conversation, todo, project, and teammate APIs.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
.function('getConversationSummary', {
|
|
319
|
+
description: 'Get summary of a specific conversation with recent messages',
|
|
320
|
+
|
|
321
|
+
input: {
|
|
322
|
+
type: 'object',
|
|
323
|
+
properties: {
|
|
324
|
+
conversationId: { type: 'string' }
|
|
325
|
+
},
|
|
326
|
+
required: ['conversationId'],
|
|
327
|
+
additionalProperties: false
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
output: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: {
|
|
333
|
+
id: { type: 'string' },
|
|
334
|
+
title: { type: 'string' },
|
|
335
|
+
messageCount: { type: 'number' },
|
|
336
|
+
lastActivity: { type: 'string' }
|
|
337
|
+
},
|
|
338
|
+
required: ['id', 'title', 'messageCount']
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
handler: async (input, ctx) => {
|
|
342
|
+
// Access MAJK conversation API
|
|
343
|
+
const conversation = await ctx.majk.conversations.get(input.conversationId);
|
|
344
|
+
|
|
345
|
+
if (!conversation) {
|
|
346
|
+
ctx.logger.warn(`Conversation not found: ${input.conversationId}`);
|
|
347
|
+
throw new Error(`Conversation ${input.conversationId} not found`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
ctx.logger.info(`Retrieved conversation: ${conversation.title}`);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
id: conversation.id,
|
|
354
|
+
title: conversation.title || 'Untitled',
|
|
355
|
+
messageCount: conversation.messages?.length || 0,
|
|
356
|
+
lastActivity: conversation.updatedAt || conversation.createdAt
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
tags: ['conversations']
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Available `ctx.majk` APIs:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// Conversations
|
|
368
|
+
await ctx.majk.conversations.list();
|
|
369
|
+
await ctx.majk.conversations.get(id);
|
|
370
|
+
|
|
371
|
+
// Todos
|
|
372
|
+
await ctx.majk.todos.list();
|
|
373
|
+
await ctx.majk.todos.get(id);
|
|
374
|
+
|
|
375
|
+
// Projects
|
|
376
|
+
await ctx.majk.projects.list();
|
|
377
|
+
await ctx.majk.projects.get(id);
|
|
378
|
+
|
|
379
|
+
// Teammates
|
|
380
|
+
await ctx.majk.teammates.list();
|
|
381
|
+
await ctx.majk.teammates.get(id);
|
|
382
|
+
|
|
383
|
+
// MCP Servers
|
|
384
|
+
await ctx.majk.mcpServers.list();
|
|
385
|
+
await ctx.majk.mcpServers.get(id);
|
|
386
|
+
|
|
387
|
+
// Event Bus
|
|
388
|
+
const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
|
|
389
|
+
console.log('Event:', event);
|
|
390
|
+
});
|
|
391
|
+
// Later: subscription.unsubscribe();
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Mutation Functions
|
|
395
|
+
|
|
396
|
+
Functions that modify state (create, update, delete) are **mutations**.
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
.function('clearEvents', {
|
|
400
|
+
description: 'Clear all logged events from storage',
|
|
401
|
+
|
|
402
|
+
input: {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {},
|
|
405
|
+
additionalProperties: false
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
output: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
success: { type: 'boolean' },
|
|
412
|
+
clearedCount: { type: 'number' }
|
|
413
|
+
},
|
|
414
|
+
required: ['success', 'clearedCount']
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
handler: async (input, ctx) => {
|
|
418
|
+
const events = await ctx.storage.get('events') || [];
|
|
419
|
+
const count = events.length;
|
|
420
|
+
|
|
421
|
+
// Clear storage
|
|
422
|
+
await ctx.storage.set('events', []);
|
|
423
|
+
|
|
424
|
+
ctx.logger.info(`Cleared ${count} events`);
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
success: true,
|
|
428
|
+
clearedCount: count
|
|
429
|
+
};
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
tags: ['mutations', 'events']
|
|
433
|
+
})
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Generated React hook will be a **mutation hook** (call `.mutate()` manually):
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// ui/src/EventsPage.tsx
|
|
440
|
+
import { useClearEvents } from './generated/hooks';
|
|
441
|
+
|
|
442
|
+
export function EventsPage() {
|
|
443
|
+
const { mutate: clearEvents, loading } = useClearEvents();
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<button
|
|
447
|
+
onClick={() => clearEvents({})}
|
|
448
|
+
disabled={loading}
|
|
449
|
+
data-majk-id="clearEventsButton"
|
|
450
|
+
data-majk-state={loading ? 'loading' : 'idle'}
|
|
451
|
+
>
|
|
452
|
+
{loading ? 'Clearing...' : 'Clear Events'}
|
|
453
|
+
</button>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## UI Logging Function (Best Practice)
|
|
459
|
+
|
|
460
|
+
**Always include a `uiLog` function** for debugging frontend issues.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
.function('uiLog', {
|
|
464
|
+
description: 'Log messages from UI for debugging. Allows frontend to send logs to backend.',
|
|
465
|
+
|
|
466
|
+
input: {
|
|
467
|
+
type: 'object',
|
|
468
|
+
properties: {
|
|
469
|
+
level: {
|
|
470
|
+
type: 'string',
|
|
471
|
+
enum: ['debug', 'info', 'warn', 'error'],
|
|
472
|
+
description: 'Log level'
|
|
473
|
+
},
|
|
474
|
+
component: {
|
|
475
|
+
type: 'string',
|
|
476
|
+
description: 'React component name'
|
|
477
|
+
},
|
|
478
|
+
message: {
|
|
479
|
+
type: 'string',
|
|
480
|
+
description: 'Log message'
|
|
481
|
+
},
|
|
482
|
+
data: {
|
|
483
|
+
type: 'object',
|
|
484
|
+
description: 'Additional context data'
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
required: ['level', 'component', 'message'],
|
|
488
|
+
additionalProperties: false
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
output: {
|
|
492
|
+
type: 'object',
|
|
493
|
+
properties: {
|
|
494
|
+
success: { type: 'boolean' }
|
|
495
|
+
},
|
|
496
|
+
required: ['success']
|
|
497
|
+
},
|
|
498
|
+
|
|
499
|
+
handler: async (input, ctx) => {
|
|
500
|
+
const prefix = `[UI:${input.component}]`;
|
|
501
|
+
const message = `${prefix} ${input.message}`;
|
|
502
|
+
|
|
503
|
+
// Route to appropriate log level
|
|
504
|
+
switch (input.level) {
|
|
505
|
+
case 'debug':
|
|
506
|
+
ctx.logger.debug(message, input.data);
|
|
507
|
+
break;
|
|
508
|
+
case 'info':
|
|
509
|
+
ctx.logger.info(message, input.data);
|
|
510
|
+
break;
|
|
511
|
+
case 'warn':
|
|
512
|
+
ctx.logger.warn(message, input.data);
|
|
513
|
+
break;
|
|
514
|
+
case 'error':
|
|
515
|
+
ctx.logger.error(message, input.data);
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { success: true };
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
tags: ['debugging', 'ui']
|
|
523
|
+
})
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Use in React:
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { useUiLog } from './generated/hooks';
|
|
530
|
+
import { useEffect } from 'react';
|
|
531
|
+
|
|
532
|
+
export function DashboardPage() {
|
|
533
|
+
const { mutate: uiLog } = useUiLog();
|
|
534
|
+
|
|
535
|
+
// Log component lifecycle
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
uiLog({
|
|
538
|
+
level: 'info',
|
|
539
|
+
component: 'DashboardPage',
|
|
540
|
+
message: 'Component mounted'
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return () => {
|
|
544
|
+
uiLog({
|
|
545
|
+
level: 'info',
|
|
546
|
+
component: 'DashboardPage',
|
|
547
|
+
message: 'Component unmounted'
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
}, [uiLog]);
|
|
551
|
+
|
|
552
|
+
// Log errors
|
|
553
|
+
const handleClick = async () => {
|
|
554
|
+
try {
|
|
555
|
+
await someOperation();
|
|
556
|
+
} catch (error) {
|
|
557
|
+
uiLog({
|
|
558
|
+
level: 'error',
|
|
559
|
+
component: 'DashboardPage',
|
|
560
|
+
message: 'Operation failed',
|
|
561
|
+
data: { error: error.message, stack: error.stack }
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
return <div>...</div>;
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## Error Handling
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
.function('riskyOperation', {
|
|
574
|
+
description: 'Performs an operation that might fail',
|
|
575
|
+
|
|
576
|
+
input: { /* schema */ },
|
|
577
|
+
output: { /* schema */ },
|
|
578
|
+
|
|
579
|
+
handler: async (input, ctx) => {
|
|
580
|
+
try {
|
|
581
|
+
// Attempt operation
|
|
582
|
+
const result = await performOperation(input);
|
|
583
|
+
|
|
584
|
+
ctx.logger.info('Operation succeeded', { result });
|
|
585
|
+
return result;
|
|
586
|
+
|
|
587
|
+
} catch (error) {
|
|
588
|
+
// Log error with context
|
|
589
|
+
ctx.logger.error('Operation failed', {
|
|
590
|
+
error: error.message,
|
|
591
|
+
stack: error.stack,
|
|
592
|
+
input
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Re-throw or return error response
|
|
596
|
+
throw new Error(`Operation failed: ${error.message}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
})
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Function Tags
|
|
603
|
+
|
|
604
|
+
Tags help organize and discover functions.
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
.function('health', { /* ... */, tags: ['monitoring', 'health'] })
|
|
608
|
+
.function('getAnalytics', { /* ... */, tags: ['analytics', 'reporting'] })
|
|
609
|
+
.function('clearEvents', { /* ... */, tags: ['mutations', 'events'] })
|
|
610
|
+
.function('uiLog', { /* ... */, tags: ['debugging', 'ui'] })
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Tags are used by:
|
|
614
|
+
- Service grouping (see SERVICES.md)
|
|
615
|
+
- Function discovery
|
|
616
|
+
- Documentation organization
|
|
617
|
+
|
|
618
|
+
## Next Steps
|
|
619
|
+
|
|
620
|
+
Run `npx @majkapp/plugin-kit --hooks` - Generated React hooks reference
|
|
621
|
+
Run `npx @majkapp/plugin-kit --context` - Complete ctx API reference
|
|
622
|
+
Run `npx @majkapp/plugin-kit --services` - Group functions into services
|
|
623
|
+
Run `npx @majkapp/plugin-kit --testing` - Test your functions
|