@majkapp/plugin-kit 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +340 -1356
- 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/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,480 +1,143 @@
|
|
|
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
|
+
.pluginRoot(__dirname) // REQUIRED: Must be first call
|
|
61
34
|
|
|
62
|
-
// Configure React UI
|
|
63
35
|
.ui({
|
|
64
36
|
appDir: 'dist',
|
|
65
|
-
base: '/',
|
|
37
|
+
base: '/plugin-screens/my-plugin', // NO trailing slash
|
|
66
38
|
history: 'hash'
|
|
67
39
|
})
|
|
68
40
|
|
|
69
|
-
|
|
41
|
+
.topbar('/plugin-screens/my-plugin/dashboard', {
|
|
42
|
+
icon: '🚀'
|
|
43
|
+
})
|
|
44
|
+
|
|
70
45
|
.screenReact({
|
|
71
|
-
id: '
|
|
72
|
-
name: '
|
|
73
|
-
description: 'Main plugin
|
|
74
|
-
route: '/plugin-screens/my-plugin/
|
|
46
|
+
id: 'dashboard',
|
|
47
|
+
name: 'Dashboard',
|
|
48
|
+
description: 'Main dashboard for my plugin. Shows key metrics and actions.',
|
|
49
|
+
route: '/plugin-screens/my-plugin/dashboard',
|
|
75
50
|
reactPath: '/'
|
|
76
51
|
})
|
|
77
52
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Use in React:
|
|
90
|
-
```tsx
|
|
91
|
-
import { useGetMessage } from './generated';
|
|
53
|
+
.apiRoute({
|
|
54
|
+
method: 'GET',
|
|
55
|
+
path: '/api/data',
|
|
56
|
+
name: 'Get Data',
|
|
57
|
+
description: 'Retrieves plugin data. Returns formatted response with metadata.',
|
|
58
|
+
handler: async (req, res, { majk, storage }) => {
|
|
59
|
+
const data = await storage.get('data') || [];
|
|
60
|
+
return { data, count: data.length };
|
|
61
|
+
}
|
|
62
|
+
})
|
|
92
63
|
|
|
93
|
-
|
|
94
|
-
|
|
64
|
+
.tool('global', {
|
|
65
|
+
name: 'myTool',
|
|
66
|
+
description: 'Does something useful. Processes input and returns results.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
param: { type: 'string' }
|
|
71
|
+
},
|
|
72
|
+
required: ['param']
|
|
73
|
+
}
|
|
74
|
+
}, async (input, { logger }) => {
|
|
75
|
+
logger.info(`Tool called with: ${input.param}`);
|
|
76
|
+
return { success: true, result: input.param.toUpperCase() };
|
|
77
|
+
})
|
|
95
78
|
|
|
96
|
-
|
|
97
|
-
<button onClick={() => mutate({ name: 'World' })}>
|
|
98
|
-
{loading ? 'Loading...' : data?.message}
|
|
99
|
-
</button>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
79
|
+
.build();
|
|
102
80
|
```
|
|
103
81
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
## 📚 Table of Contents
|
|
107
|
-
|
|
108
|
-
- [Core Concepts](#-core-concepts)
|
|
109
|
-
- [Function-First Architecture](#-function-first-architecture)
|
|
110
|
-
- [Configurable Entities](#-configurable-entities)
|
|
111
|
-
- [UI Configuration](#-ui-configuration)
|
|
112
|
-
- [API Routes (Legacy)](#-api-routes-legacy)
|
|
113
|
-
- [Tools](#-tools)
|
|
114
|
-
- [Static Entities](#-static-entities)
|
|
115
|
-
- [Secret Providers](#-secret-providers)
|
|
116
|
-
- [Lifecycle Hooks](#-lifecycle-hooks)
|
|
117
|
-
- [Client Generation](#-client-generation)
|
|
118
|
-
- [Complete Example](#-complete-example)
|
|
119
|
-
- [API Reference](#-api-reference)
|
|
120
|
-
- [MAJK API Deep Dive](#-majk-api-deep-dive)
|
|
121
|
-
- [Best Practices](#-best-practices)
|
|
122
|
-
- [Troubleshooting](#-troubleshooting)
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## 🎯 Core Concepts
|
|
82
|
+
## Core Concepts
|
|
127
83
|
|
|
128
84
|
### Plugin Definition
|
|
129
85
|
|
|
130
|
-
Every plugin starts with `definePlugin(id, name, version)`:
|
|
86
|
+
Every plugin starts with `definePlugin(id, name, version)` followed immediately by `.pluginRoot(__dirname)`:
|
|
131
87
|
|
|
132
88
|
```typescript
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const plugin = definePlugin('task-manager', 'Task Manager', '1.0.0')
|
|
136
|
-
.pluginRoot(__dirname) // Important: tells plugin-kit where your files are
|
|
137
|
-
// ... builder methods
|
|
138
|
-
.build();
|
|
139
|
-
|
|
140
|
-
export = plugin;
|
|
89
|
+
definePlugin('system-explorer', 'System Explorer', '1.0.0')
|
|
90
|
+
.pluginRoot(__dirname) // REQUIRED: Must be first call
|
|
141
91
|
```
|
|
142
92
|
|
|
143
93
|
**Rules:**
|
|
144
94
|
- `id` must be unique and URL-safe (kebab-case recommended)
|
|
145
|
-
- `name` is the
|
|
146
|
-
- `version` follows
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
## 🔥 Function-First Architecture
|
|
152
|
-
|
|
153
|
-
The modern way to build MAJK plugins. Define functions with JSON schemas, and plugin-kit automatically:
|
|
154
|
-
- Generates HTTP endpoints
|
|
155
|
-
- Creates TypeScript clients with React hooks
|
|
156
|
-
- Validates input/output at runtime
|
|
157
|
-
- Handles errors gracefully
|
|
158
|
-
|
|
159
|
-
### Defining Functions
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
.function('createTask', {
|
|
163
|
-
description: 'Creates a new task with the specified details.',
|
|
164
|
-
input: {
|
|
165
|
-
type: 'object',
|
|
166
|
-
properties: {
|
|
167
|
-
title: { type: 'string' },
|
|
168
|
-
description: { type: 'string' },
|
|
169
|
-
priority: { type: 'string', enum: ['low', 'medium', 'high'] },
|
|
170
|
-
dueDate: { type: 'string', format: 'date-time' }
|
|
171
|
-
},
|
|
172
|
-
required: ['title']
|
|
173
|
-
},
|
|
174
|
-
output: {
|
|
175
|
-
type: 'object',
|
|
176
|
-
properties: {
|
|
177
|
-
id: { type: 'string' },
|
|
178
|
-
title: { type: 'string' },
|
|
179
|
-
createdAt: { type: 'string' }
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
handler: async (input, ctx) => {
|
|
183
|
-
const task = {
|
|
184
|
-
id: generateId(),
|
|
185
|
-
title: input.title,
|
|
186
|
-
description: input.description || '',
|
|
187
|
-
priority: input.priority || 'medium',
|
|
188
|
-
dueDate: input.dueDate,
|
|
189
|
-
createdAt: new Date().toISOString()
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
await ctx.storage.set(`task:${task.id}`, task);
|
|
193
|
-
ctx.logger.info(`Created task: ${task.id}`);
|
|
194
|
-
|
|
195
|
-
return task;
|
|
196
|
-
},
|
|
197
|
-
tags: ['tasks']
|
|
198
|
-
})
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**Handler Context (`ctx`):**
|
|
202
|
-
- `storage` - Plugin-scoped key-value storage
|
|
203
|
-
- `logger` - Scoped logger (debug, info, warn, error)
|
|
204
|
-
- `majk` - Full MAJK API interface
|
|
205
|
-
|
|
206
|
-
### Subscriptions (Async Iterators)
|
|
207
|
-
|
|
208
|
-
For streaming or long-running operations:
|
|
209
|
-
|
|
210
|
-
```typescript
|
|
211
|
-
.subscription('watchTasks', {
|
|
212
|
-
description: 'Watches for task changes in real-time.',
|
|
213
|
-
input: {
|
|
214
|
-
type: 'object',
|
|
215
|
-
properties: {
|
|
216
|
-
filter: { type: 'string' }
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
output: {
|
|
220
|
-
type: 'object',
|
|
221
|
-
properties: {
|
|
222
|
-
taskId: { type: 'string' },
|
|
223
|
-
event: { type: 'string' },
|
|
224
|
-
timestamp: { type: 'string' }
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
handler: async function* (input, ctx) {
|
|
228
|
-
const subscription = ctx.majk.eventBus.channel('tasks').subscribe();
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
for await (const event of subscription) {
|
|
232
|
-
yield {
|
|
233
|
-
taskId: event.entity.id,
|
|
234
|
-
event: event.type,
|
|
235
|
-
timestamp: new Date().toISOString()
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
} finally {
|
|
239
|
-
subscription.unsubscribe();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
})
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
---
|
|
246
|
-
|
|
247
|
-
## 🔧 Configurable Entities
|
|
248
|
-
|
|
249
|
-
**New in v1.1.0** - Register entities (teammates, MCP servers) that are only created **after** the user completes a configuration wizard.
|
|
250
|
-
|
|
251
|
-
### Configuration Schema
|
|
252
|
-
|
|
253
|
-
Define a config wizard with a JSON schema:
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
const ConfigSchema = {
|
|
257
|
-
type: 'object',
|
|
258
|
-
properties: {
|
|
259
|
-
name: { type: 'string' },
|
|
260
|
-
role: { type: 'string' },
|
|
261
|
-
systemPrompt: { type: 'string' },
|
|
262
|
-
model: { type: 'string', enum: ['gpt-4', 'claude-3-sonnet'] }
|
|
263
|
-
},
|
|
264
|
-
required: ['name', 'systemPrompt']
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
.configWizard({
|
|
268
|
-
// Schema enables auto-generation of updateConfig() and getConfig()
|
|
269
|
-
schema: ConfigSchema,
|
|
270
|
-
storageKey: 'teammate-config', // Optional, defaults to '_plugin_config'
|
|
271
|
-
|
|
272
|
-
// UI
|
|
273
|
-
path: '/plugin-screens/my-plugin/main',
|
|
274
|
-
hash: '#/config-wizard',
|
|
275
|
-
title: 'Configure Your Assistant',
|
|
276
|
-
width: 900,
|
|
277
|
-
height: 700,
|
|
278
|
-
description: 'Set up your AI assistant with custom attributes.',
|
|
279
|
-
|
|
280
|
-
// When to show the wizard
|
|
281
|
-
shouldShow: async (ctx) => {
|
|
282
|
-
const config = await ctx.storage.get('teammate-config');
|
|
283
|
-
return !config; // Show if no config exists
|
|
284
|
-
}
|
|
285
|
-
})
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
**What this does:**
|
|
289
|
-
- ✅ Auto-generates `updateConfig(config)` function (validates against schema, saves to storage)
|
|
290
|
-
- ✅ Auto-generates `getConfig()` function (retrieves current config)
|
|
291
|
-
- ✅ Both functions available in generated client with React hooks
|
|
292
|
-
- ✅ Plugin-kit loads config and calls factories at capability generation time
|
|
293
|
-
|
|
294
|
-
### Configurable Teammates
|
|
295
|
-
|
|
296
|
-
Register teammates that are created based on user configuration:
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
.configurableTeamMember((config) => {
|
|
300
|
-
// config is loaded from storage by plugin-kit
|
|
301
|
-
return [{
|
|
302
|
-
id: 'my-assistant',
|
|
303
|
-
name: config.name,
|
|
304
|
-
role: 'assistant',
|
|
305
|
-
systemPrompt: config.systemPrompt,
|
|
306
|
-
model: config.model,
|
|
307
|
-
expertise: config.skills || [],
|
|
308
|
-
isActive: true
|
|
309
|
-
}];
|
|
310
|
-
})
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
**How it works:**
|
|
314
|
-
1. **First load (no config)**: Factory not called, no teammate registered, config wizard shown
|
|
315
|
-
2. **User completes wizard**: UI calls auto-generated `updateConfig()`, sends `postMessage({ type: 'majk:config-complete' })`
|
|
316
|
-
3. **Plugin reloads**: Config exists, factory called with config, teammate registered!
|
|
317
|
-
|
|
318
|
-
### Configurable MCP Servers
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
.configurableMcp((config) => {
|
|
322
|
-
if (!config.enableMcpServer) return [];
|
|
323
|
-
|
|
324
|
-
return [{
|
|
325
|
-
id: 'custom-mcp',
|
|
326
|
-
name: config.mcpServerName,
|
|
327
|
-
type: 'stdio',
|
|
328
|
-
command: config.command,
|
|
329
|
-
args: config.args || [],
|
|
330
|
-
env: config.env || {}
|
|
331
|
-
}];
|
|
332
|
-
})
|
|
333
|
-
```
|
|
95
|
+
- `name` is the display name
|
|
96
|
+
- `version` follows semver
|
|
97
|
+
- **`.pluginRoot(__dirname)` is REQUIRED** - Must be the first call after `definePlugin`
|
|
334
98
|
|
|
335
|
-
###
|
|
99
|
+
### Screens
|
|
336
100
|
|
|
337
|
-
|
|
338
|
-
.configurableEntity('project', (config) => {
|
|
339
|
-
return config.projects.map(p => ({
|
|
340
|
-
id: p.id,
|
|
341
|
-
name: p.name,
|
|
342
|
-
description: p.description
|
|
343
|
-
}));
|
|
344
|
-
})
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Settings Screen
|
|
101
|
+
#### React Screens
|
|
348
102
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
.settings({
|
|
353
|
-
path: '/plugin-screens/my-plugin/main',
|
|
354
|
-
hash: '#/settings',
|
|
355
|
-
title: 'Plugin Settings',
|
|
356
|
-
description: 'Reconfigure your assistant and preferences.'
|
|
357
|
-
})
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
The settings screen can use the same auto-generated `updateConfig()` and `getConfig()` functions that were created by `.configWizard({ schema })`.
|
|
361
|
-
|
|
362
|
-
**Note:** While `ConfigWizardDef` type includes an optional `settings` property, the current implementation requires using the separate `.settings()` method shown above.
|
|
363
|
-
|
|
364
|
-
### Build-Time Validation
|
|
365
|
-
|
|
366
|
-
Plugin-kit validates your usage:
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
// ❌ ERROR: Configurable entities require configWizard with schema
|
|
370
|
-
.configurableTeamMember((config) => [...])
|
|
371
|
-
// Missing .configWizard({ schema: ... })
|
|
372
|
-
|
|
373
|
-
// ✅ CORRECT
|
|
374
|
-
.configWizard({ schema: MySchema, ... })
|
|
375
|
-
.configurableTeamMember((config) => [...])
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
---
|
|
379
|
-
|
|
380
|
-
## 🎨 UI Configuration
|
|
381
|
-
|
|
382
|
-
### React Applications
|
|
383
|
-
|
|
384
|
-
For React SPAs, configure UI first, then add screens:
|
|
103
|
+
For React SPAs, configure UI:
|
|
385
104
|
|
|
386
105
|
```typescript
|
|
387
106
|
.ui({
|
|
388
|
-
appDir: 'dist',
|
|
389
|
-
base: '/',
|
|
390
|
-
history: 'hash'
|
|
107
|
+
appDir: 'dist', // Where your built React app is
|
|
108
|
+
base: '/plugin-screens/my-plugin', // Base path (NO trailing slash)
|
|
109
|
+
history: 'hash' // REQUIRED: Use 'hash' for iframe routing
|
|
391
110
|
})
|
|
392
111
|
|
|
393
112
|
.screenReact({
|
|
394
113
|
id: 'dashboard',
|
|
395
114
|
name: 'Dashboard',
|
|
396
|
-
description: 'Main dashboard view. Shows metrics and
|
|
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
|
|
115
|
+
description: 'Main dashboard view. Shows metrics and controls.', // 2-3 sentences
|
|
116
|
+
route: '/plugin-screens/my-plugin/dashboard', // Must start with /plugin-screens/{id}/
|
|
117
|
+
reactPath: '/' // Path within your React app
|
|
408
118
|
})
|
|
409
119
|
```
|
|
410
120
|
|
|
411
|
-
**The React app receives
|
|
412
|
-
|
|
413
|
-
window.
|
|
414
|
-
window.
|
|
415
|
-
window.__MAJK_PLUGIN_ID__ // Your plugin ID
|
|
416
|
-
```
|
|
121
|
+
**The React app receives:**
|
|
122
|
+
- `window.__MAJK_BASE_URL__` - Host base URL
|
|
123
|
+
- `window.__MAJK_IFRAME_BASE__` - Plugin base path
|
|
124
|
+
- `window.__MAJK_PLUGIN_ID__` - Your plugin ID
|
|
417
125
|
|
|
418
|
-
|
|
126
|
+
#### HTML Screens
|
|
419
127
|
|
|
420
|
-
For simple
|
|
128
|
+
For simple HTML pages:
|
|
421
129
|
|
|
422
130
|
```typescript
|
|
423
131
|
.screenHtml({
|
|
424
132
|
id: 'about',
|
|
425
133
|
name: 'About',
|
|
426
|
-
description: 'Information about the plugin. Shows version and
|
|
134
|
+
description: 'Information about the plugin. Shows version and author.',
|
|
427
135
|
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
|
|
136
|
+
html: '<html>...' // OR htmlFile: 'about.html'
|
|
442
137
|
})
|
|
443
138
|
```
|
|
444
139
|
|
|
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.
|
|
140
|
+
### API Routes
|
|
478
141
|
|
|
479
142
|
Define REST endpoints:
|
|
480
143
|
|
|
@@ -483,74 +146,60 @@ Define REST endpoints:
|
|
|
483
146
|
method: 'POST',
|
|
484
147
|
path: '/api/tasks/:id/complete',
|
|
485
148
|
name: 'Complete Task',
|
|
486
|
-
description: 'Marks a task as complete. Updates status and
|
|
149
|
+
description: 'Marks a task as complete. Updates task status and triggers notifications.',
|
|
487
150
|
handler: async (req, res, { majk, storage, logger }) => {
|
|
488
|
-
const { id } = req.params;
|
|
489
|
-
const { note } = req.body;
|
|
490
|
-
const status = req.query.get('status');
|
|
151
|
+
const { id } = req.params; // Path parameters
|
|
152
|
+
const { note } = req.body; // Request body
|
|
153
|
+
const status = req.query.get('status'); // Query params
|
|
491
154
|
|
|
492
155
|
logger.info(`Completing task ${id}`);
|
|
493
156
|
|
|
494
|
-
|
|
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;
|
|
157
|
+
// Access MAJK APIs
|
|
158
|
+
const todos = await majk.todos.list();
|
|
503
159
|
|
|
504
|
-
|
|
160
|
+
// Use plugin storage
|
|
161
|
+
await storage.set(`task:${id}`, { completed: true });
|
|
505
162
|
|
|
506
|
-
return { success: true,
|
|
163
|
+
return { success: true, taskId: id };
|
|
507
164
|
}
|
|
508
165
|
})
|
|
509
166
|
```
|
|
510
167
|
|
|
511
168
|
**Available Methods:** `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
|
|
512
169
|
|
|
513
|
-
**
|
|
514
|
-
- `
|
|
515
|
-
- `
|
|
516
|
-
- `
|
|
517
|
-
- `
|
|
518
|
-
- `storage` - Plugin storage
|
|
519
|
-
- `logger` - Scoped logger
|
|
520
|
-
- `http` - HTTP config (port, baseUrl, secret)
|
|
521
|
-
|
|
522
|
-
---
|
|
170
|
+
**Context Provided:**
|
|
171
|
+
- `majk` - Full MAJK API interface
|
|
172
|
+
- `storage` - Plugin-scoped key-value storage
|
|
173
|
+
- `logger` - Scoped logger (debug, info, warn, error)
|
|
174
|
+
- `http` - HTTP configuration (port, baseUrl, secret)
|
|
523
175
|
|
|
524
|
-
|
|
176
|
+
### Tools
|
|
525
177
|
|
|
526
|
-
Tools are functions that
|
|
178
|
+
Tools are functions that agents can invoke:
|
|
527
179
|
|
|
528
180
|
```typescript
|
|
529
181
|
.tool(
|
|
530
182
|
'conversation', // Scope: 'global' | 'conversation' | 'teammate' | 'project'
|
|
531
183
|
{
|
|
532
|
-
name: '
|
|
533
|
-
description: 'Analyzes
|
|
184
|
+
name: 'analyzeSentiment',
|
|
185
|
+
description: 'Analyzes text sentiment. Returns positive, negative, or neutral classification.',
|
|
534
186
|
inputSchema: {
|
|
535
187
|
type: 'object',
|
|
536
188
|
properties: {
|
|
537
|
-
|
|
538
|
-
language: { type: 'string' }
|
|
189
|
+
text: { type: 'string' }
|
|
539
190
|
},
|
|
540
|
-
required: ['
|
|
191
|
+
required: ['text']
|
|
541
192
|
}
|
|
542
193
|
},
|
|
543
194
|
async (input, { majk, logger }) => {
|
|
544
|
-
logger.info(
|
|
195
|
+
logger.info('Analyzing sentiment');
|
|
545
196
|
|
|
546
|
-
|
|
197
|
+
// Your implementation
|
|
198
|
+
const sentiment = analyzeSentiment(input.text);
|
|
547
199
|
|
|
548
200
|
return {
|
|
549
201
|
success: true,
|
|
550
|
-
data: {
|
|
551
|
-
issues,
|
|
552
|
-
summary: `Found ${issues.length} issues`
|
|
553
|
-
}
|
|
202
|
+
data: { sentiment, confidence: 0.95 }
|
|
554
203
|
};
|
|
555
204
|
}
|
|
556
205
|
)
|
|
@@ -558,342 +207,115 @@ Tools are functions that AI agents can invoke. They're scoped to different conte
|
|
|
558
207
|
|
|
559
208
|
**Tool Scopes:**
|
|
560
209
|
- `global` - Available everywhere
|
|
561
|
-
- `conversation` - Scoped to
|
|
562
|
-
- `teammate` - Scoped to
|
|
563
|
-
- `project` - Scoped to
|
|
564
|
-
|
|
565
|
-
**Scoped Metadata:**
|
|
210
|
+
- `conversation` - Scoped to conversations
|
|
211
|
+
- `teammate` - Scoped to teammates
|
|
212
|
+
- `project` - Scoped to projects
|
|
566
213
|
|
|
567
|
-
|
|
214
|
+
### Entities
|
|
568
215
|
|
|
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
|
-
---
|
|
581
|
-
|
|
582
|
-
## 📦 Static Entities
|
|
583
|
-
|
|
584
|
-
Declare entities your plugin always provides:
|
|
585
|
-
|
|
586
|
-
### MCP Servers
|
|
216
|
+
Declare entities your plugin provides:
|
|
587
217
|
|
|
588
218
|
```typescript
|
|
589
|
-
.
|
|
219
|
+
.entity('teammate', [
|
|
590
220
|
{
|
|
591
|
-
id: '
|
|
592
|
-
name: '
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
596
|
-
env: {
|
|
597
|
-
GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN
|
|
598
|
-
}
|
|
221
|
+
id: 'bot-assistant',
|
|
222
|
+
name: 'Bot Assistant',
|
|
223
|
+
role: 'bot',
|
|
224
|
+
capabilities: ['analysis', 'reporting']
|
|
599
225
|
}
|
|
600
226
|
])
|
|
601
|
-
```
|
|
602
227
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
```typescript
|
|
606
|
-
.teamMember([
|
|
228
|
+
.entity('mcpServer', [
|
|
607
229
|
{
|
|
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'
|
|
230
|
+
id: 'custom-server',
|
|
231
|
+
name: 'Custom MCP Server',
|
|
232
|
+
transport: { type: 'stdio', command: 'node', args: ['server.js'] }
|
|
627
233
|
}
|
|
628
234
|
])
|
|
629
235
|
```
|
|
630
236
|
|
|
631
237
|
**Supported Entity Types:**
|
|
632
238
|
- `mcpServer` - MCP servers
|
|
633
|
-
- `
|
|
239
|
+
- `teammate` - Team members/bots
|
|
634
240
|
- `conversation` - Conversations
|
|
241
|
+
- `todo` - Tasks
|
|
635
242
|
- `project` - Projects
|
|
636
243
|
- `agent` - AI agents
|
|
637
244
|
|
|
638
|
-
|
|
245
|
+
### Config Wizard & Settings
|
|
639
246
|
|
|
640
|
-
|
|
247
|
+
#### Config Wizard
|
|
641
248
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
### Basic Secret Provider
|
|
249
|
+
Show a wizard on first run:
|
|
645
250
|
|
|
646
251
|
```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
|
-
};
|
|
252
|
+
.configWizard({
|
|
253
|
+
path: '/setup',
|
|
254
|
+
title: 'Initial Setup',
|
|
255
|
+
width: 600,
|
|
256
|
+
height: 400,
|
|
257
|
+
description: 'Configure plugin settings. Set up API keys and preferences.',
|
|
258
|
+
shouldShow: async (ctx) => {
|
|
259
|
+
const config = await ctx.storage.get('config');
|
|
260
|
+
return !config; // Show if no config exists
|
|
725
261
|
}
|
|
726
262
|
})
|
|
727
263
|
```
|
|
728
264
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
Secrets can be scoped to different contexts:
|
|
265
|
+
#### Settings Screen
|
|
732
266
|
|
|
733
|
-
|
|
734
|
-
- **`project`** - Specific to a project (requires `projectId`)
|
|
735
|
-
- **`integration`** - Specific to an integration (requires `integrationId`)
|
|
736
|
-
- **`conversation`** - Specific to a conversation (requires `conversationId`)
|
|
737
|
-
- **`user`** - User-specific (requires `userId`)
|
|
738
|
-
|
|
739
|
-
### Resolution Context
|
|
740
|
-
|
|
741
|
-
When resolving secrets, MAJK provides context about where the secret is being used:
|
|
267
|
+
Ongoing settings management:
|
|
742
268
|
|
|
743
269
|
```typescript
|
|
744
|
-
|
|
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
|
-
}
|
|
270
|
+
.settings({
|
|
271
|
+
path: '/settings',
|
|
272
|
+
title: 'Plugin Settings',
|
|
273
|
+
description: 'Manage plugin configuration. Adjust behavior and display options.'
|
|
838
274
|
})
|
|
839
275
|
```
|
|
840
276
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
## 🔄 Lifecycle Hooks
|
|
277
|
+
### Lifecycle Hooks
|
|
844
278
|
|
|
845
|
-
|
|
279
|
+
#### onReady
|
|
846
280
|
|
|
847
|
-
Called after
|
|
281
|
+
Called after server starts, before `onLoad` completes:
|
|
848
282
|
|
|
849
283
|
```typescript
|
|
850
284
|
.onReady(async (ctx, cleanup) => {
|
|
851
|
-
ctx.logger.info('Plugin initializing...');
|
|
852
|
-
|
|
853
285
|
// Subscribe to events
|
|
854
|
-
const
|
|
855
|
-
ctx.logger.info(`Conversation
|
|
286
|
+
const sub = ctx.majk.eventBus.conversations().subscribe((event) => {
|
|
287
|
+
ctx.logger.info(`Conversation event: ${event.type}`);
|
|
856
288
|
});
|
|
857
|
-
cleanup(() =>
|
|
289
|
+
cleanup(() => sub.unsubscribe());
|
|
858
290
|
|
|
859
|
-
// Set up
|
|
291
|
+
// Set up timers
|
|
860
292
|
const timer = setInterval(() => {
|
|
861
|
-
ctx.logger.debug('
|
|
293
|
+
ctx.logger.debug('Periodic check');
|
|
862
294
|
}, 60000);
|
|
863
295
|
cleanup(() => clearInterval(timer));
|
|
864
296
|
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
ctx.logger.info(`Loaded ${Object.keys(data).length} items`);
|
|
297
|
+
// Any other setup
|
|
298
|
+
await loadData(ctx.storage);
|
|
868
299
|
})
|
|
869
300
|
```
|
|
870
301
|
|
|
871
|
-
**Cleanup
|
|
872
|
-
All cleanup functions
|
|
302
|
+
**Cleanup Registration:**
|
|
303
|
+
All cleanup functions are automatically called on `onUnload()`.
|
|
873
304
|
|
|
874
|
-
|
|
305
|
+
#### Health Checks
|
|
875
306
|
|
|
876
307
|
Define custom health monitoring:
|
|
877
308
|
|
|
878
309
|
```typescript
|
|
879
310
|
.health(async ({ majk, storage, logger }) => {
|
|
880
311
|
try {
|
|
881
|
-
// Check
|
|
312
|
+
// Check dependencies
|
|
882
313
|
await majk.conversations.list();
|
|
883
|
-
|
|
884
|
-
// Check storage
|
|
885
|
-
await storage.get('health-test');
|
|
886
|
-
|
|
887
|
-
// Check external dependencies
|
|
888
|
-
const externalOk = await checkExternalAPI();
|
|
314
|
+
await storage.get('health-check');
|
|
889
315
|
|
|
890
316
|
return {
|
|
891
317
|
healthy: true,
|
|
892
|
-
details: {
|
|
893
|
-
api: 'ok',
|
|
894
|
-
storage: 'ok',
|
|
895
|
-
external: externalOk ? 'ok' : 'degraded'
|
|
896
|
-
}
|
|
318
|
+
details: { api: 'ok', storage: 'ok' }
|
|
897
319
|
};
|
|
898
320
|
} catch (error) {
|
|
899
321
|
logger.error(`Health check failed: ${error.message}`);
|
|
@@ -905,209 +327,7 @@ Define custom health monitoring:
|
|
|
905
327
|
})
|
|
906
328
|
```
|
|
907
329
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
## 🎨 Client Generation
|
|
911
|
-
|
|
912
|
-
Plugin-kit can auto-generate TypeScript clients with React hooks from your function definitions.
|
|
913
|
-
|
|
914
|
-
### Generate Client
|
|
915
|
-
|
|
916
|
-
```bash
|
|
917
|
-
npx plugin-kit generate -e ./index.js -o ./ui/src/generated
|
|
918
|
-
```
|
|
919
|
-
|
|
920
|
-
**Output:**
|
|
921
|
-
```
|
|
922
|
-
ui/src/generated/
|
|
923
|
-
├── types.ts # TypeScript types from schemas
|
|
924
|
-
├── client.ts # Base client with fetch wrappers
|
|
925
|
-
├── hooks.ts # React hooks (useGetMessage, etc)
|
|
926
|
-
└── index.ts # Re-exports
|
|
927
|
-
```
|
|
928
|
-
|
|
929
|
-
### Using Generated Hooks
|
|
930
|
-
|
|
931
|
-
```tsx
|
|
932
|
-
import { useCreateTask, useGetTasks } from './generated';
|
|
933
|
-
|
|
934
|
-
function TaskList() {
|
|
935
|
-
// Query hook
|
|
936
|
-
const { data: tasks, loading, error, refetch } = useGetTasks({});
|
|
937
|
-
|
|
938
|
-
// Mutation hook
|
|
939
|
-
const { mutate: createTask, loading: creating } = useCreateTask();
|
|
940
|
-
|
|
941
|
-
const handleCreate = async () => {
|
|
942
|
-
const result = await createTask({
|
|
943
|
-
title: 'New task',
|
|
944
|
-
priority: 'high'
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
if (result.success) {
|
|
948
|
-
refetch(); // Refresh list
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
|
|
952
|
-
if (loading) return <div>Loading...</div>;
|
|
953
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
954
|
-
|
|
955
|
-
return (
|
|
956
|
-
<div>
|
|
957
|
-
{tasks?.map(task => (
|
|
958
|
-
<div key={task.id}>{task.title}</div>
|
|
959
|
-
))}
|
|
960
|
-
<button onClick={handleCreate} disabled={creating}>
|
|
961
|
-
{creating ? 'Creating...' : 'Add Task'}
|
|
962
|
-
</button>
|
|
963
|
-
</div>
|
|
964
|
-
);
|
|
965
|
-
}
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
### Hook API
|
|
969
|
-
|
|
970
|
-
**Query Hooks** (for data fetching):
|
|
971
|
-
```typescript
|
|
972
|
-
const { data, loading, error, refetch } = useGetTasks({ filter: 'active' });
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
**Mutation Hooks** (for data modification):
|
|
976
|
-
```typescript
|
|
977
|
-
const { mutate, loading, error } = useCreateTask();
|
|
978
|
-
|
|
979
|
-
// Call the mutation
|
|
980
|
-
const result = await mutate({ title: 'Task' });
|
|
981
|
-
```
|
|
982
|
-
|
|
983
|
-
### Auto-Generated Config Functions
|
|
984
|
-
|
|
985
|
-
When you define a `configWizard` with a `schema`, plugin-kit automatically generates:
|
|
986
|
-
|
|
987
|
-
```typescript
|
|
988
|
-
// In your UI
|
|
989
|
-
import { useUpdateConfig, useGetConfig } from './generated';
|
|
990
|
-
|
|
991
|
-
function ConfigWizard() {
|
|
992
|
-
const { mutate: updateConfig, loading } = useUpdateConfig();
|
|
993
|
-
const { data: config } = useGetConfig({});
|
|
994
|
-
|
|
995
|
-
const handleSubmit = async (formData) => {
|
|
996
|
-
const result = await updateConfig(formData);
|
|
997
|
-
|
|
998
|
-
if (result.success) {
|
|
999
|
-
// Close modal
|
|
1000
|
-
window.parent.postMessage({
|
|
1001
|
-
type: 'majk:config-complete',
|
|
1002
|
-
pluginId: 'my-plugin',
|
|
1003
|
-
redirectTo: '/plugins'
|
|
1004
|
-
}, '*');
|
|
1005
|
-
}
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
return <form onSubmit={handleSubmit}>...</form>;
|
|
1009
|
-
}
|
|
1010
|
-
```
|
|
1011
|
-
|
|
1012
|
-
---
|
|
1013
|
-
|
|
1014
|
-
## 📖 Complete Example
|
|
1015
|
-
|
|
1016
|
-
See `samples/kitchen-sink` for a comprehensive example showing:
|
|
1017
|
-
|
|
1018
|
-
```typescript
|
|
1019
|
-
import { definePlugin, HttpTransport } from '@majkapp/plugin-kit';
|
|
1020
|
-
|
|
1021
|
-
const ConfigSchema = {
|
|
1022
|
-
type: 'object',
|
|
1023
|
-
properties: {
|
|
1024
|
-
name: { type: 'string' },
|
|
1025
|
-
systemPrompt: { type: 'string' }
|
|
1026
|
-
},
|
|
1027
|
-
required: ['name']
|
|
1028
|
-
};
|
|
1029
|
-
|
|
1030
|
-
export = definePlugin('kitchen-sink', 'Kitchen Sink Demo', '1.0.0')
|
|
1031
|
-
.pluginRoot(__dirname)
|
|
1032
|
-
|
|
1033
|
-
// Functions
|
|
1034
|
-
.function('createTask', {
|
|
1035
|
-
description: 'Creates a new task.',
|
|
1036
|
-
input: { /* schema */ },
|
|
1037
|
-
output: { /* schema */ },
|
|
1038
|
-
handler: async (input, ctx) => { /* ... */ }
|
|
1039
|
-
})
|
|
1040
|
-
|
|
1041
|
-
// Configurable entities
|
|
1042
|
-
.configWizard({
|
|
1043
|
-
schema: ConfigSchema,
|
|
1044
|
-
storageKey: 'teammate-config',
|
|
1045
|
-
path: '/plugin-screens/kitchen-sink/main',
|
|
1046
|
-
hash: '#/config-wizard',
|
|
1047
|
-
title: 'Configure Assistant',
|
|
1048
|
-
shouldShow: async (ctx) => !await ctx.storage.get('teammate-config')
|
|
1049
|
-
})
|
|
1050
|
-
|
|
1051
|
-
.configurableTeamMember((config) => [{
|
|
1052
|
-
id: 'my-assistant',
|
|
1053
|
-
name: config.name,
|
|
1054
|
-
systemPrompt: config.systemPrompt
|
|
1055
|
-
}])
|
|
1056
|
-
|
|
1057
|
-
// UI
|
|
1058
|
-
.ui({ appDir: 'dist', base: '/', history: 'hash' })
|
|
1059
|
-
|
|
1060
|
-
.screenReact({
|
|
1061
|
-
id: 'main',
|
|
1062
|
-
name: 'Kitchen Sink',
|
|
1063
|
-
description: 'Comprehensive demo of all plugin-kit features.',
|
|
1064
|
-
route: '/plugin-screens/kitchen-sink/main',
|
|
1065
|
-
reactPath: '/'
|
|
1066
|
-
})
|
|
1067
|
-
|
|
1068
|
-
// Tools
|
|
1069
|
-
.tool('global', {
|
|
1070
|
-
name: 'analyze',
|
|
1071
|
-
description: 'Analyzes input data.',
|
|
1072
|
-
inputSchema: { /* ... */ }
|
|
1073
|
-
}, handler)
|
|
1074
|
-
|
|
1075
|
-
// Static entities
|
|
1076
|
-
.mcpServer([{ /* ... */ }])
|
|
1077
|
-
|
|
1078
|
-
// Navigation
|
|
1079
|
-
.topbar('/plugin-screens/kitchen-sink/main', { icon: '🧰' })
|
|
1080
|
-
|
|
1081
|
-
// Settings
|
|
1082
|
-
.settings({
|
|
1083
|
-
path: '/plugin-screens/kitchen-sink/main',
|
|
1084
|
-
hash: '#/settings',
|
|
1085
|
-
title: 'Settings',
|
|
1086
|
-
description: 'Reconfigure the plugin.'
|
|
1087
|
-
})
|
|
1088
|
-
|
|
1089
|
-
// Lifecycle
|
|
1090
|
-
.onReady(async (ctx, cleanup) => {
|
|
1091
|
-
const sub = ctx.majk.eventBus.subscribeAll((event) => {
|
|
1092
|
-
ctx.logger.info(`Event: ${event.type}`);
|
|
1093
|
-
});
|
|
1094
|
-
cleanup(() => sub.unsubscribe());
|
|
1095
|
-
})
|
|
1096
|
-
|
|
1097
|
-
.health(async ({ storage }) => {
|
|
1098
|
-
await storage.get('health-check');
|
|
1099
|
-
return { healthy: true };
|
|
1100
|
-
})
|
|
1101
|
-
|
|
1102
|
-
// Transport
|
|
1103
|
-
.transport(new HttpTransport())
|
|
1104
|
-
|
|
1105
|
-
.build();
|
|
1106
|
-
```
|
|
1107
|
-
|
|
1108
|
-
---
|
|
1109
|
-
|
|
1110
|
-
## 📚 API Reference
|
|
330
|
+
## API Reference
|
|
1111
331
|
|
|
1112
332
|
### PluginContext
|
|
1113
333
|
|
|
@@ -1115,71 +335,33 @@ Provided to all handlers and hooks:
|
|
|
1115
335
|
|
|
1116
336
|
```typescript
|
|
1117
337
|
interface PluginContext {
|
|
1118
|
-
pluginId: string;
|
|
1119
|
-
pluginRoot: string;
|
|
1120
|
-
dataDir: string;
|
|
338
|
+
pluginId: string; // Your plugin ID
|
|
339
|
+
pluginRoot: string; // Plugin directory path
|
|
340
|
+
dataDir: string; // Plugin data directory
|
|
1121
341
|
|
|
1122
342
|
app: {
|
|
1123
|
-
version: string;
|
|
1124
|
-
name: string;
|
|
1125
|
-
appDataDir: string;
|
|
343
|
+
version: string; // MAJK version
|
|
344
|
+
name: string; // App name
|
|
345
|
+
appDataDir: string; // App data directory
|
|
1126
346
|
};
|
|
1127
347
|
|
|
1128
348
|
http: {
|
|
1129
|
-
port: number;
|
|
1130
|
-
secret: string;
|
|
1131
|
-
baseUrl: string;
|
|
349
|
+
port: number; // Assigned HTTP port
|
|
350
|
+
secret: string; // Security secret
|
|
351
|
+
baseUrl: string; // Base URL for iframe
|
|
1132
352
|
};
|
|
1133
353
|
|
|
1134
|
-
majk: MajkInterface;
|
|
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[]>;
|
|
354
|
+
majk: MajkInterface; // Full MAJK API
|
|
355
|
+
storage: PluginStorage; // Key-value storage
|
|
356
|
+
logger: PluginLogger; // Scoped logger
|
|
357
|
+
timers?: ScopedTimers; // Managed timers
|
|
358
|
+
ipc?: ScopedIpcRegistry; // Electron IPC
|
|
1153
359
|
}
|
|
1154
|
-
|
|
1155
|
-
// Usage
|
|
1156
|
-
await storage.set('user-prefs', { theme: 'dark' });
|
|
1157
|
-
const prefs = await storage.get('user-prefs');
|
|
1158
|
-
await storage.delete('old-key');
|
|
1159
|
-
await storage.clear();
|
|
1160
|
-
const allKeys = await storage.keys();
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
### PluginLogger
|
|
1164
|
-
|
|
1165
|
-
Structured logging with levels:
|
|
1166
|
-
|
|
1167
|
-
```typescript
|
|
1168
|
-
interface PluginLogger {
|
|
1169
|
-
debug(message: string, ...args: any[]): void;
|
|
1170
|
-
info(message: string, ...args: any[]): void;
|
|
1171
|
-
warn(message: string, ...args: any[]): void;
|
|
1172
|
-
error(message: string, ...args: any[]): void;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
// Usage
|
|
1176
|
-
logger.info('User action', { userId, action: 'create' });
|
|
1177
|
-
logger.error('Failed to process', { error: error.message });
|
|
1178
360
|
```
|
|
1179
361
|
|
|
1180
362
|
### MajkInterface
|
|
1181
363
|
|
|
1182
|
-
The main MAJK API
|
|
364
|
+
The main MAJK API:
|
|
1183
365
|
|
|
1184
366
|
```typescript
|
|
1185
367
|
interface MajkInterface {
|
|
@@ -1197,464 +379,266 @@ interface MajkInterface {
|
|
|
1197
379
|
}
|
|
1198
380
|
```
|
|
1199
381
|
|
|
1200
|
-
###
|
|
382
|
+
### PluginStorage
|
|
1201
383
|
|
|
1202
|
-
|
|
384
|
+
Simple key-value storage scoped to your plugin:
|
|
1203
385
|
|
|
1204
|
-
|
|
386
|
+
```typescript
|
|
387
|
+
interface PluginStorage {
|
|
388
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
389
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
390
|
+
delete(key: string): Promise<void>;
|
|
391
|
+
clear(): Promise<void>;
|
|
392
|
+
keys(): Promise<string[]>;
|
|
393
|
+
}
|
|
394
|
+
```
|
|
1205
395
|
|
|
1206
|
-
|
|
396
|
+
**Example:**
|
|
1207
397
|
|
|
1208
398
|
```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
|
-
})
|
|
399
|
+
// Save data
|
|
400
|
+
await storage.set('user-preferences', {
|
|
401
|
+
theme: 'dark',
|
|
402
|
+
notifications: true
|
|
403
|
+
});
|
|
1223
404
|
|
|
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);
|
|
405
|
+
// Load data
|
|
406
|
+
const prefs = await storage.get<Preferences>('user-preferences');
|
|
1230
407
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
408
|
+
// List all keys
|
|
409
|
+
const keys = await storage.keys();
|
|
1234
410
|
|
|
1235
|
-
|
|
1236
|
-
|
|
411
|
+
// Delete specific key
|
|
412
|
+
await storage.delete('old-data');
|
|
1237
413
|
|
|
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
|
-
})
|
|
414
|
+
// Clear everything
|
|
415
|
+
await storage.clear();
|
|
1249
416
|
```
|
|
1250
417
|
|
|
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
|
|
418
|
+
### EventBus
|
|
1256
419
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
Manage AI teammates:
|
|
420
|
+
Subscribe to system events:
|
|
1260
421
|
|
|
1261
422
|
```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
|
-
})
|
|
423
|
+
// Listen to conversation events
|
|
424
|
+
const sub = majk.eventBus.conversations().subscribe((event) => {
|
|
425
|
+
console.log(`Event: ${event.type}`, event.entity);
|
|
426
|
+
});
|
|
1278
427
|
|
|
1279
|
-
//
|
|
1280
|
-
.
|
|
1281
|
-
description: 'Gets a specific teammate.',
|
|
1282
|
-
handler: async ({ teammateId }, ctx) => {
|
|
1283
|
-
const teammate = await ctx.majk.teammates.get(teammateId);
|
|
428
|
+
// Unsubscribe
|
|
429
|
+
sub.unsubscribe();
|
|
1284
430
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
431
|
+
// Specific event types
|
|
432
|
+
majk.eventBus.conversations().created().subscribe(...);
|
|
433
|
+
majk.eventBus.conversations().updated().subscribe(...);
|
|
434
|
+
majk.eventBus.conversations().deleted().subscribe(...);
|
|
1288
435
|
|
|
1289
|
-
|
|
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
|
-
})
|
|
436
|
+
// Custom channels
|
|
437
|
+
majk.eventBus.channel('my-events').subscribe(...);
|
|
1302
438
|
```
|
|
1303
439
|
|
|
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
|
|
440
|
+
## Validation & Error Handling
|
|
1310
441
|
|
|
1311
|
-
|
|
442
|
+
### Build-Time Validation
|
|
1312
443
|
|
|
1313
|
-
|
|
444
|
+
The kit validates at build time:
|
|
1314
445
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
const isAuthenticated = await ctx.majk.auth.isAuthenticated();
|
|
1321
|
-
const account = await ctx.majk.auth.getAccount();
|
|
446
|
+
✅ **Route Prefixes** - Screen routes must match plugin ID
|
|
447
|
+
✅ **File Existence** - React dist and HTML files must exist
|
|
448
|
+
✅ **Uniqueness** - No duplicate routes, tools, or API endpoints
|
|
449
|
+
✅ **Dependencies** - UI must be configured for React screens
|
|
450
|
+
✅ **Descriptions** - Must be 2-3 sentences ending with period
|
|
1322
451
|
|
|
1323
|
-
|
|
1324
|
-
authenticated: isAuthenticated,
|
|
1325
|
-
account: account ? {
|
|
1326
|
-
id: account.id,
|
|
1327
|
-
name: account.name,
|
|
1328
|
-
email: account.email
|
|
1329
|
-
} : null
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
})
|
|
452
|
+
**Example Error:**
|
|
1333
453
|
|
|
1334
|
-
|
|
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
|
-
})
|
|
454
|
+
```
|
|
455
|
+
❌ Plugin Build Failed: React screen route must start with "/plugin-screens/my-plugin/"
|
|
456
|
+
💡 Suggestion: Change route from "/screens/dashboard" to "/plugin-screens/my-plugin/dashboard"
|
|
457
|
+
📋 Context: {
|
|
458
|
+
"screen": "dashboard",
|
|
459
|
+
"route": "/screens/dashboard"
|
|
460
|
+
}
|
|
1349
461
|
```
|
|
1350
462
|
|
|
1351
|
-
|
|
1352
|
-
- `isAuthenticated()` - Check if user is logged in
|
|
1353
|
-
- `getAccount()` - Get current account
|
|
1354
|
-
- `getAccountsOfType(type)` - Get accounts by type ('github', 'google', 'email', '*')
|
|
1355
|
-
|
|
1356
|
-
#### EventBus API
|
|
463
|
+
### Runtime Error Handling
|
|
1357
464
|
|
|
1358
|
-
|
|
465
|
+
All API route errors are automatically caught and logged:
|
|
1359
466
|
|
|
1360
467
|
```typescript
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
//
|
|
468
|
+
.apiRoute({
|
|
469
|
+
method: 'POST',
|
|
470
|
+
path: '/api/process',
|
|
471
|
+
name: 'Process Data',
|
|
472
|
+
description: 'Processes input data. Validates and transforms the payload.',
|
|
473
|
+
handler: async (req, res, { logger }) => {
|
|
474
|
+
// Errors are automatically caught and returned as 500 responses
|
|
475
|
+
throw new Error('Processing failed');
|
|
476
|
+
|
|
477
|
+
// Returns:
|
|
1368
478
|
// {
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
// metadata: { ... }
|
|
479
|
+
// "error": "Processing failed",
|
|
480
|
+
// "route": "Process Data",
|
|
481
|
+
// "path": "/api/process"
|
|
1373
482
|
// }
|
|
1374
|
-
});
|
|
1375
|
-
|
|
1376
|
-
// Always cleanup subscriptions
|
|
1377
|
-
cleanup(() => subscription.unsubscribe());
|
|
1378
|
-
})
|
|
1379
|
-
|
|
1380
|
-
// Subscribe to specific entity type
|
|
1381
|
-
.onReady(async (ctx, cleanup) => {
|
|
1382
|
-
// Conversations only
|
|
1383
|
-
const convSub = ctx.majk.eventBus.conversations().subscribe((event) => {
|
|
1384
|
-
if (event.type === 'created') {
|
|
1385
|
-
ctx.logger.info(`New conversation: ${event.entity.title}`);
|
|
1386
|
-
}
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
cleanup(() => convSub.unsubscribe());
|
|
1390
|
-
})
|
|
1391
|
-
|
|
1392
|
-
// Subscribe to specific event type
|
|
1393
|
-
.onReady(async (ctx, cleanup) => {
|
|
1394
|
-
// Only conversation creations
|
|
1395
|
-
const sub = ctx.majk.eventBus.conversations().created().subscribe((event) => {
|
|
1396
|
-
ctx.logger.info(`Created: ${event.entity.id}`);
|
|
1397
|
-
});
|
|
1398
|
-
|
|
1399
|
-
cleanup(() => sub.unsubscribe());
|
|
1400
|
-
})
|
|
1401
|
-
|
|
1402
|
-
// Custom channel
|
|
1403
|
-
.onReady(async (ctx, cleanup) => {
|
|
1404
|
-
const sub = ctx.majk.eventBus.channel('my-plugin-events').subscribe((event) => {
|
|
1405
|
-
// Handle custom events
|
|
1406
|
-
});
|
|
1407
|
-
|
|
1408
|
-
cleanup(() => sub.unsubscribe());
|
|
1409
|
-
})
|
|
1410
|
-
```
|
|
1411
|
-
|
|
1412
|
-
**Event Types:**
|
|
1413
|
-
- `created` - Entity was created
|
|
1414
|
-
- `updated` - Entity was modified
|
|
1415
|
-
- `deleted` - Entity was removed
|
|
1416
|
-
|
|
1417
|
-
**Entity Types:**
|
|
1418
|
-
- `conversation` - Conversation events
|
|
1419
|
-
- `teammate` - Teammate events
|
|
1420
|
-
- `todo` - Todo/task events
|
|
1421
|
-
- `project` - Project events
|
|
1422
|
-
- `message` - Message events
|
|
1423
|
-
|
|
1424
|
-
**Available Methods:**
|
|
1425
|
-
- `subscribeAll(handler)` - Subscribe to all events
|
|
1426
|
-
- `conversations()` - Conversation events only
|
|
1427
|
-
- `teammates()` - Teammate events only
|
|
1428
|
-
- `todos()` - Todo events only
|
|
1429
|
-
- `projects()` - Project events only
|
|
1430
|
-
- `channel(name)` - Custom event channel
|
|
1431
|
-
- `.created()` - Filter to created events only
|
|
1432
|
-
- `.updated()` - Filter to updated events only
|
|
1433
|
-
- `.deleted()` - Filter to deleted events only
|
|
1434
|
-
- `subscription.unsubscribe()` - Stop listening
|
|
1435
|
-
|
|
1436
|
-
#### Storage API
|
|
1437
|
-
|
|
1438
|
-
Plugin-scoped persistent storage:
|
|
1439
|
-
|
|
1440
|
-
```typescript
|
|
1441
|
-
// Save data
|
|
1442
|
-
.function('saveSettings', {
|
|
1443
|
-
description: 'Saves user settings.',
|
|
1444
|
-
handler: async ({ settings }, ctx) => {
|
|
1445
|
-
await ctx.storage.set('user-settings', settings);
|
|
1446
|
-
return { success: true };
|
|
1447
|
-
}
|
|
1448
|
-
})
|
|
1449
|
-
|
|
1450
|
-
// Load data
|
|
1451
|
-
.function('getSettings', {
|
|
1452
|
-
description: 'Loads user settings.',
|
|
1453
|
-
handler: async (_, ctx) => {
|
|
1454
|
-
const settings = await ctx.storage.get('user-settings');
|
|
1455
|
-
return settings || { theme: 'light', notifications: true };
|
|
1456
|
-
}
|
|
1457
|
-
})
|
|
1458
|
-
|
|
1459
|
-
// List all keys
|
|
1460
|
-
.function('listStorageKeys', {
|
|
1461
|
-
description: 'Lists all storage keys.',
|
|
1462
|
-
handler: async (_, ctx) => {
|
|
1463
|
-
const keys = await ctx.storage.keys();
|
|
1464
|
-
return { keys, count: keys.length };
|
|
1465
|
-
}
|
|
1466
|
-
})
|
|
1467
|
-
|
|
1468
|
-
// Delete specific key
|
|
1469
|
-
.function('clearCache', {
|
|
1470
|
-
description: 'Clears cached data.',
|
|
1471
|
-
handler: async (_, ctx) => {
|
|
1472
|
-
await ctx.storage.delete('cache');
|
|
1473
|
-
return { success: true };
|
|
1474
|
-
}
|
|
1475
|
-
})
|
|
1476
|
-
|
|
1477
|
-
// Clear all storage
|
|
1478
|
-
.function('resetPlugin', {
|
|
1479
|
-
description: 'Resets all plugin data.',
|
|
1480
|
-
handler: async (_, ctx) => {
|
|
1481
|
-
await ctx.storage.clear();
|
|
1482
|
-
return { success: true, message: 'All data cleared' };
|
|
1483
483
|
}
|
|
1484
484
|
})
|
|
1485
485
|
```
|
|
1486
486
|
|
|
1487
|
-
|
|
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
|
-
// ...
|
|
487
|
+
Logs show:
|
|
488
|
+
```
|
|
489
|
+
❌ POST /api/process - Error: Processing failed
|
|
490
|
+
[stack trace]
|
|
1515
491
|
```
|
|
1516
492
|
|
|
1517
|
-
|
|
493
|
+
## Best Practices
|
|
494
|
+
|
|
495
|
+
### 1. Use Storage for State
|
|
1518
496
|
|
|
1519
497
|
```typescript
|
|
1520
|
-
// ❌ Don't use in-memory state
|
|
498
|
+
// ❌ Don't use in-memory state
|
|
1521
499
|
let cache = {};
|
|
1522
500
|
|
|
1523
|
-
// ✅ Use storage
|
|
501
|
+
// ✅ Use storage
|
|
1524
502
|
await ctx.storage.set('cache', data);
|
|
1525
503
|
```
|
|
1526
504
|
|
|
1527
|
-
###
|
|
505
|
+
### 2. Register Cleanups
|
|
1528
506
|
|
|
1529
507
|
```typescript
|
|
1530
508
|
.onReady(async (ctx, cleanup) => {
|
|
1531
|
-
// ❌
|
|
1532
|
-
const timer = setInterval(
|
|
509
|
+
// ❌ Don't forget to cleanup
|
|
510
|
+
const timer = setInterval(...);
|
|
1533
511
|
|
|
1534
|
-
// ✅
|
|
1535
|
-
const timer = setInterval(
|
|
512
|
+
// ✅ Register cleanup
|
|
513
|
+
const timer = setInterval(...);
|
|
1536
514
|
cleanup(() => clearInterval(timer));
|
|
1537
515
|
|
|
1538
516
|
// ✅ Event subscriptions
|
|
1539
|
-
const sub = ctx.majk.eventBus.conversations().subscribe(
|
|
517
|
+
const sub = ctx.majk.eventBus.conversations().subscribe(...);
|
|
1540
518
|
cleanup(() => sub.unsubscribe());
|
|
1541
519
|
})
|
|
1542
520
|
```
|
|
1543
521
|
|
|
1544
|
-
###
|
|
522
|
+
### 3. Validate Input
|
|
1545
523
|
|
|
1546
524
|
```typescript
|
|
1547
|
-
|
|
1548
|
-
|
|
525
|
+
.apiRoute({
|
|
526
|
+
method: 'POST',
|
|
527
|
+
path: '/api/create',
|
|
528
|
+
name: 'Create Item',
|
|
529
|
+
description: 'Creates a new item. Validates input before processing.',
|
|
530
|
+
handler: async (req, res) => {
|
|
531
|
+
// ✅ Validate input
|
|
532
|
+
if (!req.body?.name) {
|
|
533
|
+
res.status(400).json({ error: 'Name is required' });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
1549
536
|
|
|
1550
|
-
//
|
|
1551
|
-
|
|
537
|
+
// Process...
|
|
538
|
+
}
|
|
539
|
+
})
|
|
1552
540
|
```
|
|
1553
541
|
|
|
1554
|
-
###
|
|
542
|
+
### 4. Use Structured Logging
|
|
1555
543
|
|
|
1556
544
|
```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
|
-
})
|
|
545
|
+
// ❌ Basic logging
|
|
546
|
+
logger.info('User action');
|
|
547
|
+
|
|
548
|
+
// ✅ Structured logging
|
|
549
|
+
logger.info('User action', { userId, action: 'create', resourceId });
|
|
1574
550
|
```
|
|
1575
551
|
|
|
1576
|
-
###
|
|
552
|
+
### 5. Handle Errors Gracefully
|
|
1577
553
|
|
|
1578
554
|
```typescript
|
|
1579
|
-
.
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
555
|
+
.tool('global', spec, async (input, { logger }) => {
|
|
556
|
+
try {
|
|
557
|
+
const result = await processData(input);
|
|
558
|
+
return { success: true, data: result };
|
|
559
|
+
} catch (error) {
|
|
560
|
+
logger.error(`Tool failed: ${error.message}`);
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
error: error.message,
|
|
564
|
+
code: 'PROCESSING_ERROR'
|
|
565
|
+
};
|
|
1590
566
|
}
|
|
1591
567
|
})
|
|
1592
568
|
```
|
|
1593
569
|
|
|
1594
|
-
|
|
570
|
+
## Examples
|
|
1595
571
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
572
|
+
See `example.ts` for a comprehensive example showing:
|
|
573
|
+
- React and HTML screens
|
|
574
|
+
- API routes with parameters
|
|
575
|
+
- Tools in different scopes
|
|
576
|
+
- Entity declarations
|
|
577
|
+
- Config wizard
|
|
578
|
+
- Event subscriptions
|
|
579
|
+
- Storage usage
|
|
580
|
+
- Health checks
|
|
1600
581
|
|
|
1601
|
-
|
|
1602
|
-
.teamMember([{ id: 'assistant', name: 'Assistant', ... }])
|
|
1603
|
-
```
|
|
582
|
+
## TypeScript
|
|
1604
583
|
|
|
1605
|
-
|
|
584
|
+
Full TypeScript support with:
|
|
1606
585
|
|
|
1607
|
-
|
|
586
|
+
```typescript
|
|
587
|
+
import {
|
|
588
|
+
definePlugin,
|
|
589
|
+
FluentBuilder,
|
|
590
|
+
PluginContext,
|
|
591
|
+
RequestLike,
|
|
592
|
+
ResponseLike
|
|
593
|
+
} from '@majk/plugin-kit';
|
|
1608
594
|
|
|
1609
|
-
|
|
595
|
+
// Type-safe plugin ID
|
|
596
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0');
|
|
597
|
+
// ^ Enforces route prefixes
|
|
1610
598
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
599
|
+
// Type-safe routes
|
|
600
|
+
.screenReact({
|
|
601
|
+
route: '/plugin-screens/my-plugin/dashboard'
|
|
602
|
+
// ^^^^^^^^^ Must match plugin ID
|
|
603
|
+
})
|
|
1615
604
|
```
|
|
1616
|
-
Fix: Build your React app before building the plugin.
|
|
1617
605
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
💡 Add .configWizard({ schema: YourSchema, ... })
|
|
1622
|
-
```
|
|
606
|
+
## Troubleshooting
|
|
607
|
+
|
|
608
|
+
### "React app not built"
|
|
1623
609
|
|
|
1624
|
-
**"Screen route must start with /plugin-screens/{id}/"**
|
|
1625
610
|
```
|
|
1626
|
-
❌
|
|
1627
|
-
💡
|
|
611
|
+
❌ Plugin Build Failed: React app not built: /path/to/ui/dist/index.html does not exist
|
|
612
|
+
💡 Suggestion: Run "npm run build" in your UI directory to build the React app
|
|
1628
613
|
```
|
|
1629
614
|
|
|
1630
|
-
|
|
615
|
+
**Fix:** Build your React app before building the plugin.
|
|
1631
616
|
|
|
1632
|
-
|
|
1633
|
-
- Ensure `getCapabilities()` checks if `context` is available
|
|
1634
|
-
- This is handled automatically in v1.1.0+
|
|
617
|
+
### "Duplicate API route"
|
|
1635
618
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
619
|
+
```
|
|
620
|
+
❌ Plugin Build Failed: Duplicate API route: POST /api/data
|
|
621
|
+
```
|
|
1639
622
|
|
|
1640
|
-
|
|
623
|
+
**Fix:** Each route (method + path) must be unique.
|
|
1641
624
|
|
|
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
|
|
625
|
+
### "Description must be 2-3 sentences"
|
|
1647
626
|
|
|
1648
|
-
|
|
627
|
+
```
|
|
628
|
+
❌ Plugin Build Failed: Description for "My Screen" must be 2-3 sentences, found 1 sentences
|
|
629
|
+
💡 Suggestion: Rewrite the description to have 2-3 clear sentences.
|
|
630
|
+
```
|
|
1649
631
|
|
|
1650
|
-
|
|
632
|
+
**Fix:** Write 2-3 complete sentences ending with periods.
|
|
1651
633
|
|
|
1652
|
-
|
|
634
|
+
### "Tool names must be unique"
|
|
1653
635
|
|
|
1654
|
-
|
|
636
|
+
```
|
|
637
|
+
❌ Plugin Build Failed: Duplicate tool name: "analyze"
|
|
638
|
+
```
|
|
1655
639
|
|
|
1656
|
-
|
|
640
|
+
**Fix:** Each tool name must be unique within the plugin.
|
|
1657
641
|
|
|
1658
|
-
|
|
642
|
+
## License
|
|
1659
643
|
|
|
1660
|
-
|
|
644
|
+
MIT
|