@majkapp/plugin-kit 1.2.0 → 1.2.1

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