@majkapp/plugin-kit 1.1.0 โ†’ 1.2.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/README.md CHANGED
@@ -1,135 +1,480 @@
1
- # @majk/plugin-kit
1
+ # @majkapp/plugin-kit
2
2
 
3
- **Fluent builder framework for creating robust MAJK plugins**
3
+ **Modern, type-safe framework for building MAJK plugins with exceptional developer experience**
4
4
 
5
- Build type-safe, production-ready MAJK plugins with excellent developer experience, comprehensive validation, and clear error messages.
5
+ Build production-ready MAJK plugins using a fluent builder API with comprehensive validation, auto-generated clients, and declarative configuration management.
6
6
 
7
- ## Features
7
+ [![npm version](https://img.shields.io/npm/v/@majkapp/plugin-kit.svg)](https://www.npmjs.com/package/@majkapp/plugin-kit)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
9
 
9
- โœจ **Fluent API** - Chainable builder pattern with full TypeScript support
10
- ๐Ÿ›ก๏ธ **Type Safety** - Compile-time checks for routes, IDs, and descriptions
11
- โœ… **Build-Time Validation** - Catches errors before runtime
12
- ๐Ÿ“ **Clear Error Messages** - Actionable suggestions when things go wrong
13
- ๐Ÿ”„ **Auto HTTP Server** - Built-in routing, CORS, error handling
14
- โš›๏ธ **React & HTML Screens** - Support for both SPA and simple HTML UIs
15
- ๐Ÿ”ง **Tool Management** - Declare tools with schema validation
16
- ๐Ÿ’พ **Storage Integration** - Direct access to plugin storage
17
- ๐Ÿ“ก **Event Bus** - Subscribe to system events
18
- ๐Ÿงน **Auto Cleanup** - Managed lifecycle with automatic resource cleanup
19
- โค๏ธ **Health Checks** - Built-in health monitoring
10
+ ## โœจ Features
20
11
 
21
- ## Installation
12
+ - ๐ŸŽฏ **Function-First Architecture** - Define backend functions with JSON schemas, auto-generate TypeScript clients
13
+ - ๐Ÿ”ง **Configurable Entities** - Declaratively register teammates, MCP servers based on user configuration
14
+ - ๐ŸŽจ **React & HTML Support** - Build rich UIs with React or simple HTML pages
15
+ - ๐Ÿ›ก๏ธ **Type Safety** - Full TypeScript support with compile-time validation
16
+ - ๐Ÿ“ **Auto-Generated Clients** - Generate React hooks and TypeScript clients from function definitions
17
+ - โœ… **Build-Time Validation** - Catch errors before runtime with comprehensive checks
18
+ - ๐Ÿ”„ **HTTP Transport** - Built-in HTTP server with routing, CORS, error handling
19
+ - ๐Ÿ’พ **Integrated Storage** - Plugin-scoped key-value storage
20
+ - ๐Ÿ“ก **Event Bus** - Subscribe to system events with cleanup management
21
+ - โค๏ธ **Health Monitoring** - Built-in health checks with custom logic
22
+
23
+ ---
24
+
25
+ ## ๐Ÿ“ฆ Installation
22
26
 
23
27
  ```bash
24
- npm install @majk/plugin-kit
28
+ npm install @majkapp/plugin-kit
25
29
  ```
26
30
 
27
- ## Quick Start
31
+ ---
32
+
33
+ ## ๐Ÿš€ Quick Start
28
34
 
29
35
  ```typescript
30
- import { definePlugin } from '@majk/plugin-kit';
36
+ import { definePlugin, HttpTransport } from '@majkapp/plugin-kit';
31
37
 
32
- export default definePlugin('my-plugin', 'My Plugin', '1.0.0')
33
- .ui({ appDir: 'ui/dist' })
38
+ export = definePlugin('my-plugin', 'My Plugin', '1.0.0')
39
+ .pluginRoot(__dirname)
34
40
 
35
- .topbar('/plugin-screens/my-plugin/dashboard', {
36
- icon: '๐Ÿš€'
41
+ // Define a backend function
42
+ .function('getMessage', {
43
+ description: 'Gets a greeting message for the user.',
44
+ input: {
45
+ type: 'object',
46
+ properties: {
47
+ name: { type: 'string' }
48
+ },
49
+ required: ['name']
50
+ },
51
+ output: {
52
+ type: 'object',
53
+ properties: {
54
+ message: { type: 'string' }
55
+ }
56
+ },
57
+ handler: async (input, ctx) => {
58
+ return { message: `Hello, ${input.name}!` };
59
+ }
37
60
  })
38
61
 
39
- .screenReact({
40
- id: 'dashboard',
41
- name: 'Dashboard',
42
- description: 'Main dashboard for my plugin. Shows key metrics and actions.',
43
- route: '/plugin-screens/my-plugin/dashboard',
44
- reactPath: '/'
62
+ // Configure React UI
63
+ .ui({
64
+ appDir: 'dist',
65
+ base: '/',
66
+ history: 'hash'
45
67
  })
46
68
 
47
- .apiRoute({
48
- method: 'GET',
49
- path: '/api/data',
50
- name: 'Get Data',
51
- description: 'Retrieves plugin data. Returns formatted response with metadata.',
52
- handler: async (req, res, { majk, storage }) => {
53
- const data = await storage.get('data') || [];
54
- return { data, count: data.length };
55
- }
69
+ // Add a screen
70
+ .screenReact({
71
+ id: 'main',
72
+ name: 'My Plugin',
73
+ description: 'Main plugin screen. Shows greeting and controls.',
74
+ route: '/plugin-screens/my-plugin/main',
75
+ reactPath: '/'
56
76
  })
57
77
 
58
- .tool('global', {
59
- name: 'myTool',
60
- description: 'Does something useful. Processes input and returns results.',
61
- inputSchema: {
62
- type: 'object',
63
- properties: {
64
- param: { type: 'string' }
65
- },
66
- required: ['param']
67
- }
68
- }, async (input, { logger }) => {
69
- logger.info(`Tool called with: ${input.param}`);
70
- return { success: true, result: input.param.toUpperCase() };
71
- })
78
+ // Enable HTTP transport
79
+ .transport(new HttpTransport())
72
80
 
73
81
  .build();
74
82
  ```
75
83
 
76
- ## Core Concepts
84
+ Generate client:
85
+ ```bash
86
+ npx plugin-kit generate -e ./index.js -o ./ui/src/generated
87
+ ```
88
+
89
+ Use in React:
90
+ ```tsx
91
+ import { useGetMessage } from './generated';
92
+
93
+ function App() {
94
+ const { data, loading, error, mutate } = useGetMessage();
95
+
96
+ return (
97
+ <button onClick={() => mutate({ name: 'World' })}>
98
+ {loading ? 'Loading...' : data?.message}
99
+ </button>
100
+ );
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## ๐Ÿ“š Table of Contents
107
+
108
+ - [Core Concepts](#-core-concepts)
109
+ - [Function-First Architecture](#-function-first-architecture)
110
+ - [Configurable Entities](#-configurable-entities)
111
+ - [UI Configuration](#-ui-configuration)
112
+ - [API Routes (Legacy)](#-api-routes-legacy)
113
+ - [Tools](#-tools)
114
+ - [Static Entities](#-static-entities)
115
+ - [Secret Providers](#-secret-providers)
116
+ - [Lifecycle Hooks](#-lifecycle-hooks)
117
+ - [Client Generation](#-client-generation)
118
+ - [Complete Example](#-complete-example)
119
+ - [API Reference](#-api-reference)
120
+ - [MAJK API Deep Dive](#-majk-api-deep-dive)
121
+ - [Best Practices](#-best-practices)
122
+ - [Troubleshooting](#-troubleshooting)
123
+
124
+ ---
125
+
126
+ ## ๐ŸŽฏ Core Concepts
77
127
 
78
128
  ### Plugin Definition
79
129
 
80
130
  Every plugin starts with `definePlugin(id, name, version)`:
81
131
 
82
132
  ```typescript
83
- definePlugin('system-explorer', 'System Explorer', '1.0.0')
133
+ import { definePlugin } from '@majkapp/plugin-kit';
134
+
135
+ const plugin = definePlugin('task-manager', 'Task Manager', '1.0.0')
136
+ .pluginRoot(__dirname) // Important: tells plugin-kit where your files are
137
+ // ... builder methods
138
+ .build();
139
+
140
+ export = plugin;
84
141
  ```
85
142
 
86
143
  **Rules:**
87
144
  - `id` must be unique and URL-safe (kebab-case recommended)
88
- - `name` is the display name
89
- - `version` follows semver
145
+ - `name` is the human-readable display name
146
+ - `version` follows semantic versioning (semver)
147
+ - Always call `.pluginRoot(__dirname)` for reliable file resolution
148
+
149
+ ---
150
+
151
+ ## ๐Ÿ”ฅ Function-First Architecture
152
+
153
+ The modern way to build MAJK plugins. Define functions with JSON schemas, and plugin-kit automatically:
154
+ - Generates HTTP endpoints
155
+ - Creates TypeScript clients with React hooks
156
+ - Validates input/output at runtime
157
+ - Handles errors gracefully
158
+
159
+ ### Defining Functions
160
+
161
+ ```typescript
162
+ .function('createTask', {
163
+ description: 'Creates a new task with the specified details.',
164
+ input: {
165
+ type: 'object',
166
+ properties: {
167
+ title: { type: 'string' },
168
+ description: { type: 'string' },
169
+ priority: { type: 'string', enum: ['low', 'medium', 'high'] },
170
+ dueDate: { type: 'string', format: 'date-time' }
171
+ },
172
+ required: ['title']
173
+ },
174
+ output: {
175
+ type: 'object',
176
+ properties: {
177
+ id: { type: 'string' },
178
+ title: { type: 'string' },
179
+ createdAt: { type: 'string' }
180
+ }
181
+ },
182
+ handler: async (input, ctx) => {
183
+ const task = {
184
+ id: generateId(),
185
+ title: input.title,
186
+ description: input.description || '',
187
+ priority: input.priority || 'medium',
188
+ dueDate: input.dueDate,
189
+ createdAt: new Date().toISOString()
190
+ };
191
+
192
+ await ctx.storage.set(`task:${task.id}`, task);
193
+ ctx.logger.info(`Created task: ${task.id}`);
194
+
195
+ return task;
196
+ },
197
+ tags: ['tasks']
198
+ })
199
+ ```
200
+
201
+ **Handler Context (`ctx`):**
202
+ - `storage` - Plugin-scoped key-value storage
203
+ - `logger` - Scoped logger (debug, info, warn, error)
204
+ - `majk` - Full MAJK API interface
205
+
206
+ ### Subscriptions (Async Iterators)
207
+
208
+ For streaming or long-running operations:
209
+
210
+ ```typescript
211
+ .subscription('watchTasks', {
212
+ description: 'Watches for task changes in real-time.',
213
+ input: {
214
+ type: 'object',
215
+ properties: {
216
+ filter: { type: 'string' }
217
+ }
218
+ },
219
+ output: {
220
+ type: 'object',
221
+ properties: {
222
+ taskId: { type: 'string' },
223
+ event: { type: 'string' },
224
+ timestamp: { type: 'string' }
225
+ }
226
+ },
227
+ handler: async function* (input, ctx) {
228
+ const subscription = ctx.majk.eventBus.channel('tasks').subscribe();
229
+
230
+ try {
231
+ for await (const event of subscription) {
232
+ yield {
233
+ taskId: event.entity.id,
234
+ event: event.type,
235
+ timestamp: new Date().toISOString()
236
+ };
237
+ }
238
+ } finally {
239
+ subscription.unsubscribe();
240
+ }
241
+ }
242
+ })
243
+ ```
244
+
245
+ ---
246
+
247
+ ## ๐Ÿ”ง Configurable Entities
248
+
249
+ **New in v1.1.0** - Register entities (teammates, MCP servers) that are only created **after** the user completes a configuration wizard.
250
+
251
+ ### Configuration Schema
252
+
253
+ Define a config wizard with a JSON schema:
254
+
255
+ ```typescript
256
+ const ConfigSchema = {
257
+ type: 'object',
258
+ properties: {
259
+ name: { type: 'string' },
260
+ role: { type: 'string' },
261
+ systemPrompt: { type: 'string' },
262
+ model: { type: 'string', enum: ['gpt-4', 'claude-3-sonnet'] }
263
+ },
264
+ required: ['name', 'systemPrompt']
265
+ };
266
+
267
+ .configWizard({
268
+ // Schema enables auto-generation of updateConfig() and getConfig()
269
+ schema: ConfigSchema,
270
+ storageKey: 'teammate-config', // Optional, defaults to '_plugin_config'
271
+
272
+ // UI
273
+ path: '/plugin-screens/my-plugin/main',
274
+ hash: '#/config-wizard',
275
+ title: 'Configure Your Assistant',
276
+ width: 900,
277
+ height: 700,
278
+ description: 'Set up your AI assistant with custom attributes.',
279
+
280
+ // When to show the wizard
281
+ shouldShow: async (ctx) => {
282
+ const config = await ctx.storage.get('teammate-config');
283
+ return !config; // Show if no config exists
284
+ }
285
+ })
286
+ ```
287
+
288
+ **What this does:**
289
+ - โœ… Auto-generates `updateConfig(config)` function (validates against schema, saves to storage)
290
+ - โœ… Auto-generates `getConfig()` function (retrieves current config)
291
+ - โœ… Both functions available in generated client with React hooks
292
+ - โœ… Plugin-kit loads config and calls factories at capability generation time
293
+
294
+ ### Configurable Teammates
295
+
296
+ Register teammates that are created based on user configuration:
297
+
298
+ ```typescript
299
+ .configurableTeamMember((config) => {
300
+ // config is loaded from storage by plugin-kit
301
+ return [{
302
+ id: 'my-assistant',
303
+ name: config.name,
304
+ role: 'assistant',
305
+ systemPrompt: config.systemPrompt,
306
+ model: config.model,
307
+ expertise: config.skills || [],
308
+ isActive: true
309
+ }];
310
+ })
311
+ ```
312
+
313
+ **How it works:**
314
+ 1. **First load (no config)**: Factory not called, no teammate registered, config wizard shown
315
+ 2. **User completes wizard**: UI calls auto-generated `updateConfig()`, sends `postMessage({ type: 'majk:config-complete' })`
316
+ 3. **Plugin reloads**: Config exists, factory called with config, teammate registered!
317
+
318
+ ### Configurable MCP Servers
319
+
320
+ ```typescript
321
+ .configurableMcp((config) => {
322
+ if (!config.enableMcpServer) return [];
323
+
324
+ return [{
325
+ id: 'custom-mcp',
326
+ name: config.mcpServerName,
327
+ type: 'stdio',
328
+ command: config.command,
329
+ args: config.args || [],
330
+ env: config.env || {}
331
+ }];
332
+ })
333
+ ```
334
+
335
+ ### Generic Configurable Entities
336
+
337
+ ```typescript
338
+ .configurableEntity('project', (config) => {
339
+ return config.projects.map(p => ({
340
+ id: p.id,
341
+ name: p.name,
342
+ description: p.description
343
+ }));
344
+ })
345
+ ```
346
+
347
+ ### Settings Screen
348
+
349
+ Provide a way to reconfigure after initial setup using a **separate** `.settings()` call:
350
+
351
+ ```typescript
352
+ .settings({
353
+ path: '/plugin-screens/my-plugin/main',
354
+ hash: '#/settings',
355
+ title: 'Plugin Settings',
356
+ description: 'Reconfigure your assistant and preferences.'
357
+ })
358
+ ```
359
+
360
+ The settings screen can use the same auto-generated `updateConfig()` and `getConfig()` functions that were created by `.configWizard({ schema })`.
361
+
362
+ **Note:** While `ConfigWizardDef` type includes an optional `settings` property, the current implementation requires using the separate `.settings()` method shown above.
90
363
 
91
- ### Screens
364
+ ### Build-Time Validation
365
+
366
+ Plugin-kit validates your usage:
367
+
368
+ ```typescript
369
+ // โŒ ERROR: Configurable entities require configWizard with schema
370
+ .configurableTeamMember((config) => [...])
371
+ // Missing .configWizard({ schema: ... })
372
+
373
+ // โœ… CORRECT
374
+ .configWizard({ schema: MySchema, ... })
375
+ .configurableTeamMember((config) => [...])
376
+ ```
377
+
378
+ ---
92
379
 
93
- #### React Screens
380
+ ## ๐ŸŽจ UI Configuration
94
381
 
95
- For React SPAs, configure UI first:
382
+ ### React Applications
383
+
384
+ For React SPAs, configure UI first, then add screens:
96
385
 
97
386
  ```typescript
98
387
  .ui({
99
- appDir: 'ui/dist', // Where your built React app is
100
- base: '/', // Base URL for the SPA
101
- history: 'browser' // 'browser' or 'hash' routing
388
+ appDir: 'dist', // Where your built React app is
389
+ base: '/', // Base URL for the SPA
390
+ history: 'hash' // 'browser' or 'hash' routing
102
391
  })
103
392
 
104
393
  .screenReact({
105
394
  id: 'dashboard',
106
395
  name: 'Dashboard',
107
- description: 'Main dashboard view. Shows metrics and controls.', // 2-3 sentences
108
- route: '/plugin-screens/my-plugin/dashboard', // Must start with /plugin-screens/{id}/
109
- reactPath: '/' // Path within your React app
396
+ description: 'Main dashboard view. Shows metrics and activity.',
397
+ route: '/plugin-screens/my-plugin/dashboard', // Must start with /plugin-screens/{pluginId}/
398
+ reactPath: '/' // Path within your React app
399
+ })
400
+
401
+ .screenReact({
402
+ id: 'settings',
403
+ name: 'Settings',
404
+ description: 'Plugin settings screen. Configure behavior and appearance.',
405
+ route: '/plugin-screens/my-plugin/settings',
406
+ reactPath: '/settings',
407
+ hash: '#/settings' // Optional: hash fragment for hash routing
110
408
  })
111
409
  ```
112
410
 
113
- **The React app receives:**
114
- - `window.__MAJK_BASE_URL__` - Host base URL
115
- - `window.__MAJK_IFRAME_BASE__` - Plugin base path
116
- - `window.__MAJK_PLUGIN_ID__` - Your plugin ID
411
+ **The React app receives globals:**
412
+ ```javascript
413
+ window.__MAJK_BASE_URL__ // Host base URL
414
+ window.__MAJK_IFRAME_BASE__ // Plugin base path
415
+ window.__MAJK_PLUGIN_ID__ // Your plugin ID
416
+ ```
117
417
 
118
- #### HTML Screens
418
+ ### HTML Screens
119
419
 
120
- For simple HTML pages:
420
+ For simple static pages:
121
421
 
122
422
  ```typescript
123
423
  .screenHtml({
124
424
  id: 'about',
125
425
  name: 'About',
126
- description: 'Information about the plugin. Shows version and author.',
426
+ description: 'Information about the plugin. Shows version and features.',
127
427
  route: '/plugin-screens/my-plugin/about',
128
- html: '<html>...' // OR htmlFile: 'about.html'
428
+ htmlFile: 'about.html' // Relative to pluginRoot
429
+ // OR
430
+ html: '<html><body>...</body></html>'
431
+ })
432
+ ```
433
+
434
+ ### Top Bar Navigation
435
+
436
+ Add plugin to MAJK's top navigation:
437
+
438
+ ```typescript
439
+ .topbar('/plugin-screens/my-plugin/dashboard', {
440
+ icon: '๐Ÿ“Š',
441
+ name: 'Task Manager' // Optional, defaults to plugin name
129
442
  })
130
443
  ```
131
444
 
132
- ### API Routes
445
+ ### Top Bar Menu
446
+
447
+ Add items to MAJK's menu:
448
+
449
+ ```typescript
450
+ .topBarMenu([
451
+ {
452
+ path: 'My Plugin.Dashboard',
453
+ label: 'Overview',
454
+ icon: '๐Ÿ“Š',
455
+ route: '/plugin-screens/my-plugin/dashboard',
456
+ description: 'View dashboard with key metrics.',
457
+ badge: { label: 'New', variant: 'success' }
458
+ },
459
+ {
460
+ type: 'divider',
461
+ path: 'My Plugin.Divider1'
462
+ },
463
+ {
464
+ path: 'My Plugin.Settings',
465
+ label: 'Settings',
466
+ icon: 'โš™๏ธ',
467
+ route: '/plugin-screens/my-plugin/settings',
468
+ description: 'Configure plugin behavior.'
469
+ }
470
+ ])
471
+ ```
472
+
473
+ ---
474
+
475
+ ## ๐Ÿ”Œ API Routes (Legacy)
476
+
477
+ **Note:** For new plugins, prefer [Function-First Architecture](#-function-first-architecture). API routes are still supported for backward compatibility.
133
478
 
134
479
  Define REST endpoints:
135
480
 
@@ -138,60 +483,74 @@ Define REST endpoints:
138
483
  method: 'POST',
139
484
  path: '/api/tasks/:id/complete',
140
485
  name: 'Complete Task',
141
- description: 'Marks a task as complete. Updates task status and triggers notifications.',
486
+ description: 'Marks a task as complete. Updates status and notifies users.',
142
487
  handler: async (req, res, { majk, storage, logger }) => {
143
- const { id } = req.params; // Path parameters
144
- const { note } = req.body; // Request body
145
- const status = req.query.get('status'); // Query params
488
+ const { id } = req.params; // Path parameters
489
+ const { note } = req.body; // Request body (JSON parsed)
490
+ const status = req.query.get('status'); // Query string
146
491
 
147
492
  logger.info(`Completing task ${id}`);
148
493
 
149
- // Access MAJK APIs
150
- const todos = await majk.todos.list();
494
+ const task = await storage.get(`task:${id}`);
495
+ if (!task) {
496
+ res.status(404).json({ error: 'Task not found' });
497
+ return;
498
+ }
499
+
500
+ task.completed = true;
501
+ task.completedAt = new Date().toISOString();
502
+ task.note = note;
151
503
 
152
- // Use plugin storage
153
- await storage.set(`task:${id}`, { completed: true });
504
+ await storage.set(`task:${id}`, task);
154
505
 
155
- return { success: true, taskId: id };
506
+ return { success: true, task };
156
507
  }
157
508
  })
158
509
  ```
159
510
 
160
511
  **Available Methods:** `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
161
512
 
162
- **Context Provided:**
163
- - `majk` - Full MAJK API interface
164
- - `storage` - Plugin-scoped key-value storage
165
- - `logger` - Scoped logger (debug, info, warn, error)
166
- - `http` - HTTP configuration (port, baseUrl, secret)
513
+ **Handler Context:**
514
+ - `req.params` - Path parameters from route pattern
515
+ - `req.body` - Parsed JSON body (POST/PUT/PATCH)
516
+ - `req.query` - URLSearchParams for query string
517
+ - `majk` - Full MAJK API
518
+ - `storage` - Plugin storage
519
+ - `logger` - Scoped logger
520
+ - `http` - HTTP config (port, baseUrl, secret)
167
521
 
168
- ### Tools
522
+ ---
169
523
 
170
- Tools are functions that agents can invoke:
524
+ ## ๐Ÿ› ๏ธ Tools
525
+
526
+ Tools are functions that AI agents can invoke. They're scoped to different contexts.
171
527
 
172
528
  ```typescript
173
529
  .tool(
174
530
  'conversation', // Scope: 'global' | 'conversation' | 'teammate' | 'project'
175
531
  {
176
- name: 'analyzeSentiment',
177
- description: 'Analyzes text sentiment. Returns positive, negative, or neutral classification.',
532
+ name: 'analyzeCode',
533
+ description: 'Analyzes code for potential issues. Returns findings with severity.',
178
534
  inputSchema: {
179
535
  type: 'object',
180
536
  properties: {
181
- text: { type: 'string' }
537
+ code: { type: 'string' },
538
+ language: { type: 'string' }
182
539
  },
183
- required: ['text']
540
+ required: ['code', 'language']
184
541
  }
185
542
  },
186
543
  async (input, { majk, logger }) => {
187
- logger.info('Analyzing sentiment');
544
+ logger.info(`Analyzing ${input.language} code`);
188
545
 
189
- // Your implementation
190
- const sentiment = analyzeSentiment(input.text);
546
+ const issues = analyzeCode(input.code, input.language);
191
547
 
192
548
  return {
193
549
  success: true,
194
- data: { sentiment, confidence: 0.95 }
550
+ data: {
551
+ issues,
552
+ summary: `Found ${issues.length} issues`
553
+ }
195
554
  };
196
555
  }
197
556
  )
@@ -199,115 +558,342 @@ Tools are functions that agents can invoke:
199
558
 
200
559
  **Tool Scopes:**
201
560
  - `global` - Available everywhere
202
- - `conversation` - Scoped to conversations
203
- - `teammate` - Scoped to teammates
204
- - `project` - Scoped to projects
561
+ - `conversation` - Scoped to specific conversations
562
+ - `teammate` - Scoped to specific teammates
563
+ - `project` - Scoped to specific projects
564
+
565
+ **Scoped Metadata:**
566
+
567
+ For non-global tools, specify scope entity in metadata:
205
568
 
206
- ### Entities
569
+ ```typescript
570
+ .tool('conversation', {
571
+ name: 'contextualSearch',
572
+ description: 'Searches within conversation context.',
573
+ inputSchema: { /* ... */ },
574
+ metadata: {
575
+ conversationId: 'specific-conversation-id' // Makes tool available only in this conversation
576
+ }
577
+ }, handler)
578
+ ```
579
+
580
+ ---
207
581
 
208
- Declare entities your plugin provides:
582
+ ## ๐Ÿ“ฆ Static Entities
583
+
584
+ Declare entities your plugin always provides:
585
+
586
+ ### MCP Servers
209
587
 
210
588
  ```typescript
211
- .entity('teammate', [
589
+ .mcpServer([
212
590
  {
213
- id: 'bot-assistant',
214
- name: 'Bot Assistant',
215
- role: 'bot',
216
- capabilities: ['analysis', 'reporting']
591
+ id: 'github-mcp',
592
+ name: 'GitHub MCP Server',
593
+ type: 'stdio',
594
+ command: 'npx',
595
+ args: ['-y', '@modelcontextprotocol/server-github'],
596
+ env: {
597
+ GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN
598
+ }
217
599
  }
218
600
  ])
601
+ ```
602
+
603
+ ### Team Members
219
604
 
220
- .entity('mcpServer', [
605
+ ```typescript
606
+ .teamMember([
221
607
  {
222
- id: 'custom-server',
223
- name: 'Custom MCP Server',
224
- transport: { type: 'stdio', command: 'node', args: ['server.js'] }
608
+ id: 'code-reviewer',
609
+ name: 'Code Reviewer',
610
+ role: 'reviewer',
611
+ systemPrompt: 'You are an expert code reviewer focusing on best practices.',
612
+ model: 'claude-3-sonnet',
613
+ expertise: ['javascript', 'typescript', 'code-review'],
614
+ isActive: true
615
+ }
616
+ ])
617
+ ```
618
+
619
+ ### Generic Entities
620
+
621
+ ```typescript
622
+ .entity('project', [
623
+ {
624
+ id: 'sample-project',
625
+ name: 'Sample Project',
626
+ description: 'A sample project for demonstration'
225
627
  }
226
628
  ])
227
629
  ```
228
630
 
229
631
  **Supported Entity Types:**
230
632
  - `mcpServer` - MCP servers
231
- - `teammate` - Team members/bots
633
+ - `teamMember` - Team members/bots
232
634
  - `conversation` - Conversations
233
- - `todo` - Tasks
234
635
  - `project` - Projects
235
636
  - `agent` - AI agents
236
637
 
237
- ### Config Wizard & Settings
638
+ ---
639
+
640
+ ## ๐Ÿ” Secret Providers
238
641
 
239
- #### Config Wizard
642
+ Plugins can act as secret providers, allowing them to supply secrets (API keys, tokens, credentials) to MAJK and other plugins. This enables integration with external secret management services like 1Password, AWS Secrets Manager, HashiCorp Vault, etc.
240
643
 
241
- Show a wizard on first run:
644
+ ### Basic Secret Provider
242
645
 
243
646
  ```typescript
244
- .configWizard({
245
- path: '/setup',
246
- title: 'Initial Setup',
247
- width: 600,
248
- height: 400,
249
- description: 'Configure plugin settings. Set up API keys and preferences.',
250
- shouldShow: async (ctx) => {
251
- const config = await ctx.storage.get('config');
252
- return !config; // Show if no config exists
647
+ .secretProvider({
648
+ name: 'My Vault',
649
+ priority: 50, // Lower = queried first (default: 100)
650
+ scopes: ['global', 'project', 'integration'],
651
+ description: 'Provides secrets from my vault service',
652
+ icon: '๐Ÿ”',
653
+ tags: ['vault', 'secrets'],
654
+ factory: (ctx) => {
655
+ // Initialize your secret vault connection
656
+ const vault = new Map([
657
+ ['API_KEY', { value: 'secret-key-123', scope: 'global' }],
658
+ ['DB_PASSWORD', { value: 'db-pass-456', scope: 'project', projectId: 'proj-1' }]
659
+ ]);
660
+
661
+ return {
662
+ // Required: resolve a secret by key
663
+ async resolve(key, scope, context) {
664
+ ctx.logger.info(`Resolving secret: ${key}`);
665
+
666
+ const secret = vault.get(key);
667
+ if (!secret) {
668
+ return {
669
+ found: false,
670
+ source: 'my-vault:not_found'
671
+ };
672
+ }
673
+
674
+ // Check scope matching
675
+ if (scope && secret.scope !== scope.type) {
676
+ return {
677
+ found: false,
678
+ source: 'my-vault:scope_mismatch'
679
+ };
680
+ }
681
+
682
+ return {
683
+ found: true,
684
+ value: secret.value,
685
+ scope: secret.scope,
686
+ source: 'my-vault'
687
+ };
688
+ },
689
+
690
+ // Optional: list available secrets
691
+ async list(scope) {
692
+ const secrets = [];
693
+ for (const [key, secret] of vault.entries()) {
694
+ if (!scope || secret.scope === scope.type) {
695
+ secrets.push({
696
+ key,
697
+ scope: { type: secret.scope, id: secret.projectId },
698
+ description: 'Secret from my vault',
699
+ createdAt: new Date(),
700
+ tags: ['my-vault']
701
+ });
702
+ }
703
+ }
704
+ return secrets;
705
+ },
706
+
707
+ // Optional: check if secret exists
708
+ async has(key, scope) {
709
+ const secret = vault.get(key);
710
+ if (!secret) return false;
711
+ if (!scope) return true;
712
+ return secret.scope === scope.type;
713
+ },
714
+
715
+ // Optional: get provider info
716
+ async getInfo() {
717
+ return {
718
+ connected: true,
719
+ vaultSize: vault.size,
720
+ lastSync: new Date(),
721
+ secretKeys: Array.from(vault.keys())
722
+ };
723
+ }
724
+ };
253
725
  }
254
726
  })
255
727
  ```
256
728
 
257
- #### Settings Screen
729
+ ### Scope Types
730
+
731
+ Secrets can be scoped to different contexts:
732
+
733
+ - **`global`** - Available everywhere
734
+ - **`project`** - Specific to a project (requires `projectId`)
735
+ - **`integration`** - Specific to an integration (requires `integrationId`)
736
+ - **`conversation`** - Specific to a conversation (requires `conversationId`)
737
+ - **`user`** - User-specific (requires `userId`)
258
738
 
259
- Ongoing settings management:
739
+ ### Resolution Context
740
+
741
+ When resolving secrets, MAJK provides context about where the secret is being used:
260
742
 
261
743
  ```typescript
262
- .settings({
263
- path: '/settings',
264
- title: 'Plugin Settings',
265
- description: 'Manage plugin configuration. Adjust behavior and display options.'
744
+ async resolve(key, scope, context) {
745
+ // context may contain:
746
+ // - conversationId: Current conversation ID
747
+ // - projectId: Current project ID
748
+ // - teamMemberId: Team member requesting the secret
749
+ // - integrationId: Integration requesting the secret
750
+ // - userId: User requesting the secret
751
+
752
+ if (scope?.type === 'project' && context?.projectId) {
753
+ // Find project-specific secret
754
+ return await getProjectSecret(key, context.projectId);
755
+ }
756
+
757
+ // Fall back to global secret
758
+ return await getGlobalSecret(key);
759
+ }
760
+ ```
761
+
762
+ ### Priority
763
+
764
+ Multiple secret providers can be registered. MAJK queries them in priority order (lower number = higher priority) until one returns `found: true`.
765
+
766
+ **Priority Guidelines:**
767
+ - **1-50**: High priority (external services like 1Password, AWS Secrets)
768
+ - **100**: Default priority
769
+ - **500+**: Low priority (fallback providers, testing)
770
+
771
+ ### Integration with External Services
772
+
773
+ **Example: AWS Secrets Manager**
774
+
775
+ ```typescript
776
+ import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
777
+
778
+ .secretProvider({
779
+ name: 'AWS Secrets Manager',
780
+ priority: 10,
781
+ scopes: ['global', 'project'],
782
+ factory: (ctx) => {
783
+ const client = new SecretsManagerClient({ region: 'us-east-1' });
784
+
785
+ return {
786
+ async resolve(key, scope) {
787
+ try {
788
+ const command = new GetSecretValueCommand({ SecretId: key });
789
+ const response = await client.send(command);
790
+
791
+ return {
792
+ found: true,
793
+ value: response.SecretString,
794
+ scope: 'global',
795
+ source: 'aws-secrets-manager'
796
+ };
797
+ } catch (error) {
798
+ if (error.name === 'ResourceNotFoundException') {
799
+ return { found: false, source: 'aws-secrets-manager:not_found' };
800
+ }
801
+ throw error;
802
+ }
803
+ }
804
+ };
805
+ }
266
806
  })
267
807
  ```
268
808
 
269
- ### Lifecycle Hooks
809
+ **Example: 1Password CLI**
270
810
 
271
- #### onReady
811
+ ```typescript
812
+ import { exec } from 'child_process';
813
+ import { promisify } from 'util';
272
814
 
273
- Called after server starts, before `onLoad` completes:
815
+ const execAsync = promisify(exec);
816
+
817
+ .secretProvider({
818
+ name: '1Password',
819
+ priority: 5,
820
+ scopes: ['global'],
821
+ factory: (ctx) => {
822
+ return {
823
+ async resolve(key) {
824
+ try {
825
+ const { stdout } = await execAsync(`op item get "${key}" --fields password`);
826
+ return {
827
+ found: true,
828
+ value: stdout.trim(),
829
+ scope: 'global',
830
+ source: '1password'
831
+ };
832
+ } catch (error) {
833
+ return { found: false, source: '1password:not_found' };
834
+ }
835
+ }
836
+ };
837
+ }
838
+ })
839
+ ```
840
+
841
+ ---
842
+
843
+ ## ๐Ÿ”„ Lifecycle Hooks
844
+
845
+ ### onReady
846
+
847
+ Called after the plugin's HTTP server starts, before `onLoad` completes:
274
848
 
275
849
  ```typescript
276
850
  .onReady(async (ctx, cleanup) => {
851
+ ctx.logger.info('Plugin initializing...');
852
+
277
853
  // Subscribe to events
278
- const sub = ctx.majk.eventBus.conversations().subscribe((event) => {
279
- ctx.logger.info(`Conversation event: ${event.type}`);
854
+ const conversationSub = ctx.majk.eventBus.conversations().subscribe((event) => {
855
+ ctx.logger.info(`Conversation ${event.type}: ${event.entity?.id}`);
280
856
  });
281
- cleanup(() => sub.unsubscribe());
857
+ cleanup(() => conversationSub.unsubscribe());
282
858
 
283
- // Set up timers
859
+ // Set up periodic tasks
284
860
  const timer = setInterval(() => {
285
- ctx.logger.debug('Periodic check');
861
+ ctx.logger.debug('Health check');
286
862
  }, 60000);
287
863
  cleanup(() => clearInterval(timer));
288
864
 
289
- // Any other setup
290
- await loadData(ctx.storage);
865
+ // Load data
866
+ const data = await ctx.storage.get('plugin-data') || {};
867
+ ctx.logger.info(`Loaded ${Object.keys(data).length} items`);
291
868
  })
292
869
  ```
293
870
 
294
- **Cleanup Registration:**
295
- All cleanup functions are automatically called on `onUnload()`.
871
+ **Cleanup Management:**
872
+ All cleanup functions registered via `cleanup()` are automatically called when the plugin unloads.
296
873
 
297
- #### Health Checks
874
+ ### Health Checks
298
875
 
299
876
  Define custom health monitoring:
300
877
 
301
878
  ```typescript
302
879
  .health(async ({ majk, storage, logger }) => {
303
880
  try {
304
- // Check dependencies
881
+ // Check MAJK API
305
882
  await majk.conversations.list();
306
- await storage.get('health-check');
883
+
884
+ // Check storage
885
+ await storage.get('health-test');
886
+
887
+ // Check external dependencies
888
+ const externalOk = await checkExternalAPI();
307
889
 
308
890
  return {
309
891
  healthy: true,
310
- details: { api: 'ok', storage: 'ok' }
892
+ details: {
893
+ api: 'ok',
894
+ storage: 'ok',
895
+ external: externalOk ? 'ok' : 'degraded'
896
+ }
311
897
  };
312
898
  } catch (error) {
313
899
  logger.error(`Health check failed: ${error.message}`);
@@ -319,7 +905,209 @@ Define custom health monitoring:
319
905
  })
320
906
  ```
321
907
 
322
- ## API Reference
908
+ ---
909
+
910
+ ## ๐ŸŽจ Client Generation
911
+
912
+ Plugin-kit can auto-generate TypeScript clients with React hooks from your function definitions.
913
+
914
+ ### Generate Client
915
+
916
+ ```bash
917
+ npx plugin-kit generate -e ./index.js -o ./ui/src/generated
918
+ ```
919
+
920
+ **Output:**
921
+ ```
922
+ ui/src/generated/
923
+ โ”œโ”€โ”€ types.ts # TypeScript types from schemas
924
+ โ”œโ”€โ”€ client.ts # Base client with fetch wrappers
925
+ โ”œโ”€โ”€ hooks.ts # React hooks (useGetMessage, etc)
926
+ โ””โ”€โ”€ index.ts # Re-exports
927
+ ```
928
+
929
+ ### Using Generated Hooks
930
+
931
+ ```tsx
932
+ import { useCreateTask, useGetTasks } from './generated';
933
+
934
+ function TaskList() {
935
+ // Query hook
936
+ const { data: tasks, loading, error, refetch } = useGetTasks({});
937
+
938
+ // Mutation hook
939
+ const { mutate: createTask, loading: creating } = useCreateTask();
940
+
941
+ const handleCreate = async () => {
942
+ const result = await createTask({
943
+ title: 'New task',
944
+ priority: 'high'
945
+ });
946
+
947
+ if (result.success) {
948
+ refetch(); // Refresh list
949
+ }
950
+ };
951
+
952
+ if (loading) return <div>Loading...</div>;
953
+ if (error) return <div>Error: {error.message}</div>;
954
+
955
+ return (
956
+ <div>
957
+ {tasks?.map(task => (
958
+ <div key={task.id}>{task.title}</div>
959
+ ))}
960
+ <button onClick={handleCreate} disabled={creating}>
961
+ {creating ? 'Creating...' : 'Add Task'}
962
+ </button>
963
+ </div>
964
+ );
965
+ }
966
+ ```
967
+
968
+ ### Hook API
969
+
970
+ **Query Hooks** (for data fetching):
971
+ ```typescript
972
+ const { data, loading, error, refetch } = useGetTasks({ filter: 'active' });
973
+ ```
974
+
975
+ **Mutation Hooks** (for data modification):
976
+ ```typescript
977
+ const { mutate, loading, error } = useCreateTask();
978
+
979
+ // Call the mutation
980
+ const result = await mutate({ title: 'Task' });
981
+ ```
982
+
983
+ ### Auto-Generated Config Functions
984
+
985
+ When you define a `configWizard` with a `schema`, plugin-kit automatically generates:
986
+
987
+ ```typescript
988
+ // In your UI
989
+ import { useUpdateConfig, useGetConfig } from './generated';
990
+
991
+ function ConfigWizard() {
992
+ const { mutate: updateConfig, loading } = useUpdateConfig();
993
+ const { data: config } = useGetConfig({});
994
+
995
+ const handleSubmit = async (formData) => {
996
+ const result = await updateConfig(formData);
997
+
998
+ if (result.success) {
999
+ // Close modal
1000
+ window.parent.postMessage({
1001
+ type: 'majk:config-complete',
1002
+ pluginId: 'my-plugin',
1003
+ redirectTo: '/plugins'
1004
+ }, '*');
1005
+ }
1006
+ };
1007
+
1008
+ return <form onSubmit={handleSubmit}>...</form>;
1009
+ }
1010
+ ```
1011
+
1012
+ ---
1013
+
1014
+ ## ๐Ÿ“– Complete Example
1015
+
1016
+ See `samples/kitchen-sink` for a comprehensive example showing:
1017
+
1018
+ ```typescript
1019
+ import { definePlugin, HttpTransport } from '@majkapp/plugin-kit';
1020
+
1021
+ const ConfigSchema = {
1022
+ type: 'object',
1023
+ properties: {
1024
+ name: { type: 'string' },
1025
+ systemPrompt: { type: 'string' }
1026
+ },
1027
+ required: ['name']
1028
+ };
1029
+
1030
+ export = definePlugin('kitchen-sink', 'Kitchen Sink Demo', '1.0.0')
1031
+ .pluginRoot(__dirname)
1032
+
1033
+ // Functions
1034
+ .function('createTask', {
1035
+ description: 'Creates a new task.',
1036
+ input: { /* schema */ },
1037
+ output: { /* schema */ },
1038
+ handler: async (input, ctx) => { /* ... */ }
1039
+ })
1040
+
1041
+ // Configurable entities
1042
+ .configWizard({
1043
+ schema: ConfigSchema,
1044
+ storageKey: 'teammate-config',
1045
+ path: '/plugin-screens/kitchen-sink/main',
1046
+ hash: '#/config-wizard',
1047
+ title: 'Configure Assistant',
1048
+ shouldShow: async (ctx) => !await ctx.storage.get('teammate-config')
1049
+ })
1050
+
1051
+ .configurableTeamMember((config) => [{
1052
+ id: 'my-assistant',
1053
+ name: config.name,
1054
+ systemPrompt: config.systemPrompt
1055
+ }])
1056
+
1057
+ // UI
1058
+ .ui({ appDir: 'dist', base: '/', history: 'hash' })
1059
+
1060
+ .screenReact({
1061
+ id: 'main',
1062
+ name: 'Kitchen Sink',
1063
+ description: 'Comprehensive demo of all plugin-kit features.',
1064
+ route: '/plugin-screens/kitchen-sink/main',
1065
+ reactPath: '/'
1066
+ })
1067
+
1068
+ // Tools
1069
+ .tool('global', {
1070
+ name: 'analyze',
1071
+ description: 'Analyzes input data.',
1072
+ inputSchema: { /* ... */ }
1073
+ }, handler)
1074
+
1075
+ // Static entities
1076
+ .mcpServer([{ /* ... */ }])
1077
+
1078
+ // Navigation
1079
+ .topbar('/plugin-screens/kitchen-sink/main', { icon: '๐Ÿงฐ' })
1080
+
1081
+ // Settings
1082
+ .settings({
1083
+ path: '/plugin-screens/kitchen-sink/main',
1084
+ hash: '#/settings',
1085
+ title: 'Settings',
1086
+ description: 'Reconfigure the plugin.'
1087
+ })
1088
+
1089
+ // Lifecycle
1090
+ .onReady(async (ctx, cleanup) => {
1091
+ const sub = ctx.majk.eventBus.subscribeAll((event) => {
1092
+ ctx.logger.info(`Event: ${event.type}`);
1093
+ });
1094
+ cleanup(() => sub.unsubscribe());
1095
+ })
1096
+
1097
+ .health(async ({ storage }) => {
1098
+ await storage.get('health-check');
1099
+ return { healthy: true };
1100
+ })
1101
+
1102
+ // Transport
1103
+ .transport(new HttpTransport())
1104
+
1105
+ .build();
1106
+ ```
1107
+
1108
+ ---
1109
+
1110
+ ## ๐Ÿ“š API Reference
323
1111
 
324
1112
  ### PluginContext
325
1113
 
@@ -327,33 +1115,71 @@ Provided to all handlers and hooks:
327
1115
 
328
1116
  ```typescript
329
1117
  interface PluginContext {
330
- pluginId: string; // Your plugin ID
331
- pluginRoot: string; // Plugin directory path
332
- dataDir: string; // Plugin data directory
1118
+ pluginId: string; // Your plugin ID
1119
+ pluginRoot: string; // Plugin directory path
1120
+ dataDir: string; // Plugin data directory
333
1121
 
334
1122
  app: {
335
- version: string; // MAJK version
336
- name: string; // App name
337
- appDataDir: string; // App data directory
1123
+ version: string; // MAJK version
1124
+ name: string; // App name
1125
+ appDataDir: string; // App data directory
338
1126
  };
339
1127
 
340
1128
  http: {
341
- port: number; // Assigned HTTP port
342
- secret: string; // Security secret
343
- baseUrl: string; // Base URL for iframe
1129
+ port: number; // Assigned HTTP port
1130
+ secret: string; // Security secret
1131
+ baseUrl: string; // Base URL for iframe
344
1132
  };
345
1133
 
346
- majk: MajkInterface; // Full MAJK API
347
- storage: PluginStorage; // Key-value storage
348
- logger: PluginLogger; // Scoped logger
349
- timers?: ScopedTimers; // Managed timers
350
- ipc?: ScopedIpcRegistry; // Electron IPC
1134
+ majk: MajkInterface; // Full MAJK API
1135
+ storage: PluginStorage; // Key-value storage
1136
+ logger: PluginLogger; // Scoped logger
1137
+ timers?: ScopedTimers; // Managed timers
1138
+ ipc?: ScopedIpcRegistry; // Electron IPC
1139
+ }
1140
+ ```
1141
+
1142
+ ### PluginStorage
1143
+
1144
+ Simple key-value storage scoped to your plugin:
1145
+
1146
+ ```typescript
1147
+ interface PluginStorage {
1148
+ get<T>(key: string): Promise<T | undefined>;
1149
+ set<T>(key: string, value: T): Promise<void>;
1150
+ delete(key: string): Promise<void>;
1151
+ clear(): Promise<void>;
1152
+ keys(): Promise<string[]>;
351
1153
  }
1154
+
1155
+ // Usage
1156
+ await storage.set('user-prefs', { theme: 'dark' });
1157
+ const prefs = await storage.get('user-prefs');
1158
+ await storage.delete('old-key');
1159
+ await storage.clear();
1160
+ const allKeys = await storage.keys();
1161
+ ```
1162
+
1163
+ ### PluginLogger
1164
+
1165
+ Structured logging with levels:
1166
+
1167
+ ```typescript
1168
+ interface PluginLogger {
1169
+ debug(message: string, ...args: any[]): void;
1170
+ info(message: string, ...args: any[]): void;
1171
+ warn(message: string, ...args: any[]): void;
1172
+ error(message: string, ...args: any[]): void;
1173
+ }
1174
+
1175
+ // Usage
1176
+ logger.info('User action', { userId, action: 'create' });
1177
+ logger.error('Failed to process', { error: error.message });
352
1178
  ```
353
1179
 
354
1180
  ### MajkInterface
355
1181
 
356
- The main MAJK API:
1182
+ The main MAJK API (subset shown):
357
1183
 
358
1184
  ```typescript
359
1185
  interface MajkInterface {
@@ -371,266 +1197,464 @@ interface MajkInterface {
371
1197
  }
372
1198
  ```
373
1199
 
374
- ### PluginStorage
1200
+ ### MAJK API Deep Dive
375
1201
 
376
- Simple key-value storage scoped to your plugin:
1202
+ The `ctx.majk` object provides access to the full MAJK platform API. Below are detailed examples of each subsystem with **verified** code from the kitchen-sink sample.
377
1203
 
378
- ```typescript
379
- interface PluginStorage {
380
- get<T>(key: string): Promise<T | undefined>;
381
- set<T>(key: string, value: T): Promise<void>;
382
- delete(key: string): Promise<void>;
383
- clear(): Promise<void>;
384
- keys(): Promise<string[]>;
385
- }
386
- ```
1204
+ #### Conversations API
387
1205
 
388
- **Example:**
1206
+ Manage conversations and messages:
389
1207
 
390
1208
  ```typescript
391
- // Save data
392
- await storage.set('user-preferences', {
393
- theme: 'dark',
394
- notifications: true
395
- });
1209
+ // List all conversations
1210
+ .function('getConversations', {
1211
+ description: 'Lists all conversations.',
1212
+ handler: async (_, ctx) => {
1213
+ const conversations = await ctx.majk.conversations.list();
1214
+
1215
+ return conversations.map(conv => ({
1216
+ id: conv.id,
1217
+ title: conv.title || 'Untitled',
1218
+ createdAt: conv.createdAt,
1219
+ messageCount: conv.messageCount || 0
1220
+ }));
1221
+ }
1222
+ })
396
1223
 
397
- // Load data
398
- const prefs = await storage.get<Preferences>('user-preferences');
1224
+ // Get a specific conversation with messages
1225
+ .function('getConversationMessages', {
1226
+ description: 'Gets messages from a conversation.',
1227
+ handler: async ({ conversationId }, ctx) => {
1228
+ // Get conversation handle
1229
+ const conversation = await ctx.majk.conversations.get(conversationId);
399
1230
 
400
- // List all keys
401
- const keys = await storage.keys();
1231
+ if (!conversation) {
1232
+ return { success: false, error: 'Conversation not found' };
1233
+ }
402
1234
 
403
- // Delete specific key
404
- await storage.delete('old-data');
1235
+ // Fetch messages
1236
+ const messages = await conversation.getMessages();
405
1237
 
406
- // Clear everything
407
- await storage.clear();
1238
+ return {
1239
+ success: true,
1240
+ data: messages.map(msg => ({
1241
+ id: msg.id,
1242
+ content: msg.content,
1243
+ role: msg.role,
1244
+ createdAt: msg.createdAt
1245
+ }))
1246
+ };
1247
+ }
1248
+ })
408
1249
  ```
409
1250
 
410
- ### EventBus
1251
+ **Available Methods:**
1252
+ - `list()` - Get all conversations
1253
+ - `get(id)` - Get conversation handle
1254
+ - `conversation.getMessages()` - Get messages from conversation
1255
+ - `conversation.addMessage(message)` - Add message to conversation
1256
+
1257
+ #### Teammates API
411
1258
 
412
- Subscribe to system events:
1259
+ Manage AI teammates:
413
1260
 
414
1261
  ```typescript
415
- // Listen to conversation events
416
- const sub = majk.eventBus.conversations().subscribe((event) => {
417
- console.log(`Event: ${event.type}`, event.entity);
418
- });
1262
+ // List all teammates
1263
+ .function('getTeammates', {
1264
+ description: 'Lists all teammates.',
1265
+ handler: async (_, ctx) => {
1266
+ const teammates = await ctx.majk.teammates.list();
1267
+
1268
+ return teammates.map(teammate => ({
1269
+ id: teammate.id,
1270
+ name: teammate.name,
1271
+ systemPrompt: teammate.systemPrompt,
1272
+ expertise: teammate.expertise || [],
1273
+ mcpServerIds: teammate.mcpServerIds || [],
1274
+ skills: teammate.skills || {}
1275
+ }));
1276
+ }
1277
+ })
419
1278
 
420
- // Unsubscribe
421
- sub.unsubscribe();
1279
+ // Get specific teammate
1280
+ .function('getTeammate', {
1281
+ description: 'Gets a specific teammate.',
1282
+ handler: async ({ teammateId }, ctx) => {
1283
+ const teammate = await ctx.majk.teammates.get(teammateId);
422
1284
 
423
- // Specific event types
424
- majk.eventBus.conversations().created().subscribe(...);
425
- majk.eventBus.conversations().updated().subscribe(...);
426
- majk.eventBus.conversations().deleted().subscribe(...);
1285
+ if (!teammate) {
1286
+ return { success: false, error: 'Teammate not found' };
1287
+ }
427
1288
 
428
- // Custom channels
429
- majk.eventBus.channel('my-events').subscribe(...);
1289
+ return {
1290
+ success: true,
1291
+ data: {
1292
+ id: teammate.id,
1293
+ name: teammate.name,
1294
+ systemPrompt: teammate.systemPrompt,
1295
+ expertise: teammate.expertise,
1296
+ personality: teammate.personality,
1297
+ metadata: teammate.metadata
1298
+ }
1299
+ };
1300
+ }
1301
+ })
430
1302
  ```
431
1303
 
432
- ## Validation & Error Handling
1304
+ **Available Methods:**
1305
+ - `list()` - Get all teammates
1306
+ - `get(id)` - Get specific teammate
1307
+ - `create(teammate)` - Create new teammate (used by configurable entities)
1308
+ - `update(id, teammate)` - Update teammate
1309
+ - `delete(id)` - Delete teammate
433
1310
 
434
- ### Build-Time Validation
1311
+ #### Authentication API
435
1312
 
436
- The kit validates at build time:
1313
+ Access user authentication information:
437
1314
 
438
- โœ… **Route Prefixes** - Screen routes must match plugin ID
439
- โœ… **File Existence** - React dist and HTML files must exist
440
- โœ… **Uniqueness** - No duplicate routes, tools, or API endpoints
441
- โœ… **Dependencies** - UI must be configured for React screens
442
- โœ… **Descriptions** - Must be 2-3 sentences ending with period
1315
+ ```typescript
1316
+ // Check authentication status
1317
+ .function('checkAuth', {
1318
+ description: 'Checks if user is authenticated.',
1319
+ handler: async (_, ctx) => {
1320
+ const isAuthenticated = await ctx.majk.auth.isAuthenticated();
1321
+ const account = await ctx.majk.auth.getAccount();
443
1322
 
444
- **Example Error:**
1323
+ return {
1324
+ authenticated: isAuthenticated,
1325
+ account: account ? {
1326
+ id: account.id,
1327
+ name: account.name,
1328
+ email: account.email
1329
+ } : null
1330
+ };
1331
+ }
1332
+ })
445
1333
 
1334
+ // Get accounts of specific type
1335
+ .function('getAccounts', {
1336
+ description: 'Gets accounts by type.',
1337
+ handler: async ({ accountType }, ctx) => {
1338
+ // accountType: 'github', 'google', 'email', or '*' for all
1339
+ const accounts = await ctx.majk.auth.getAccountsOfType(accountType);
1340
+
1341
+ return accounts.map(account => ({
1342
+ id: account.id,
1343
+ type: account.type,
1344
+ name: account.name,
1345
+ email: account.email
1346
+ }));
1347
+ }
1348
+ })
446
1349
  ```
447
- โŒ Plugin Build Failed: React screen route must start with "/plugin-screens/my-plugin/"
448
- ๐Ÿ’ก Suggestion: Change route from "/screens/dashboard" to "/plugin-screens/my-plugin/dashboard"
449
- ๐Ÿ“‹ Context: {
450
- "screen": "dashboard",
451
- "route": "/screens/dashboard"
452
- }
453
- ```
454
1350
 
455
- ### Runtime Error Handling
1351
+ **Available Methods:**
1352
+ - `isAuthenticated()` - Check if user is logged in
1353
+ - `getAccount()` - Get current account
1354
+ - `getAccountsOfType(type)` - Get accounts by type ('github', 'google', 'email', '*')
1355
+
1356
+ #### EventBus API
456
1357
 
457
- All API route errors are automatically caught and logged:
1358
+ Subscribe to real-time system events:
458
1359
 
459
1360
  ```typescript
460
- .apiRoute({
461
- method: 'POST',
462
- path: '/api/process',
463
- name: 'Process Data',
464
- description: 'Processes input data. Validates and transforms the payload.',
465
- handler: async (req, res, { logger }) => {
466
- // Errors are automatically caught and returned as 500 responses
467
- throw new Error('Processing failed');
468
-
469
- // Returns:
1361
+ // Subscribe to all events (in onReady hook)
1362
+ .onReady(async (ctx, cleanup) => {
1363
+ const subscription = ctx.majk.eventBus.subscribeAll((event) => {
1364
+ ctx.logger.info(`Event: ${event.entityType}.${event.type}`);
1365
+ ctx.logger.info(`Entity ID: ${event.entity?.id}`);
1366
+
1367
+ // event structure:
470
1368
  // {
471
- // "error": "Processing failed",
472
- // "route": "Process Data",
473
- // "path": "/api/process"
1369
+ // type: 'created' | 'updated' | 'deleted',
1370
+ // entityType: 'conversation' | 'teammate' | 'todo' | etc,
1371
+ // entity: { id, ...otherFields },
1372
+ // metadata: { ... }
474
1373
  // }
475
- }
1374
+ });
1375
+
1376
+ // Always cleanup subscriptions
1377
+ cleanup(() => subscription.unsubscribe());
1378
+ })
1379
+
1380
+ // Subscribe to specific entity type
1381
+ .onReady(async (ctx, cleanup) => {
1382
+ // Conversations only
1383
+ const convSub = ctx.majk.eventBus.conversations().subscribe((event) => {
1384
+ if (event.type === 'created') {
1385
+ ctx.logger.info(`New conversation: ${event.entity.title}`);
1386
+ }
1387
+ });
1388
+
1389
+ cleanup(() => convSub.unsubscribe());
476
1390
  })
477
- ```
478
1391
 
479
- Logs show:
1392
+ // Subscribe to specific event type
1393
+ .onReady(async (ctx, cleanup) => {
1394
+ // Only conversation creations
1395
+ const sub = ctx.majk.eventBus.conversations().created().subscribe((event) => {
1396
+ ctx.logger.info(`Created: ${event.entity.id}`);
1397
+ });
1398
+
1399
+ cleanup(() => sub.unsubscribe());
1400
+ })
1401
+
1402
+ // Custom channel
1403
+ .onReady(async (ctx, cleanup) => {
1404
+ const sub = ctx.majk.eventBus.channel('my-plugin-events').subscribe((event) => {
1405
+ // Handle custom events
1406
+ });
1407
+
1408
+ cleanup(() => sub.unsubscribe());
1409
+ })
480
1410
  ```
481
- โŒ POST /api/process - Error: Processing failed
482
- [stack trace]
1411
+
1412
+ **Event Types:**
1413
+ - `created` - Entity was created
1414
+ - `updated` - Entity was modified
1415
+ - `deleted` - Entity was removed
1416
+
1417
+ **Entity Types:**
1418
+ - `conversation` - Conversation events
1419
+ - `teammate` - Teammate events
1420
+ - `todo` - Todo/task events
1421
+ - `project` - Project events
1422
+ - `message` - Message events
1423
+
1424
+ **Available Methods:**
1425
+ - `subscribeAll(handler)` - Subscribe to all events
1426
+ - `conversations()` - Conversation events only
1427
+ - `teammates()` - Teammate events only
1428
+ - `todos()` - Todo events only
1429
+ - `projects()` - Project events only
1430
+ - `channel(name)` - Custom event channel
1431
+ - `.created()` - Filter to created events only
1432
+ - `.updated()` - Filter to updated events only
1433
+ - `.deleted()` - Filter to deleted events only
1434
+ - `subscription.unsubscribe()` - Stop listening
1435
+
1436
+ #### Storage API
1437
+
1438
+ Plugin-scoped persistent storage:
1439
+
1440
+ ```typescript
1441
+ // Save data
1442
+ .function('saveSettings', {
1443
+ description: 'Saves user settings.',
1444
+ handler: async ({ settings }, ctx) => {
1445
+ await ctx.storage.set('user-settings', settings);
1446
+ return { success: true };
1447
+ }
1448
+ })
1449
+
1450
+ // Load data
1451
+ .function('getSettings', {
1452
+ description: 'Loads user settings.',
1453
+ handler: async (_, ctx) => {
1454
+ const settings = await ctx.storage.get('user-settings');
1455
+ return settings || { theme: 'light', notifications: true };
1456
+ }
1457
+ })
1458
+
1459
+ // List all keys
1460
+ .function('listStorageKeys', {
1461
+ description: 'Lists all storage keys.',
1462
+ handler: async (_, ctx) => {
1463
+ const keys = await ctx.storage.keys();
1464
+ return { keys, count: keys.length };
1465
+ }
1466
+ })
1467
+
1468
+ // Delete specific key
1469
+ .function('clearCache', {
1470
+ description: 'Clears cached data.',
1471
+ handler: async (_, ctx) => {
1472
+ await ctx.storage.delete('cache');
1473
+ return { success: true };
1474
+ }
1475
+ })
1476
+
1477
+ // Clear all storage
1478
+ .function('resetPlugin', {
1479
+ description: 'Resets all plugin data.',
1480
+ handler: async (_, ctx) => {
1481
+ await ctx.storage.clear();
1482
+ return { success: true, message: 'All data cleared' };
1483
+ }
1484
+ })
483
1485
  ```
484
1486
 
485
- ## Best Practices
1487
+ **Available Methods:**
1488
+ - `get<T>(key)` - Retrieve value (returns undefined if not found)
1489
+ - `set<T>(key, value)` - Store value (any JSON-serializable data)
1490
+ - `delete(key)` - Remove specific key
1491
+ - `clear()` - Remove all plugin data
1492
+ - `keys()` - List all storage keys
1493
+
1494
+ **Best Practices:**
1495
+ - โœ… Use namespaced keys: `user-settings`, `cache:conversations`, `state:ui`
1496
+ - โœ… Type your storage: `await ctx.storage.get<UserSettings>('settings')`
1497
+ - โœ… Handle missing data: `const data = await ctx.storage.get('key') || defaultValue`
1498
+ - โŒ Don't store secrets - use environment variables or MAJK secrets API
1499
+
1500
+ ---
486
1501
 
487
- ### 1. Use Storage for State
1502
+ ## โœ… Best Practices
1503
+
1504
+ ### 1. Always Use `.pluginRoot(__dirname)`
488
1505
 
489
1506
  ```typescript
490
- // โŒ Don't use in-memory state
1507
+ // โœ… CORRECT
1508
+ definePlugin('my-plugin', 'My Plugin', '1.0.0')
1509
+ .pluginRoot(__dirname)
1510
+ // ...
1511
+
1512
+ // โŒ WRONG - file resolution may fail
1513
+ definePlugin('my-plugin', 'My Plugin', '1.0.0')
1514
+ // ...
1515
+ ```
1516
+
1517
+ ### 2. Use Storage for State
1518
+
1519
+ ```typescript
1520
+ // โŒ Don't use in-memory state (lost on reload)
491
1521
  let cache = {};
492
1522
 
493
- // โœ… Use storage
1523
+ // โœ… Use storage (persisted)
494
1524
  await ctx.storage.set('cache', data);
495
1525
  ```
496
1526
 
497
- ### 2. Register Cleanups
1527
+ ### 3. Register Cleanups
498
1528
 
499
1529
  ```typescript
500
1530
  .onReady(async (ctx, cleanup) => {
501
- // โŒ Don't forget to cleanup
502
- const timer = setInterval(...);
1531
+ // โŒ Forgot to cleanup
1532
+ const timer = setInterval(() => { /* ... */ }, 1000);
503
1533
 
504
- // โœ… Register cleanup
505
- const timer = setInterval(...);
1534
+ // โœ… Registered cleanup
1535
+ const timer = setInterval(() => { /* ... */ }, 1000);
506
1536
  cleanup(() => clearInterval(timer));
507
1537
 
508
1538
  // โœ… Event subscriptions
509
- const sub = ctx.majk.eventBus.conversations().subscribe(...);
1539
+ const sub = ctx.majk.eventBus.conversations().subscribe(handler);
510
1540
  cleanup(() => sub.unsubscribe());
511
1541
  })
512
1542
  ```
513
1543
 
514
- ### 3. Validate Input
1544
+ ### 4. Write Good Descriptions
515
1545
 
516
1546
  ```typescript
517
- .apiRoute({
518
- method: 'POST',
519
- path: '/api/create',
520
- name: 'Create Item',
521
- description: 'Creates a new item. Validates input before processing.',
522
- handler: async (req, res) => {
523
- // โœ… Validate input
524
- if (!req.body?.name) {
525
- res.status(400).json({ error: 'Name is required' });
526
- return;
527
- }
1547
+ // โŒ Too short
1548
+ description: 'Dashboard screen'
528
1549
 
529
- // Process...
530
- }
531
- })
1550
+ // โœ… 2-3 sentences, clear purpose
1551
+ description: 'Main dashboard view. Shows key metrics and recent activity.'
532
1552
  ```
533
1553
 
534
- ### 4. Use Structured Logging
1554
+ ### 5. Handle Errors Gracefully
535
1555
 
536
1556
  ```typescript
537
- // โŒ Basic logging
538
- logger.info('User action');
539
-
540
- // โœ… Structured logging
541
- logger.info('User action', { userId, action: 'create', resourceId });
1557
+ // โœ… Return structured errors
1558
+ .function('process', {
1559
+ // ...
1560
+ handler: async (input, ctx) => {
1561
+ try {
1562
+ const result = await processData(input);
1563
+ return { success: true, data: result };
1564
+ } catch (error) {
1565
+ ctx.logger.error('Processing failed', { error: error.message });
1566
+ return {
1567
+ success: false,
1568
+ error: error.message,
1569
+ code: 'PROCESSING_ERROR'
1570
+ };
1571
+ }
1572
+ }
1573
+ })
542
1574
  ```
543
1575
 
544
- ### 5. Handle Errors Gracefully
1576
+ ### 6. Validate Input
545
1577
 
546
1578
  ```typescript
547
- .tool('global', spec, async (input, { logger }) => {
548
- try {
549
- const result = await processData(input);
550
- return { success: true, data: result };
551
- } catch (error) {
552
- logger.error(`Tool failed: ${error.message}`);
553
- return {
554
- success: false,
555
- error: error.message,
556
- code: 'PROCESSING_ERROR'
557
- };
1579
+ .function('create', {
1580
+ // ...
1581
+ handler: async (input, ctx) => {
1582
+ // Validate business rules beyond schema
1583
+ if (input.priority === 'high' && !input.assignee) {
1584
+ return {
1585
+ success: false,
1586
+ error: 'High priority items must have an assignee'
1587
+ };
1588
+ }
1589
+ // Process...
558
1590
  }
559
1591
  })
560
1592
  ```
561
1593
 
562
- ## Examples
1594
+ ### 7. Use Configurable Entities for User Customization
563
1595
 
564
- See `example.ts` for a comprehensive example showing:
565
- - React and HTML screens
566
- - API routes with parameters
567
- - Tools in different scopes
568
- - Entity declarations
569
- - Config wizard
570
- - Event subscriptions
571
- - Storage usage
572
- - Health checks
1596
+ ```typescript
1597
+ // โœ… Let users configure teammates
1598
+ .configWizard({ schema: TeammateConfigSchema, ... })
1599
+ .configurableTeamMember((config) => [buildTeammate(config)])
573
1600
 
574
- ## TypeScript
1601
+ // โŒ Hard-coded teammates (less flexible)
1602
+ .teamMember([{ id: 'assistant', name: 'Assistant', ... }])
1603
+ ```
575
1604
 
576
- Full TypeScript support with:
1605
+ ---
577
1606
 
578
- ```typescript
579
- import {
580
- definePlugin,
581
- FluentBuilder,
582
- PluginContext,
583
- RequestLike,
584
- ResponseLike
585
- } from '@majk/plugin-kit';
1607
+ ## ๐Ÿ› Troubleshooting
586
1608
 
587
- // Type-safe plugin ID
588
- const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0');
589
- // ^ Enforces route prefixes
1609
+ ### Build Errors
590
1610
 
591
- // Type-safe routes
592
- .screenReact({
593
- route: '/plugin-screens/my-plugin/dashboard'
594
- // ^^^^^^^^^ Must match plugin ID
595
- })
1611
+ **"React app not built"**
596
1612
  ```
1613
+ โŒ React app not built: /path/to/ui/dist/index.html does not exist
1614
+ ๐Ÿ’ก Run "npm run build" in your UI directory
1615
+ ```
1616
+ Fix: Build your React app before building the plugin.
597
1617
 
598
- ## Troubleshooting
599
-
600
- ### "React app not built"
1618
+ **"Configurable entities require configWizard with schema"**
1619
+ ```
1620
+ โŒ You used .configurableTeamMember() but did not call .configWizard()
1621
+ ๐Ÿ’ก Add .configWizard({ schema: YourSchema, ... })
1622
+ ```
601
1623
 
1624
+ **"Screen route must start with /plugin-screens/{id}/"**
602
1625
  ```
603
- โŒ Plugin Build Failed: React app not built: /path/to/ui/dist/index.html does not exist
604
- ๐Ÿ’ก Suggestion: Run "npm run build" in your UI directory to build the React app
1626
+ โŒ Route "/screens/dashboard" doesn't match pattern
1627
+ ๐Ÿ’ก Change to "/plugin-screens/my-plugin/dashboard"
605
1628
  ```
606
1629
 
607
- **Fix:** Build your React app before building the plugin.
1630
+ ### Runtime Errors
608
1631
 
609
- ### "Duplicate API route"
1632
+ **"Cannot read properties of undefined (reading 'logger')"**
1633
+ - Ensure `getCapabilities()` checks if `context` is available
1634
+ - This is handled automatically in v1.1.0+
610
1635
 
611
- ```
612
- โŒ Plugin Build Failed: Duplicate API route: POST /api/data
613
- ```
1636
+ **Functions not appearing in generated client**
1637
+ - Ensure you call `.transport(new HttpTransport())`
1638
+ - Run `npx plugin-kit generate` after adding functions
614
1639
 
615
- **Fix:** Each route (method + path) must be unique.
1640
+ ### Config Not Working
616
1641
 
617
- ### "Description must be 2-3 sentences"
1642
+ **Teammate not appearing after config wizard**
1643
+ - Check that `postMessage({ type: 'majk:config-complete' })` is sent
1644
+ - Verify `pluginId` in postMessage matches your plugin ID
1645
+ - Check plugin logs for `[ConfigWizard]` messages
1646
+ - Ensure plugin reloaded after config completion
618
1647
 
619
- ```
620
- โŒ Plugin Build Failed: Description for "My Screen" must be 2-3 sentences, found 1 sentences
621
- ๐Ÿ’ก Suggestion: Rewrite the description to have 2-3 clear sentences.
622
- ```
1648
+ ---
623
1649
 
624
- **Fix:** Write 2-3 complete sentences ending with periods.
1650
+ ## ๐Ÿ“ License
625
1651
 
626
- ### "Tool names must be unique"
1652
+ MIT
627
1653
 
628
- ```
629
- โŒ Plugin Build Failed: Duplicate tool name: "analyze"
630
- ```
1654
+ ## ๐Ÿค Contributing
631
1655
 
632
- **Fix:** Each tool name must be unique within the plugin.
1656
+ Issues and pull requests welcome at https://github.com/gaiin-platform/majk-plugins
633
1657
 
634
- ## License
1658
+ ---
635
1659
 
636
- MIT
1660
+ **Built with โค๏ธ by the MAJK team**