@majkapp/plugin-kit 1.2.0 → 1.2.2

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,480 +1,143 @@
1
- # @majkapp/plugin-kit
1
+ # @majk/plugin-kit
2
2
 
3
- **Modern, type-safe framework for building MAJK plugins with exceptional developer experience**
3
+ **Fluent builder framework for creating robust MAJK plugins**
4
4
 
5
- Build production-ready MAJK plugins using a fluent builder API with comprehensive validation, auto-generated clients, and declarative configuration management.
5
+ Build type-safe, production-ready MAJK plugins with excellent developer experience, comprehensive validation, and clear error messages.
6
6
 
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)
7
+ ## Features
9
8
 
10
- ## Features
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
11
20
 
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
21
+ ## Installation
26
22
 
27
23
  ```bash
28
- npm install @majkapp/plugin-kit
24
+ npm install @majk/plugin-kit
29
25
  ```
30
26
 
31
- ---
32
-
33
- ## 🚀 Quick Start
27
+ ## Quick Start
34
28
 
35
29
  ```typescript
36
- import { definePlugin, HttpTransport } from '@majkapp/plugin-kit';
37
-
38
- export = definePlugin('my-plugin', 'My Plugin', '1.0.0')
39
- .pluginRoot(__dirname)
30
+ import { definePlugin } from '@majk/plugin-kit';
40
31
 
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
- }
60
- })
32
+ export default definePlugin('my-plugin', 'My Plugin', '1.0.0')
33
+ .pluginRoot(__dirname) // REQUIRED: Must be first call
61
34
 
62
- // Configure React UI
63
35
  .ui({
64
36
  appDir: 'dist',
65
- base: '/',
37
+ base: '/plugin-screens/my-plugin', // NO trailing slash
66
38
  history: 'hash'
67
39
  })
68
40
 
69
- // Add a screen
41
+ .topbar('/plugin-screens/my-plugin/dashboard', {
42
+ icon: '🚀'
43
+ })
44
+
70
45
  .screenReact({
71
- id: 'main',
72
- name: 'My Plugin',
73
- description: 'Main plugin screen. Shows greeting and controls.',
74
- route: '/plugin-screens/my-plugin/main',
46
+ id: 'dashboard',
47
+ name: 'Dashboard',
48
+ description: 'Main dashboard for my plugin. Shows key metrics and actions.',
49
+ route: '/plugin-screens/my-plugin/dashboard',
75
50
  reactPath: '/'
76
51
  })
77
52
 
78
- // Enable HTTP transport
79
- .transport(new HttpTransport())
80
-
81
- .build();
82
- ```
83
-
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';
53
+ .apiRoute({
54
+ method: 'GET',
55
+ path: '/api/data',
56
+ name: 'Get Data',
57
+ description: 'Retrieves plugin data. Returns formatted response with metadata.',
58
+ handler: async (req, res, { majk, storage }) => {
59
+ const data = await storage.get('data') || [];
60
+ return { data, count: data.length };
61
+ }
62
+ })
92
63
 
93
- function App() {
94
- const { data, loading, error, mutate } = useGetMessage();
64
+ .tool('global', {
65
+ name: 'myTool',
66
+ description: 'Does something useful. Processes input and returns results.',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ param: { type: 'string' }
71
+ },
72
+ required: ['param']
73
+ }
74
+ }, async (input, { logger }) => {
75
+ logger.info(`Tool called with: ${input.param}`);
76
+ return { success: true, result: input.param.toUpperCase() };
77
+ })
95
78
 
96
- return (
97
- <button onClick={() => mutate({ name: 'World' })}>
98
- {loading ? 'Loading...' : data?.message}
99
- </button>
100
- );
101
- }
79
+ .build();
102
80
  ```
103
81
 
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
82
+ ## Core Concepts
127
83
 
128
84
  ### Plugin Definition
129
85
 
130
- Every plugin starts with `definePlugin(id, name, version)`:
86
+ Every plugin starts with `definePlugin(id, name, version)` followed immediately by `.pluginRoot(__dirname)`:
131
87
 
132
88
  ```typescript
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;
89
+ definePlugin('system-explorer', 'System Explorer', '1.0.0')
90
+ .pluginRoot(__dirname) // REQUIRED: Must be first call
141
91
  ```
142
92
 
143
93
  **Rules:**
144
94
  - `id` must be unique and URL-safe (kebab-case recommended)
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
- ```
95
+ - `name` is the display name
96
+ - `version` follows semver
97
+ - **`.pluginRoot(__dirname)` is REQUIRED** - Must be the first call after `definePlugin`
334
98
 
335
- ### Generic Configurable Entities
99
+ ### Screens
336
100
 
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
101
+ #### React Screens
348
102
 
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.
363
-
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
- ---
379
-
380
- ## 🎨 UI Configuration
381
-
382
- ### React Applications
383
-
384
- For React SPAs, configure UI first, then add screens:
103
+ For React SPAs, configure UI:
385
104
 
386
105
  ```typescript
387
106
  .ui({
388
- appDir: 'dist', // Where your built React app is
389
- base: '/', // Base URL for the SPA
390
- history: 'hash' // 'browser' or 'hash' routing
107
+ appDir: 'dist', // Where your built React app is
108
+ base: '/plugin-screens/my-plugin', // Base path (NO trailing slash)
109
+ history: 'hash' // REQUIRED: Use 'hash' for iframe routing
391
110
  })
392
111
 
393
112
  .screenReact({
394
113
  id: 'dashboard',
395
114
  name: 'Dashboard',
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
115
+ description: 'Main dashboard view. Shows metrics and controls.', // 2-3 sentences
116
+ route: '/plugin-screens/my-plugin/dashboard', // Must start with /plugin-screens/{id}/
117
+ reactPath: '/' // Path within your React app
408
118
  })
409
119
  ```
410
120
 
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
- ```
121
+ **The React app receives:**
122
+ - `window.__MAJK_BASE_URL__` - Host base URL
123
+ - `window.__MAJK_IFRAME_BASE__` - Plugin base path
124
+ - `window.__MAJK_PLUGIN_ID__` - Your plugin ID
417
125
 
418
- ### HTML Screens
126
+ #### HTML Screens
419
127
 
420
- For simple static pages:
128
+ For simple HTML pages:
421
129
 
422
130
  ```typescript
423
131
  .screenHtml({
424
132
  id: 'about',
425
133
  name: 'About',
426
- description: 'Information about the plugin. Shows version and features.',
134
+ description: 'Information about the plugin. Shows version and author.',
427
135
  route: '/plugin-screens/my-plugin/about',
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
136
+ html: '<html>...' // OR htmlFile: 'about.html'
442
137
  })
443
138
  ```
444
139
 
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.
140
+ ### API Routes
478
141
 
479
142
  Define REST endpoints:
480
143
 
@@ -483,74 +146,60 @@ Define REST endpoints:
483
146
  method: 'POST',
484
147
  path: '/api/tasks/:id/complete',
485
148
  name: 'Complete Task',
486
- description: 'Marks a task as complete. Updates status and notifies users.',
149
+ description: 'Marks a task as complete. Updates task status and triggers notifications.',
487
150
  handler: async (req, res, { majk, storage, logger }) => {
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
151
+ const { id } = req.params; // Path parameters
152
+ const { note } = req.body; // Request body
153
+ const status = req.query.get('status'); // Query params
491
154
 
492
155
  logger.info(`Completing task ${id}`);
493
156
 
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;
157
+ // Access MAJK APIs
158
+ const todos = await majk.todos.list();
503
159
 
504
- await storage.set(`task:${id}`, task);
160
+ // Use plugin storage
161
+ await storage.set(`task:${id}`, { completed: true });
505
162
 
506
- return { success: true, task };
163
+ return { success: true, taskId: id };
507
164
  }
508
165
  })
509
166
  ```
510
167
 
511
168
  **Available Methods:** `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
512
169
 
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)
521
-
522
- ---
170
+ **Context Provided:**
171
+ - `majk` - Full MAJK API interface
172
+ - `storage` - Plugin-scoped key-value storage
173
+ - `logger` - Scoped logger (debug, info, warn, error)
174
+ - `http` - HTTP configuration (port, baseUrl, secret)
523
175
 
524
- ## 🛠️ Tools
176
+ ### Tools
525
177
 
526
- Tools are functions that AI agents can invoke. They're scoped to different contexts.
178
+ Tools are functions that agents can invoke:
527
179
 
528
180
  ```typescript
529
181
  .tool(
530
182
  'conversation', // Scope: 'global' | 'conversation' | 'teammate' | 'project'
531
183
  {
532
- name: 'analyzeCode',
533
- description: 'Analyzes code for potential issues. Returns findings with severity.',
184
+ name: 'analyzeSentiment',
185
+ description: 'Analyzes text sentiment. Returns positive, negative, or neutral classification.',
534
186
  inputSchema: {
535
187
  type: 'object',
536
188
  properties: {
537
- code: { type: 'string' },
538
- language: { type: 'string' }
189
+ text: { type: 'string' }
539
190
  },
540
- required: ['code', 'language']
191
+ required: ['text']
541
192
  }
542
193
  },
543
194
  async (input, { majk, logger }) => {
544
- logger.info(`Analyzing ${input.language} code`);
195
+ logger.info('Analyzing sentiment');
545
196
 
546
- const issues = analyzeCode(input.code, input.language);
197
+ // Your implementation
198
+ const sentiment = analyzeSentiment(input.text);
547
199
 
548
200
  return {
549
201
  success: true,
550
- data: {
551
- issues,
552
- summary: `Found ${issues.length} issues`
553
- }
202
+ data: { sentiment, confidence: 0.95 }
554
203
  };
555
204
  }
556
205
  )
@@ -558,342 +207,115 @@ Tools are functions that AI agents can invoke. They're scoped to different conte
558
207
 
559
208
  **Tool Scopes:**
560
209
  - `global` - Available everywhere
561
- - `conversation` - Scoped to specific conversations
562
- - `teammate` - Scoped to specific teammates
563
- - `project` - Scoped to specific projects
564
-
565
- **Scoped Metadata:**
210
+ - `conversation` - Scoped to conversations
211
+ - `teammate` - Scoped to teammates
212
+ - `project` - Scoped to projects
566
213
 
567
- For non-global tools, specify scope entity in metadata:
214
+ ### Entities
568
215
 
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
- ---
581
-
582
- ## 📦 Static Entities
583
-
584
- Declare entities your plugin always provides:
585
-
586
- ### MCP Servers
216
+ Declare entities your plugin provides:
587
217
 
588
218
  ```typescript
589
- .mcpServer([
219
+ .entity('teammate', [
590
220
  {
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
- }
221
+ id: 'bot-assistant',
222
+ name: 'Bot Assistant',
223
+ role: 'bot',
224
+ capabilities: ['analysis', 'reporting']
599
225
  }
600
226
  ])
601
- ```
602
227
 
603
- ### Team Members
604
-
605
- ```typescript
606
- .teamMember([
228
+ .entity('mcpServer', [
607
229
  {
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'
230
+ id: 'custom-server',
231
+ name: 'Custom MCP Server',
232
+ transport: { type: 'stdio', command: 'node', args: ['server.js'] }
627
233
  }
628
234
  ])
629
235
  ```
630
236
 
631
237
  **Supported Entity Types:**
632
238
  - `mcpServer` - MCP servers
633
- - `teamMember` - Team members/bots
239
+ - `teammate` - Team members/bots
634
240
  - `conversation` - Conversations
241
+ - `todo` - Tasks
635
242
  - `project` - Projects
636
243
  - `agent` - AI agents
637
244
 
638
- ---
245
+ ### Config Wizard & Settings
639
246
 
640
- ## 🔐 Secret Providers
247
+ #### Config Wizard
641
248
 
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.
643
-
644
- ### Basic Secret Provider
249
+ Show a wizard on first run:
645
250
 
646
251
  ```typescript
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
- };
252
+ .configWizard({
253
+ path: '/setup',
254
+ title: 'Initial Setup',
255
+ width: 600,
256
+ height: 400,
257
+ description: 'Configure plugin settings. Set up API keys and preferences.',
258
+ shouldShow: async (ctx) => {
259
+ const config = await ctx.storage.get('config');
260
+ return !config; // Show if no config exists
725
261
  }
726
262
  })
727
263
  ```
728
264
 
729
- ### Scope Types
730
-
731
- Secrets can be scoped to different contexts:
265
+ #### Settings Screen
732
266
 
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`)
738
-
739
- ### Resolution Context
740
-
741
- When resolving secrets, MAJK provides context about where the secret is being used:
267
+ Ongoing settings management:
742
268
 
743
269
  ```typescript
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
- }
806
- })
807
- ```
808
-
809
- **Example: 1Password CLI**
810
-
811
- ```typescript
812
- import { exec } from 'child_process';
813
- import { promisify } from 'util';
814
-
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
- }
270
+ .settings({
271
+ path: '/settings',
272
+ title: 'Plugin Settings',
273
+ description: 'Manage plugin configuration. Adjust behavior and display options.'
838
274
  })
839
275
  ```
840
276
 
841
- ---
842
-
843
- ## 🔄 Lifecycle Hooks
277
+ ### Lifecycle Hooks
844
278
 
845
- ### onReady
279
+ #### onReady
846
280
 
847
- Called after the plugin's HTTP server starts, before `onLoad` completes:
281
+ Called after server starts, before `onLoad` completes:
848
282
 
849
283
  ```typescript
850
284
  .onReady(async (ctx, cleanup) => {
851
- ctx.logger.info('Plugin initializing...');
852
-
853
285
  // Subscribe to events
854
- const conversationSub = ctx.majk.eventBus.conversations().subscribe((event) => {
855
- ctx.logger.info(`Conversation ${event.type}: ${event.entity?.id}`);
286
+ const sub = ctx.majk.eventBus.conversations().subscribe((event) => {
287
+ ctx.logger.info(`Conversation event: ${event.type}`);
856
288
  });
857
- cleanup(() => conversationSub.unsubscribe());
289
+ cleanup(() => sub.unsubscribe());
858
290
 
859
- // Set up periodic tasks
291
+ // Set up timers
860
292
  const timer = setInterval(() => {
861
- ctx.logger.debug('Health check');
293
+ ctx.logger.debug('Periodic check');
862
294
  }, 60000);
863
295
  cleanup(() => clearInterval(timer));
864
296
 
865
- // Load data
866
- const data = await ctx.storage.get('plugin-data') || {};
867
- ctx.logger.info(`Loaded ${Object.keys(data).length} items`);
297
+ // Any other setup
298
+ await loadData(ctx.storage);
868
299
  })
869
300
  ```
870
301
 
871
- **Cleanup Management:**
872
- All cleanup functions registered via `cleanup()` are automatically called when the plugin unloads.
302
+ **Cleanup Registration:**
303
+ All cleanup functions are automatically called on `onUnload()`.
873
304
 
874
- ### Health Checks
305
+ #### Health Checks
875
306
 
876
307
  Define custom health monitoring:
877
308
 
878
309
  ```typescript
879
310
  .health(async ({ majk, storage, logger }) => {
880
311
  try {
881
- // Check MAJK API
312
+ // Check dependencies
882
313
  await majk.conversations.list();
883
-
884
- // Check storage
885
- await storage.get('health-test');
886
-
887
- // Check external dependencies
888
- const externalOk = await checkExternalAPI();
314
+ await storage.get('health-check');
889
315
 
890
316
  return {
891
317
  healthy: true,
892
- details: {
893
- api: 'ok',
894
- storage: 'ok',
895
- external: externalOk ? 'ok' : 'degraded'
896
- }
318
+ details: { api: 'ok', storage: 'ok' }
897
319
  };
898
320
  } catch (error) {
899
321
  logger.error(`Health check failed: ${error.message}`);
@@ -905,209 +327,7 @@ Define custom health monitoring:
905
327
  })
906
328
  ```
907
329
 
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
330
+ ## API Reference
1111
331
 
1112
332
  ### PluginContext
1113
333
 
@@ -1115,71 +335,33 @@ Provided to all handlers and hooks:
1115
335
 
1116
336
  ```typescript
1117
337
  interface PluginContext {
1118
- pluginId: string; // Your plugin ID
1119
- pluginRoot: string; // Plugin directory path
1120
- dataDir: string; // Plugin data directory
338
+ pluginId: string; // Your plugin ID
339
+ pluginRoot: string; // Plugin directory path
340
+ dataDir: string; // Plugin data directory
1121
341
 
1122
342
  app: {
1123
- version: string; // MAJK version
1124
- name: string; // App name
1125
- appDataDir: string; // App data directory
343
+ version: string; // MAJK version
344
+ name: string; // App name
345
+ appDataDir: string; // App data directory
1126
346
  };
1127
347
 
1128
348
  http: {
1129
- port: number; // Assigned HTTP port
1130
- secret: string; // Security secret
1131
- baseUrl: string; // Base URL for iframe
349
+ port: number; // Assigned HTTP port
350
+ secret: string; // Security secret
351
+ baseUrl: string; // Base URL for iframe
1132
352
  };
1133
353
 
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[]>;
354
+ majk: MajkInterface; // Full MAJK API
355
+ storage: PluginStorage; // Key-value storage
356
+ logger: PluginLogger; // Scoped logger
357
+ timers?: ScopedTimers; // Managed timers
358
+ ipc?: ScopedIpcRegistry; // Electron IPC
1153
359
  }
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 });
1178
360
  ```
1179
361
 
1180
362
  ### MajkInterface
1181
363
 
1182
- The main MAJK API (subset shown):
364
+ The main MAJK API:
1183
365
 
1184
366
  ```typescript
1185
367
  interface MajkInterface {
@@ -1197,464 +379,266 @@ interface MajkInterface {
1197
379
  }
1198
380
  ```
1199
381
 
1200
- ### MAJK API Deep Dive
382
+ ### PluginStorage
1201
383
 
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.
384
+ Simple key-value storage scoped to your plugin:
1203
385
 
1204
- #### Conversations API
386
+ ```typescript
387
+ interface PluginStorage {
388
+ get<T>(key: string): Promise<T | undefined>;
389
+ set<T>(key: string, value: T): Promise<void>;
390
+ delete(key: string): Promise<void>;
391
+ clear(): Promise<void>;
392
+ keys(): Promise<string[]>;
393
+ }
394
+ ```
1205
395
 
1206
- Manage conversations and messages:
396
+ **Example:**
1207
397
 
1208
398
  ```typescript
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
- })
399
+ // Save data
400
+ await storage.set('user-preferences', {
401
+ theme: 'dark',
402
+ notifications: true
403
+ });
1223
404
 
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);
405
+ // Load data
406
+ const prefs = await storage.get<Preferences>('user-preferences');
1230
407
 
1231
- if (!conversation) {
1232
- return { success: false, error: 'Conversation not found' };
1233
- }
408
+ // List all keys
409
+ const keys = await storage.keys();
1234
410
 
1235
- // Fetch messages
1236
- const messages = await conversation.getMessages();
411
+ // Delete specific key
412
+ await storage.delete('old-data');
1237
413
 
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
- })
414
+ // Clear everything
415
+ await storage.clear();
1249
416
  ```
1250
417
 
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
418
+ ### EventBus
1256
419
 
1257
- #### Teammates API
1258
-
1259
- Manage AI teammates:
420
+ Subscribe to system events:
1260
421
 
1261
422
  ```typescript
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
- })
423
+ // Listen to conversation events
424
+ const sub = majk.eventBus.conversations().subscribe((event) => {
425
+ console.log(`Event: ${event.type}`, event.entity);
426
+ });
1278
427
 
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);
428
+ // Unsubscribe
429
+ sub.unsubscribe();
1284
430
 
1285
- if (!teammate) {
1286
- return { success: false, error: 'Teammate not found' };
1287
- }
431
+ // Specific event types
432
+ majk.eventBus.conversations().created().subscribe(...);
433
+ majk.eventBus.conversations().updated().subscribe(...);
434
+ majk.eventBus.conversations().deleted().subscribe(...);
1288
435
 
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
- })
436
+ // Custom channels
437
+ majk.eventBus.channel('my-events').subscribe(...);
1302
438
  ```
1303
439
 
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
440
+ ## Validation & Error Handling
1310
441
 
1311
- #### Authentication API
442
+ ### Build-Time Validation
1312
443
 
1313
- Access user authentication information:
444
+ The kit validates at build time:
1314
445
 
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();
446
+ ✅ **Route Prefixes** - Screen routes must match plugin ID
447
+ **File Existence** - React dist and HTML files must exist
448
+ ✅ **Uniqueness** - No duplicate routes, tools, or API endpoints
449
+ **Dependencies** - UI must be configured for React screens
450
+ **Descriptions** - Must be 2-3 sentences ending with period
1322
451
 
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
- })
452
+ **Example Error:**
1333
453
 
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
- })
454
+ ```
455
+ Plugin Build Failed: React screen route must start with "/plugin-screens/my-plugin/"
456
+ 💡 Suggestion: Change route from "/screens/dashboard" to "/plugin-screens/my-plugin/dashboard"
457
+ 📋 Context: {
458
+ "screen": "dashboard",
459
+ "route": "/screens/dashboard"
460
+ }
1349
461
  ```
1350
462
 
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
463
+ ### Runtime Error Handling
1357
464
 
1358
- Subscribe to real-time system events:
465
+ All API route errors are automatically caught and logged:
1359
466
 
1360
467
  ```typescript
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:
468
+ .apiRoute({
469
+ method: 'POST',
470
+ path: '/api/process',
471
+ name: 'Process Data',
472
+ description: 'Processes input data. Validates and transforms the payload.',
473
+ handler: async (req, res, { logger }) => {
474
+ // Errors are automatically caught and returned as 500 responses
475
+ throw new Error('Processing failed');
476
+
477
+ // Returns:
1368
478
  // {
1369
- // type: 'created' | 'updated' | 'deleted',
1370
- // entityType: 'conversation' | 'teammate' | 'todo' | etc,
1371
- // entity: { id, ...otherFields },
1372
- // metadata: { ... }
479
+ // "error": "Processing failed",
480
+ // "route": "Process Data",
481
+ // "path": "/api/process"
1373
482
  // }
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());
1390
- })
1391
-
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
- })
1410
- ```
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
483
  }
1484
484
  })
1485
485
  ```
1486
486
 
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
- ---
1501
-
1502
- ## ✅ Best Practices
1503
-
1504
- ### 1. Always Use `.pluginRoot(__dirname)`
1505
-
1506
- ```typescript
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
- // ...
487
+ Logs show:
488
+ ```
489
+ POST /api/process - Error: Processing failed
490
+ [stack trace]
1515
491
  ```
1516
492
 
1517
- ### 2. Use Storage for State
493
+ ## Best Practices
494
+
495
+ ### 1. Use Storage for State
1518
496
 
1519
497
  ```typescript
1520
- // ❌ Don't use in-memory state (lost on reload)
498
+ // ❌ Don't use in-memory state
1521
499
  let cache = {};
1522
500
 
1523
- // ✅ Use storage (persisted)
501
+ // ✅ Use storage
1524
502
  await ctx.storage.set('cache', data);
1525
503
  ```
1526
504
 
1527
- ### 3. Register Cleanups
505
+ ### 2. Register Cleanups
1528
506
 
1529
507
  ```typescript
1530
508
  .onReady(async (ctx, cleanup) => {
1531
- // ❌ Forgot to cleanup
1532
- const timer = setInterval(() => { /* ... */ }, 1000);
509
+ // ❌ Don't forget to cleanup
510
+ const timer = setInterval(...);
1533
511
 
1534
- // ✅ Registered cleanup
1535
- const timer = setInterval(() => { /* ... */ }, 1000);
512
+ // ✅ Register cleanup
513
+ const timer = setInterval(...);
1536
514
  cleanup(() => clearInterval(timer));
1537
515
 
1538
516
  // ✅ Event subscriptions
1539
- const sub = ctx.majk.eventBus.conversations().subscribe(handler);
517
+ const sub = ctx.majk.eventBus.conversations().subscribe(...);
1540
518
  cleanup(() => sub.unsubscribe());
1541
519
  })
1542
520
  ```
1543
521
 
1544
- ### 4. Write Good Descriptions
522
+ ### 3. Validate Input
1545
523
 
1546
524
  ```typescript
1547
- // ❌ Too short
1548
- description: 'Dashboard screen'
525
+ .apiRoute({
526
+ method: 'POST',
527
+ path: '/api/create',
528
+ name: 'Create Item',
529
+ description: 'Creates a new item. Validates input before processing.',
530
+ handler: async (req, res) => {
531
+ // ✅ Validate input
532
+ if (!req.body?.name) {
533
+ res.status(400).json({ error: 'Name is required' });
534
+ return;
535
+ }
1549
536
 
1550
- // ✅ 2-3 sentences, clear purpose
1551
- description: 'Main dashboard view. Shows key metrics and recent activity.'
537
+ // Process...
538
+ }
539
+ })
1552
540
  ```
1553
541
 
1554
- ### 5. Handle Errors Gracefully
542
+ ### 4. Use Structured Logging
1555
543
 
1556
544
  ```typescript
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
- })
545
+ // Basic logging
546
+ logger.info('User action');
547
+
548
+ // Structured logging
549
+ logger.info('User action', { userId, action: 'create', resourceId });
1574
550
  ```
1575
551
 
1576
- ### 6. Validate Input
552
+ ### 5. Handle Errors Gracefully
1577
553
 
1578
554
  ```typescript
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...
555
+ .tool('global', spec, async (input, { logger }) => {
556
+ try {
557
+ const result = await processData(input);
558
+ return { success: true, data: result };
559
+ } catch (error) {
560
+ logger.error(`Tool failed: ${error.message}`);
561
+ return {
562
+ success: false,
563
+ error: error.message,
564
+ code: 'PROCESSING_ERROR'
565
+ };
1590
566
  }
1591
567
  })
1592
568
  ```
1593
569
 
1594
- ### 7. Use Configurable Entities for User Customization
570
+ ## Examples
1595
571
 
1596
- ```typescript
1597
- // Let users configure teammates
1598
- .configWizard({ schema: TeammateConfigSchema, ... })
1599
- .configurableTeamMember((config) => [buildTeammate(config)])
572
+ See `example.ts` for a comprehensive example showing:
573
+ - React and HTML screens
574
+ - API routes with parameters
575
+ - Tools in different scopes
576
+ - Entity declarations
577
+ - Config wizard
578
+ - Event subscriptions
579
+ - Storage usage
580
+ - Health checks
1600
581
 
1601
- // ❌ Hard-coded teammates (less flexible)
1602
- .teamMember([{ id: 'assistant', name: 'Assistant', ... }])
1603
- ```
582
+ ## TypeScript
1604
583
 
1605
- ---
584
+ Full TypeScript support with:
1606
585
 
1607
- ## 🐛 Troubleshooting
586
+ ```typescript
587
+ import {
588
+ definePlugin,
589
+ FluentBuilder,
590
+ PluginContext,
591
+ RequestLike,
592
+ ResponseLike
593
+ } from '@majk/plugin-kit';
1608
594
 
1609
- ### Build Errors
595
+ // Type-safe plugin ID
596
+ const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0');
597
+ // ^ Enforces route prefixes
1610
598
 
1611
- **"React app not built"**
1612
- ```
1613
- ❌ React app not built: /path/to/ui/dist/index.html does not exist
1614
- 💡 Run "npm run build" in your UI directory
599
+ // Type-safe routes
600
+ .screenReact({
601
+ route: '/plugin-screens/my-plugin/dashboard'
602
+ // ^^^^^^^^^ Must match plugin ID
603
+ })
1615
604
  ```
1616
- Fix: Build your React app before building the plugin.
1617
605
 
1618
- **"Configurable entities require configWizard with schema"**
1619
- ```
1620
- You used .configurableTeamMember() but did not call .configWizard()
1621
- 💡 Add .configWizard({ schema: YourSchema, ... })
1622
- ```
606
+ ## Troubleshooting
607
+
608
+ ### "React app not built"
1623
609
 
1624
- **"Screen route must start with /plugin-screens/{id}/"**
1625
610
  ```
1626
- Route "/screens/dashboard" doesn't match pattern
1627
- 💡 Change to "/plugin-screens/my-plugin/dashboard"
611
+ Plugin Build Failed: React app not built: /path/to/ui/dist/index.html does not exist
612
+ 💡 Suggestion: Run "npm run build" in your UI directory to build the React app
1628
613
  ```
1629
614
 
1630
- ### Runtime Errors
615
+ **Fix:** Build your React app before building the plugin.
1631
616
 
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+
617
+ ### "Duplicate API route"
1635
618
 
1636
- **Functions not appearing in generated client**
1637
- - Ensure you call `.transport(new HttpTransport())`
1638
- - Run `npx plugin-kit generate` after adding functions
619
+ ```
620
+ Plugin Build Failed: Duplicate API route: POST /api/data
621
+ ```
1639
622
 
1640
- ### Config Not Working
623
+ **Fix:** Each route (method + path) must be unique.
1641
624
 
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
625
+ ### "Description must be 2-3 sentences"
1647
626
 
1648
- ---
627
+ ```
628
+ ❌ Plugin Build Failed: Description for "My Screen" must be 2-3 sentences, found 1 sentences
629
+ 💡 Suggestion: Rewrite the description to have 2-3 clear sentences.
630
+ ```
1649
631
 
1650
- ## 📝 License
632
+ **Fix:** Write 2-3 complete sentences ending with periods.
1651
633
 
1652
- MIT
634
+ ### "Tool names must be unique"
1653
635
 
1654
- ## 🤝 Contributing
636
+ ```
637
+ ❌ Plugin Build Failed: Duplicate tool name: "analyze"
638
+ ```
1655
639
 
1656
- Issues and pull requests welcome at https://github.com/gaiin-platform/majk-plugins
640
+ **Fix:** Each tool name must be unique within the plugin.
1657
641
 
1658
- ---
642
+ ## License
1659
643
 
1660
- **Built with ❤️ by the MAJK team**
644
+ MIT