@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 +334 -1358
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/majk-interface-types.d.ts +979 -0
- package/dist/majk-interface-types.d.ts.map +1 -0
- package/dist/majk-interface-types.js +8 -0
- package/dist/plugin-kit.d.ts +1 -3
- package/dist/plugin-kit.d.ts.map +1 -1
- package/dist/plugin-kit.js +2 -62
- package/dist/types.d.ts +2 -43
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,480 +1,135 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @majk/plugin-kit
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Fluent builder framework for creating robust MAJK plugins**
|
|
4
4
|
|
|
5
|
-
Build production-ready MAJK plugins
|
|
5
|
+
Build type-safe, production-ready MAJK plugins with excellent developer experience, comprehensive validation, and clear error messages.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
## Features
|
|
9
8
|
|
|
10
|
-
|
|
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
|
-
|
|
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 @
|
|
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
|
|
37
|
-
|
|
38
|
-
export = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
39
|
-
.pluginRoot(__dirname)
|
|
30
|
+
import { definePlugin } from '@majk/plugin-kit';
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
.
|
|
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
|
-
|
|
63
|
-
|
|
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: '
|
|
72
|
-
name: '
|
|
73
|
-
description: 'Main plugin
|
|
74
|
-
route: '/plugin-screens/my-plugin/
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
- `version` follows
|
|
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
|
-
###
|
|
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
|
-
|
|
93
|
+
#### React Screens
|
|
381
94
|
|
|
382
|
-
|
|
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',
|
|
389
|
-
base: '/',
|
|
390
|
-
history: '
|
|
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
|
|
397
|
-
route: '/plugin-screens/my-plugin/dashboard', // Must start with /plugin-screens/{
|
|
398
|
-
reactPath: '/'
|
|
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
|
|
412
|
-
|
|
413
|
-
window.
|
|
414
|
-
window.
|
|
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
|
-
|
|
118
|
+
#### HTML Screens
|
|
419
119
|
|
|
420
|
-
For simple
|
|
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
|
|
126
|
+
description: 'Information about the plugin. Shows version and author.',
|
|
427
127
|
route: '/plugin-screens/my-plugin/about',
|
|
428
|
-
|
|
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
|
-
###
|
|
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
|
|
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;
|
|
489
|
-
const { note } = req.body;
|
|
490
|
-
const status = req.query.get('status');
|
|
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
|
-
|
|
495
|
-
|
|
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
|
-
|
|
152
|
+
// Use plugin storage
|
|
153
|
+
await storage.set(`task:${id}`, { completed: true });
|
|
505
154
|
|
|
506
|
-
return { success: true,
|
|
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
|
-
**
|
|
514
|
-
- `
|
|
515
|
-
- `
|
|
516
|
-
- `
|
|
517
|
-
- `
|
|
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
|
-
|
|
168
|
+
### Tools
|
|
525
169
|
|
|
526
|
-
Tools are functions that
|
|
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: '
|
|
533
|
-
description: 'Analyzes
|
|
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
|
-
|
|
538
|
-
language: { type: 'string' }
|
|
181
|
+
text: { type: 'string' }
|
|
539
182
|
},
|
|
540
|
-
required: ['
|
|
183
|
+
required: ['text']
|
|
541
184
|
}
|
|
542
185
|
},
|
|
543
186
|
async (input, { majk, logger }) => {
|
|
544
|
-
logger.info(
|
|
187
|
+
logger.info('Analyzing sentiment');
|
|
545
188
|
|
|
546
|
-
|
|
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
|
|
562
|
-
- `teammate` - Scoped to
|
|
563
|
-
- `project` - Scoped to
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
Declare entities your plugin always provides:
|
|
585
|
-
|
|
586
|
-
### MCP Servers
|
|
208
|
+
Declare entities your plugin provides:
|
|
587
209
|
|
|
588
210
|
```typescript
|
|
589
|
-
.
|
|
211
|
+
.entity('teammate', [
|
|
590
212
|
{
|
|
591
|
-
id: '
|
|
592
|
-
name: '
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
606
|
-
.teamMember([
|
|
220
|
+
.entity('mcpServer', [
|
|
607
221
|
{
|
|
608
|
-
id: '
|
|
609
|
-
name: '
|
|
610
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
239
|
+
#### Config Wizard
|
|
643
240
|
|
|
644
|
-
|
|
241
|
+
Show a wizard on first run:
|
|
645
242
|
|
|
646
243
|
```typescript
|
|
647
|
-
.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
271
|
+
#### onReady
|
|
846
272
|
|
|
847
|
-
Called after
|
|
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
|
|
855
|
-
ctx.logger.info(`Conversation
|
|
278
|
+
const sub = ctx.majk.eventBus.conversations().subscribe((event) => {
|
|
279
|
+
ctx.logger.info(`Conversation event: ${event.type}`);
|
|
856
280
|
});
|
|
857
|
-
cleanup(() =>
|
|
281
|
+
cleanup(() => sub.unsubscribe());
|
|
858
282
|
|
|
859
|
-
// Set up
|
|
283
|
+
// Set up timers
|
|
860
284
|
const timer = setInterval(() => {
|
|
861
|
-
ctx.logger.debug('
|
|
285
|
+
ctx.logger.debug('Periodic check');
|
|
862
286
|
}, 60000);
|
|
863
287
|
cleanup(() => clearInterval(timer));
|
|
864
288
|
|
|
865
|
-
//
|
|
866
|
-
|
|
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
|
|
872
|
-
All cleanup functions
|
|
294
|
+
**Cleanup Registration:**
|
|
295
|
+
All cleanup functions are automatically called on `onUnload()`.
|
|
873
296
|
|
|
874
|
-
|
|
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
|
|
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;
|
|
1119
|
-
pluginRoot: string;
|
|
1120
|
-
dataDir: string;
|
|
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;
|
|
1124
|
-
name: string;
|
|
1125
|
-
appDataDir: string;
|
|
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;
|
|
1130
|
-
secret: string;
|
|
1131
|
-
baseUrl: string;
|
|
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;
|
|
1135
|
-
storage: PluginStorage;
|
|
1136
|
-
logger: PluginLogger;
|
|
1137
|
-
timers?: ScopedTimers;
|
|
1138
|
-
ipc?: ScopedIpcRegistry;
|
|
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
|
|
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
|
-
###
|
|
374
|
+
### PluginStorage
|
|
1201
375
|
|
|
1202
|
-
|
|
376
|
+
Simple key-value storage scoped to your plugin:
|
|
1203
377
|
|
|
1204
|
-
|
|
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
|
-
|
|
388
|
+
**Example:**
|
|
1207
389
|
|
|
1208
390
|
```typescript
|
|
1209
|
-
//
|
|
1210
|
-
.
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
-
//
|
|
1225
|
-
.
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
400
|
+
// List all keys
|
|
401
|
+
const keys = await storage.keys();
|
|
1234
402
|
|
|
1235
|
-
|
|
1236
|
-
|
|
403
|
+
// Delete specific key
|
|
404
|
+
await storage.delete('old-data');
|
|
1237
405
|
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
+
Subscribe to system events:
|
|
1260
413
|
|
|
1261
414
|
```typescript
|
|
1262
|
-
//
|
|
1263
|
-
.
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
//
|
|
1280
|
-
.
|
|
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
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
+
### Build-Time Validation
|
|
1312
435
|
|
|
1313
|
-
|
|
436
|
+
The kit validates at build time:
|
|
1314
437
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
+
All API route errors are automatically caught and logged:
|
|
1359
458
|
|
|
1360
459
|
```typescript
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
//
|
|
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
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
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
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
-
|
|
485
|
+
## Best Practices
|
|
486
|
+
|
|
487
|
+
### 1. Use Storage for State
|
|
1518
488
|
|
|
1519
489
|
```typescript
|
|
1520
|
-
// ❌ Don't use in-memory state
|
|
490
|
+
// ❌ Don't use in-memory state
|
|
1521
491
|
let cache = {};
|
|
1522
492
|
|
|
1523
|
-
// ✅ Use storage
|
|
493
|
+
// ✅ Use storage
|
|
1524
494
|
await ctx.storage.set('cache', data);
|
|
1525
495
|
```
|
|
1526
496
|
|
|
1527
|
-
###
|
|
497
|
+
### 2. Register Cleanups
|
|
1528
498
|
|
|
1529
499
|
```typescript
|
|
1530
500
|
.onReady(async (ctx, cleanup) => {
|
|
1531
|
-
// ❌
|
|
1532
|
-
const timer = setInterval(
|
|
501
|
+
// ❌ Don't forget to cleanup
|
|
502
|
+
const timer = setInterval(...);
|
|
1533
503
|
|
|
1534
|
-
// ✅
|
|
1535
|
-
const timer = setInterval(
|
|
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(
|
|
509
|
+
const sub = ctx.majk.eventBus.conversations().subscribe(...);
|
|
1540
510
|
cleanup(() => sub.unsubscribe());
|
|
1541
511
|
})
|
|
1542
512
|
```
|
|
1543
513
|
|
|
1544
|
-
###
|
|
514
|
+
### 3. Validate Input
|
|
1545
515
|
|
|
1546
516
|
```typescript
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
-
//
|
|
1551
|
-
|
|
529
|
+
// Process...
|
|
530
|
+
}
|
|
531
|
+
})
|
|
1552
532
|
```
|
|
1553
533
|
|
|
1554
|
-
###
|
|
534
|
+
### 4. Use Structured Logging
|
|
1555
535
|
|
|
1556
536
|
```typescript
|
|
1557
|
-
//
|
|
1558
|
-
.
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
###
|
|
544
|
+
### 5. Handle Errors Gracefully
|
|
1577
545
|
|
|
1578
546
|
```typescript
|
|
1579
|
-
.
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
562
|
+
## Examples
|
|
1595
563
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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
|
-
|
|
1602
|
-
.teamMember([{ id: 'assistant', name: 'Assistant', ... }])
|
|
1603
|
-
```
|
|
574
|
+
## TypeScript
|
|
1604
575
|
|
|
1605
|
-
|
|
576
|
+
Full TypeScript support with:
|
|
1606
577
|
|
|
1607
|
-
|
|
578
|
+
```typescript
|
|
579
|
+
import {
|
|
580
|
+
definePlugin,
|
|
581
|
+
FluentBuilder,
|
|
582
|
+
PluginContext,
|
|
583
|
+
RequestLike,
|
|
584
|
+
ResponseLike
|
|
585
|
+
} from '@majk/plugin-kit';
|
|
1608
586
|
|
|
1609
|
-
|
|
587
|
+
// Type-safe plugin ID
|
|
588
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0');
|
|
589
|
+
// ^ Enforces route prefixes
|
|
1610
590
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
❌
|
|
1627
|
-
💡
|
|
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
|
-
|
|
607
|
+
**Fix:** Build your React app before building the plugin.
|
|
1631
608
|
|
|
1632
|
-
|
|
1633
|
-
- Ensure `getCapabilities()` checks if `context` is available
|
|
1634
|
-
- This is handled automatically in v1.1.0+
|
|
609
|
+
### "Duplicate API route"
|
|
1635
610
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
611
|
+
```
|
|
612
|
+
❌ Plugin Build Failed: Duplicate API route: POST /api/data
|
|
613
|
+
```
|
|
1639
614
|
|
|
1640
|
-
|
|
615
|
+
**Fix:** Each route (method + path) must be unique.
|
|
1641
616
|
|
|
1642
|
-
|
|
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
|
-
|
|
624
|
+
**Fix:** Write 2-3 complete sentences ending with periods.
|
|
1651
625
|
|
|
1652
|
-
|
|
626
|
+
### "Tool names must be unique"
|
|
1653
627
|
|
|
1654
|
-
|
|
628
|
+
```
|
|
629
|
+
❌ Plugin Build Failed: Duplicate tool name: "analyze"
|
|
630
|
+
```
|
|
1655
631
|
|
|
1656
|
-
|
|
632
|
+
**Fix:** Each tool name must be unique within the plugin.
|
|
1657
633
|
|
|
1658
|
-
|
|
634
|
+
## License
|
|
1659
635
|
|
|
1660
|
-
|
|
636
|
+
MIT
|