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