johankit-runtime 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -0
- package/package.json +29 -0
- package/src/app.ts +18 -0
- package/src/core/event/dispatch.js +31 -0
- package/src/core/mcp/http-server.js +52 -0
- package/src/core/mcp/tools-mcp.js +50 -0
- package/src/core/package.js +79 -0
- package/src/core/parse/cognites.js +22 -0
- package/src/core/parse/hooks.js +37 -0
- package/src/core/parse/middleware.js +35 -0
- package/src/core/parse/predicates.js +19 -0
- package/src/core/parse/routes.js +27 -0
- package/src/core/parse/tools.js +82 -0
- package/src/core/register/decorators.js +150 -0
- package/src/core/register/decorators.test.js +13 -0
- package/src/core/register.js +37 -0
- package/src/core/routes.js +89 -0
- package/src/index.ts +42 -0
- package/src/server.ts +15 -0
- package/src/types.ts +104 -0
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Johankit Runtime
|
|
2
|
+
|
|
3
|
+
A modular runtime built around **events**, **capabilities**, and **contracts**.
|
|
4
|
+
It allows systems to grow by composition instead of coupling, using self-contained packages that are discovered and wired automatically.
|
|
5
|
+
|
|
6
|
+
This project is not a framework in the traditional sense.
|
|
7
|
+
It is an **execution model**.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why This Exists
|
|
12
|
+
|
|
13
|
+
Most systems grow by accumulating integrations:
|
|
14
|
+
- interfaces
|
|
15
|
+
- protocols
|
|
16
|
+
- adapters
|
|
17
|
+
- glue code
|
|
18
|
+
|
|
19
|
+
Johankit Runtime approaches this differently:
|
|
20
|
+
|
|
21
|
+
> Everything is a **package**.
|
|
22
|
+
> Everything meaningful is an **event**, a **route**, or a **callable action**.
|
|
23
|
+
|
|
24
|
+
No central registry.
|
|
25
|
+
No hard dependencies between packages.
|
|
26
|
+
No shared business logic.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Core Concepts
|
|
31
|
+
|
|
32
|
+
### Packages
|
|
33
|
+
|
|
34
|
+
A package is an isolated unit of behavior living inside a workspace directory.
|
|
35
|
+
|
|
36
|
+
A package may declare:
|
|
37
|
+
- routes
|
|
38
|
+
- event hooks
|
|
39
|
+
- callable tools
|
|
40
|
+
- predicates
|
|
41
|
+
- middleware
|
|
42
|
+
- startup logic
|
|
43
|
+
|
|
44
|
+
Anything not explicitly declared is ignored.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Discovery
|
|
49
|
+
|
|
50
|
+
At startup, the runtime:
|
|
51
|
+
1. Scans the workspace
|
|
52
|
+
2. Loads packages
|
|
53
|
+
3. Inspects exported functions
|
|
54
|
+
4. Registers only what is explicitly annotated
|
|
55
|
+
|
|
56
|
+
There is no manual wiring.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Declarative Annotations
|
|
61
|
+
|
|
62
|
+
Behavior is declared using structured comments.
|
|
63
|
+
|
|
64
|
+
**Important rule: annotations must be inside the function body.**
|
|
65
|
+
|
|
66
|
+
This is intentional.
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
function example() {
|
|
70
|
+
/**
|
|
71
|
+
* @register_hook something
|
|
72
|
+
*/
|
|
73
|
+
}
|
|
74
|
+
````
|
|
75
|
+
|
|
76
|
+
Annotations outside the function will not be detected.
|
|
77
|
+
|
|
78
|
+
This makes annotations part of the behavior, not part of the language.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Execution Primitives
|
|
83
|
+
|
|
84
|
+
### Events (Hooks)
|
|
85
|
+
|
|
86
|
+
Events represent something that happened.
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
/**
|
|
90
|
+
* @register_hook user_created
|
|
91
|
+
*/
|
|
92
|
+
function enrich(payload) {
|
|
93
|
+
return { ...payload, enriched: true }
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
* Events are broadcast to all packages
|
|
98
|
+
* Hooks run sequentially
|
|
99
|
+
* Returned values replace the payload
|
|
100
|
+
* `null` or `undefined` are ignored
|
|
101
|
+
* Failures do not stop execution
|
|
102
|
+
|
|
103
|
+
Events are **data pipelines**, not notifications.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### Routes
|
|
108
|
+
|
|
109
|
+
Routes expose request handlers automatically.
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
function status() {
|
|
113
|
+
/**
|
|
114
|
+
* @register_router status
|
|
115
|
+
* @method GET
|
|
116
|
+
*/
|
|
117
|
+
return { ok: true }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Routes are namespaced by package name and require no manual registration.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### Tools
|
|
126
|
+
|
|
127
|
+
Tools are structured, callable actions.
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
function calculate() {
|
|
131
|
+
/**
|
|
132
|
+
* @register_tool calculate
|
|
133
|
+
* @description Performs a calculation
|
|
134
|
+
* @param {number} x - first value
|
|
135
|
+
* @param {number?} y - optional value
|
|
136
|
+
*/
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Tools:
|
|
141
|
+
|
|
142
|
+
* expose their input schema
|
|
143
|
+
* can be listed dynamically
|
|
144
|
+
* can be invoked programmatically
|
|
145
|
+
* can be exposed through control interfaces
|
|
146
|
+
|
|
147
|
+
They are ideal for automation and orchestration.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Predicates
|
|
152
|
+
|
|
153
|
+
Predicates are named boolean checks.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
function isAdmin() {
|
|
157
|
+
/**
|
|
158
|
+
* @register_predicate is_admin
|
|
159
|
+
*/
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
They do nothing by themselves, but enable conditional execution elsewhere.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### Middleware
|
|
169
|
+
|
|
170
|
+
Middleware can be conditionally enabled using predicates.
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
function guard() {
|
|
174
|
+
/**
|
|
175
|
+
* @register_middleware access_guard
|
|
176
|
+
* @predicate is_admin
|
|
177
|
+
*/
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Only predicates that evaluate to `true` activate the middleware.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Conditions
|
|
186
|
+
|
|
187
|
+
Many declarations support optional conditions:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
@only web
|
|
191
|
+
@never mobile
|
|
192
|
+
@when authenticated
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Conditions are **metadata**, not enforcement rules.
|
|
196
|
+
They allow higher-level orchestration without hardcoding logic.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Event Dispatching
|
|
201
|
+
|
|
202
|
+
Events are dispatched explicitly:
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
dispatchEvent('user_created', payload)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The runtime:
|
|
209
|
+
|
|
210
|
+
* loads all packages
|
|
211
|
+
* finds matching hooks
|
|
212
|
+
* executes them safely
|
|
213
|
+
* returns the final payload
|
|
214
|
+
|
|
215
|
+
This enables cross-package collaboration without coupling.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Tool Control Interface (MCP-style)
|
|
220
|
+
|
|
221
|
+
The runtime can expose tools through a standardized control surface.
|
|
222
|
+
|
|
223
|
+
Capabilities:
|
|
224
|
+
|
|
225
|
+
* list tools
|
|
226
|
+
* inspect schemas
|
|
227
|
+
* invoke tools dynamically
|
|
228
|
+
|
|
229
|
+
This makes the runtime suitable for:
|
|
230
|
+
|
|
231
|
+
* automation systems
|
|
232
|
+
* agents
|
|
233
|
+
* orchestration layers
|
|
234
|
+
* external controllers
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Bootstrapping
|
|
239
|
+
|
|
240
|
+
The runtime is started by providing a workspace and optional interfaces.
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
bootstrap({
|
|
244
|
+
workspace: './packages',
|
|
245
|
+
http: { enabled: true },
|
|
246
|
+
mcp: { enabled: true }
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The bootstrap process:
|
|
251
|
+
|
|
252
|
+
* resolves the workspace
|
|
253
|
+
* loads all packages
|
|
254
|
+
* initializes declared interfaces
|
|
255
|
+
* returns references to running components
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Design Principles
|
|
260
|
+
|
|
261
|
+
* Explicit behavior over magic
|
|
262
|
+
* Convention over configuration
|
|
263
|
+
* Isolation between packages
|
|
264
|
+
* Protocol-agnostic core
|
|
265
|
+
* Graceful failure handling
|
|
266
|
+
* Composition over inheritance
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## What Fits Naturally
|
|
271
|
+
|
|
272
|
+
Because the runtime is protocol-agnostic, it adapts well to:
|
|
273
|
+
|
|
274
|
+
* real-time channels
|
|
275
|
+
* background processing
|
|
276
|
+
* schedulers
|
|
277
|
+
* message streams
|
|
278
|
+
* file watchers
|
|
279
|
+
* control interfaces
|
|
280
|
+
* agents and decision systems
|
|
281
|
+
|
|
282
|
+
All of these are just **event translators**.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## What This Is Not
|
|
287
|
+
|
|
288
|
+
* Not a monolithic framework
|
|
289
|
+
* Not a dependency injection container
|
|
290
|
+
* Not a plugin marketplace
|
|
291
|
+
* Not a DSL
|
|
292
|
+
|
|
293
|
+
It is a **runtime contract**.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Project Status
|
|
298
|
+
|
|
299
|
+
Early-stage, evolving by usage.
|
|
300
|
+
APIs are intentionally minimal and may evolve as patterns emerge.
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "johankit-runtime",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A pluggable runtime for any back-end",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"test:coverage": "vitest run --coverage"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"express": "^5.2.1",
|
|
17
|
+
"js-yaml": "^4.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"vitest": "latest",
|
|
21
|
+
"@vitest/coverage-v8": "latest",
|
|
22
|
+
"typescript": "^5.4.0",
|
|
23
|
+
"@types/node": "^20.0.0",
|
|
24
|
+
"@types/express": "^4.17.21"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"express": "^4.x"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import express, { Application } from 'express'
|
|
2
|
+
import { registerPackages } from './core/register'
|
|
3
|
+
|
|
4
|
+
type JohankitApp = Application & {
|
|
5
|
+
setup: () => Promise<void>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const app = express() as Application
|
|
9
|
+
|
|
10
|
+
;(app as JohankitApp).setup = async () => {
|
|
11
|
+
try {
|
|
12
|
+
await registerPackages(app)
|
|
13
|
+
} catch (error: any) {
|
|
14
|
+
console.log(`Error registering packages: ${error.message}`, 'error')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default app as JohankitApp
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { registerHooks } = require('../register/decorators');
|
|
2
|
+
const { getPackages } = require('../package');
|
|
3
|
+
|
|
4
|
+
async function dispatchEvent(eventName, payload) {
|
|
5
|
+
const packages = await getPackages();
|
|
6
|
+
let finalResult = payload;
|
|
7
|
+
|
|
8
|
+
for (const pkg of packages) {
|
|
9
|
+
try {
|
|
10
|
+
const hooks = await registerHooks(pkg.folder);
|
|
11
|
+
const relevant = hooks.filter(h => h.event === eventName);
|
|
12
|
+
|
|
13
|
+
for (const hook of relevant) {
|
|
14
|
+
try {
|
|
15
|
+
const result = await hook.call(finalResult);
|
|
16
|
+
if (result !== undefined && result !== null) {
|
|
17
|
+
finalResult = result;
|
|
18
|
+
}
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.warn(`[Hook Error] Package ${pkg.folder} failed on event ${eventName}:`, err.message);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return finalResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { dispatchEvent };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const bodyParser = require('body-parser');
|
|
3
|
+
const { bootstrapMcp } = require('./tools-mcp');
|
|
4
|
+
|
|
5
|
+
async function createMcpHttpServer(options = {}) {
|
|
6
|
+
const app = express();
|
|
7
|
+
const port = options.port || 3333;
|
|
8
|
+
const workspace = options.workspace || process.env.PACKAGES_PATH;
|
|
9
|
+
|
|
10
|
+
if (!workspace) {
|
|
11
|
+
throw new Error('PACKAGES_PATH or workspace option is required');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const mcp = await bootstrapMcp(workspace);
|
|
15
|
+
|
|
16
|
+
app.use(bodyParser.json());
|
|
17
|
+
|
|
18
|
+
app.get('/mcp', (req, res) => {
|
|
19
|
+
res.json({ protocol: mcp.protocol, version: mcp.version });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.get('/mcp/tools', (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
res.json(mcp.listTools());
|
|
25
|
+
} catch (err) {
|
|
26
|
+
res.status(500).json({ error: err.message });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.post('/mcp/call', async (req, res) => {
|
|
31
|
+
const { name, args } = req.body || {};
|
|
32
|
+
|
|
33
|
+
if (!name) {
|
|
34
|
+
return res.status(400).json({ error: 'Tool name is required' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await mcp.callTool(name, args || {});
|
|
39
|
+
res.json({ result });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
res.status(500).json({ error: err.message });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const server = app.listen(port, () => {
|
|
46
|
+
console.log(`MCP HTTP server running on port ${port}`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return { app, server, mcp };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { createMcpHttpServer };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { registerTools } = require('../register/decorators');
|
|
2
|
+
|
|
3
|
+
function toolToMcp(tool) {
|
|
4
|
+
return {
|
|
5
|
+
name: tool.name,
|
|
6
|
+
description: tool.description || '',
|
|
7
|
+
inputSchema: tool.parameters || { type: 'object', properties: {} },
|
|
8
|
+
handler: async (args) => {
|
|
9
|
+
return await tool.call(args);
|
|
10
|
+
},
|
|
11
|
+
condition: tool.condition || null
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function loadMcpTools(agentFolder) {
|
|
16
|
+
const tools = await registerTools(agentFolder);
|
|
17
|
+
return tools.map(toolToMcp);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createMcpServer(tools) {
|
|
21
|
+
return {
|
|
22
|
+
protocol: 'mcp',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
listTools() {
|
|
25
|
+
return tools.map(t => ({
|
|
26
|
+
name: t.name,
|
|
27
|
+
description: t.description,
|
|
28
|
+
inputSchema: t.inputSchema
|
|
29
|
+
}));
|
|
30
|
+
},
|
|
31
|
+
async callTool(name, args) {
|
|
32
|
+
const tool = tools.find(t => t.name === name);
|
|
33
|
+
if (!tool) {
|
|
34
|
+
throw new Error(`Tool not found: ${name}`);
|
|
35
|
+
}
|
|
36
|
+
return await tool.handler(args);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function bootstrapMcp(agentFolder) {
|
|
42
|
+
const tools = await loadMcpTools(agentFolder);
|
|
43
|
+
return createMcpServer(tools);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
loadMcpTools,
|
|
48
|
+
createMcpServer,
|
|
49
|
+
bootstrapMcp
|
|
50
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { log } = console;
|
|
5
|
+
|
|
6
|
+
async function getPackage(folderName) {
|
|
7
|
+
try {
|
|
8
|
+
const pluginsPath = process.env.PACKAGES_PATH || path.join(__dirname, '..', 'packages');
|
|
9
|
+
const manifestPath = path.join(pluginsPath, folderName, 'package.json');
|
|
10
|
+
|
|
11
|
+
let manifest = {};
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
if (fs.existsSync(manifestPath)) {
|
|
15
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(`Manifest not found in ${folderName}:`, error);
|
|
19
|
+
throw new Error(`Manifest not found in folder ${folderName}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
...manifest,
|
|
24
|
+
folder: folderName,
|
|
25
|
+
};
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function getPackages() {
|
|
32
|
+
try {
|
|
33
|
+
const pluginsPath = process.env.PACKAGES_PATH || path.join(__dirname, '..', 'packages');
|
|
34
|
+
const pluginFolders = fs.readdirSync(pluginsPath).filter(f => fs.statSync(path.join(pluginsPath, f)).isDirectory());
|
|
35
|
+
|
|
36
|
+
const folderPackages = await Promise.all(pluginFolders.map(async folderName => {
|
|
37
|
+
return await getPackage(folderName);
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
return folderPackages;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const registerPackage = async (app, pkg) => {
|
|
47
|
+
const basePackagesPath = process.env.PACKAGES_PATH || path.join(__dirname, '..', 'packages');
|
|
48
|
+
const packageDir = path.join(basePackagesPath, pkg.folder);
|
|
49
|
+
const manifestPath = path.join(packageDir, 'package.json');
|
|
50
|
+
pkg.dir = packageDir;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (!fs.existsSync(manifestPath)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(path.join(packageDir, 'routes.js'))) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
62
|
+
const usePackage = manifest.entry ? require(path.join(packageDir, manifest.entry)) : () => { };
|
|
63
|
+
|
|
64
|
+
if (typeof usePackage !== 'function') {
|
|
65
|
+
log(`${pkg.name} invalid entry point. Expected a function.`, 'error');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const packg = usePackage(app) ?? usePackage;
|
|
70
|
+
packg?.setup && packg.setup();
|
|
71
|
+
|
|
72
|
+
log(`${pkg.name} ${pkg.version ? `version ${pkg.version}` : ''} is running.\n`);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
log(`Error with package ${pkg.name}: ${error.message}`, 'error');
|
|
75
|
+
log(`${pkg.name} ${pkg.version ? `version ${pkg.version}` : ''} isn't running.\n`);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
module.exports = { getPackage, getPackages, registerPackage };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function parseCognite(fn) {
|
|
2
|
+
if (typeof fn !== 'function') return null;
|
|
3
|
+
|
|
4
|
+
const fnStr = fn.toString();
|
|
5
|
+
|
|
6
|
+
const docMatch = fnStr.match(/\/\*\*([\s\S]*?)\*\//);
|
|
7
|
+
if (!docMatch) return null;
|
|
8
|
+
|
|
9
|
+
const doc = docMatch[1];
|
|
10
|
+
|
|
11
|
+
const cogMatch = doc.match(/@register_cognite/);
|
|
12
|
+
if (!cogMatch) return null;
|
|
13
|
+
|
|
14
|
+
const name = fn.name || "anonymous";
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
execute: fn
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { parseCognite };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function parseHook(fn) {
|
|
2
|
+
if (typeof fn !== 'function') return null;
|
|
3
|
+
|
|
4
|
+
const fnStr = fn.toString();
|
|
5
|
+
const docMatch = fnStr.match(/\/\*\*([\s\S]*?)\*\//);
|
|
6
|
+
if (!docMatch) return null;
|
|
7
|
+
|
|
8
|
+
const doc = docMatch[1];
|
|
9
|
+
|
|
10
|
+
const hookMatch = doc.match(/@register_hook\s+([^\s]+)/);
|
|
11
|
+
if (!hookMatch) return null;
|
|
12
|
+
|
|
13
|
+
const event = hookMatch[1].trim();
|
|
14
|
+
|
|
15
|
+
const condition = { only: [], never: [], when: [] };
|
|
16
|
+
|
|
17
|
+
const onlyMatches = [...doc.matchAll(/@only\s+([^\s]+)/g)];
|
|
18
|
+
onlyMatches.forEach(m => condition.only.push(m[1]));
|
|
19
|
+
|
|
20
|
+
const neverMatches = [...doc.matchAll(/@never\s+([^\s]+)/g)];
|
|
21
|
+
neverMatches.forEach(m => condition.never.push(m[1]));
|
|
22
|
+
|
|
23
|
+
const whenMatches = [...doc.matchAll(/@when\s+([^\s]+)/g)];
|
|
24
|
+
whenMatches.forEach(m => condition.when.push(m[1]));
|
|
25
|
+
|
|
26
|
+
Object.keys(condition).forEach(k => {
|
|
27
|
+
if (condition[k].length === 0) delete condition[k];
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
event,
|
|
32
|
+
call: fn,
|
|
33
|
+
condition
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { parseHook };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function parseMiddleware(fn) {
|
|
2
|
+
if (typeof fn !== 'function') return null;
|
|
3
|
+
|
|
4
|
+
const fnStr = fn.toString();
|
|
5
|
+
|
|
6
|
+
// 1. Extrai o corpo da função (entre { })
|
|
7
|
+
const bodyMatch = fnStr.match(/\{([\s\S]*)\}$/);
|
|
8
|
+
if (!bodyMatch) return null;
|
|
9
|
+
|
|
10
|
+
const body = bodyMatch[1];
|
|
11
|
+
|
|
12
|
+
// 2. Procura SOMENTE JSDoc dentro do corpo
|
|
13
|
+
const docMatch = body.match(/\/\*\*([\s\S]*?)\*\//);
|
|
14
|
+
if (!docMatch) return null;
|
|
15
|
+
|
|
16
|
+
const doc = docMatch[1];
|
|
17
|
+
|
|
18
|
+
// 3. Exige explicitamente o decorator
|
|
19
|
+
const registerMatch = doc.match(/@register_middleware\s+(\w+)/);
|
|
20
|
+
if (!registerMatch) return null;
|
|
21
|
+
|
|
22
|
+
const name = registerMatch[1];
|
|
23
|
+
|
|
24
|
+
// 4. Predicates (somente os declarados ali)
|
|
25
|
+
const predicates = [...doc.matchAll(/@predicate\s+(\w+)/g)]
|
|
26
|
+
.map(m => m[1]);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
call: fn,
|
|
31
|
+
predicates
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { parseMiddleware };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function parsePredicate(fn) {
|
|
2
|
+
if (typeof fn !== 'function') return null;
|
|
3
|
+
|
|
4
|
+
const fnStr = fn.toString();
|
|
5
|
+
const docMatch = fnStr.match(/\/\*\*([\s\S]*?)\*\//);
|
|
6
|
+
if (!docMatch) return null;
|
|
7
|
+
|
|
8
|
+
const doc = docMatch[1];
|
|
9
|
+
|
|
10
|
+
const match = doc.match(/@register_predicate\s*(\w+)?/);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
|
|
13
|
+
const name = match[1] || fn.name || null;
|
|
14
|
+
if (!name) return null;
|
|
15
|
+
|
|
16
|
+
return { name, call: fn };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { parsePredicate };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function parseRouter(fn) {
|
|
2
|
+
if (typeof fn !== 'function') return null;
|
|
3
|
+
|
|
4
|
+
const fnStr = fn.toString();
|
|
5
|
+
|
|
6
|
+
const docMatch = fnStr.match(/\/\*\*([\s\S]*?)\*\//);
|
|
7
|
+
if (!docMatch) return null;
|
|
8
|
+
|
|
9
|
+
const doc = docMatch[1];
|
|
10
|
+
|
|
11
|
+
const routeMatch = doc.match(/@register_router\s+([^\s]+)/);
|
|
12
|
+
if (!routeMatch) return null;
|
|
13
|
+
|
|
14
|
+
const path = routeMatch[1].trim();
|
|
15
|
+
|
|
16
|
+
const methodMatch = doc.match(/@method\s+([^\s]+)/);
|
|
17
|
+
const method = methodMatch ? methodMatch[1].trim().toLowerCase() : "get";
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
path,
|
|
21
|
+
method,
|
|
22
|
+
handler: fn
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { parseRouter };
|
|
27
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function parseTool(fn) {
|
|
2
|
+
const meta = {
|
|
3
|
+
name: fn.name || null,
|
|
4
|
+
description: null,
|
|
5
|
+
parameters: [],
|
|
6
|
+
call: fn,
|
|
7
|
+
condition: { only: [], never: [], when: [] }
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const fnString = fn.toString();
|
|
11
|
+
const jsdocMatch = fnString.match(/\/\*\*([\s\S]*?)\*\//);
|
|
12
|
+
if (!jsdocMatch) return null;
|
|
13
|
+
|
|
14
|
+
const lines = jsdocMatch[1].split('\n').map(l => l.trim().replace(/^\*\s?/, ''));
|
|
15
|
+
|
|
16
|
+
const registerLine = lines.find(l => l.startsWith('@register_tool'));
|
|
17
|
+
if (!registerLine) return null;
|
|
18
|
+
|
|
19
|
+
const registerMatch = registerLine.match(/@register_tool\s*(\S+)?/);
|
|
20
|
+
if (registerMatch && registerMatch[1]) {
|
|
21
|
+
meta.name = registerMatch[1];
|
|
22
|
+
} else if (!meta.name) {
|
|
23
|
+
meta.name = 'anonymousTool';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const paramMap = {};
|
|
27
|
+
|
|
28
|
+
const descriptionLine = lines.find(l => l.startsWith('@description'));
|
|
29
|
+
if (descriptionLine) meta.description = descriptionLine.replace('@description', '').trim();
|
|
30
|
+
|
|
31
|
+
lines.forEach(line => {
|
|
32
|
+
if (line.startsWith('@param')) {
|
|
33
|
+
const match = line.match(/@param {([\w\[\]\?]+)} (\S+) - (.+)/);
|
|
34
|
+
if (!match) return;
|
|
35
|
+
|
|
36
|
+
let [_, type, name, description] = match;
|
|
37
|
+
let required = !type.endsWith('?');
|
|
38
|
+
if (!required) type = type.slice(0, -1);
|
|
39
|
+
|
|
40
|
+
const cleanName = name.replace(/\[\]/g, '');
|
|
41
|
+
const isArray = name.includes('[]');
|
|
42
|
+
const isNested = cleanName.includes('.');
|
|
43
|
+
|
|
44
|
+
if (isNested) {
|
|
45
|
+
const parentName = cleanName.split('.')[0];
|
|
46
|
+
if (!paramMap[parentName]) {
|
|
47
|
+
const parent = {
|
|
48
|
+
name: parentName,
|
|
49
|
+
type: isArray ? 'array' : 'object',
|
|
50
|
+
description: `${parentName} object`,
|
|
51
|
+
required
|
|
52
|
+
};
|
|
53
|
+
meta.parameters.push(parent);
|
|
54
|
+
paramMap[parentName] = parent;
|
|
55
|
+
} else if (required) {
|
|
56
|
+
paramMap[parentName].required = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const param = { name, type, description, required };
|
|
61
|
+
meta.parameters.push(param);
|
|
62
|
+
paramMap[name] = param;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const onlyMatch = line.match(/@only\s+([^\s]+)/);
|
|
66
|
+
if (onlyMatch) meta.condition.only.push(onlyMatch[1]);
|
|
67
|
+
|
|
68
|
+
const neverMatch = line.match(/@never\s+([^\s]+)/);
|
|
69
|
+
if (neverMatch) meta.condition.never.push(neverMatch[1]);
|
|
70
|
+
|
|
71
|
+
const whenMatch = line.match(/@when\s+([^\s]+)/);
|
|
72
|
+
if (whenMatch) meta.condition.when.push(whenMatch[1]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
Object.keys(meta.condition).forEach(k => {
|
|
76
|
+
if (meta.condition[k].length === 0) delete meta.condition[k];
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return meta;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { parseTool };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
|
|
4
|
+
const { parseTool } = require('../parse/tools');
|
|
5
|
+
const { parseHook } = require('../parse/hooks');
|
|
6
|
+
const { parseCognite } = require('../parse/cognites');
|
|
7
|
+
const { parseRouter } = require('../parse/routes');
|
|
8
|
+
const { parsePredicate } = require('../parse/predicates');
|
|
9
|
+
const { parseMiddleware } = require('../parse/middleware');
|
|
10
|
+
|
|
11
|
+
async function getJsFilesRecursively(dir) {
|
|
12
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
13
|
+
const tasks = entries.map(async entry => {
|
|
14
|
+
const fullPath = path.join(dir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) return getJsFilesRecursively(fullPath);
|
|
16
|
+
if (entry.isFile() && entry.name.endsWith('.js')) return [fullPath];
|
|
17
|
+
return [];
|
|
18
|
+
});
|
|
19
|
+
const results = await Promise.all(tasks);
|
|
20
|
+
return results.flat();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function isRelevantFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
26
|
+
return content.includes('@register_');
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractExportedFunctions(mod) {
|
|
33
|
+
if (typeof mod === 'function') return [mod];
|
|
34
|
+
if (typeof mod === 'object' && mod !== null) return Object.values(mod);
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function registerGeneric(agentFolder, parser, mapFn) {
|
|
39
|
+
const baseDir = process.env.PACKAGES_PATH || path.resolve(__dirname, '..', '..', 'packages');
|
|
40
|
+
|
|
41
|
+
let folderPath;
|
|
42
|
+
if (path.isAbsolute(String(agentFolder))) {
|
|
43
|
+
folderPath = agentFolder;
|
|
44
|
+
} else if (String(agentFolder).includes('packages')) {
|
|
45
|
+
const folderName = path.basename(agentFolder);
|
|
46
|
+
folderPath = path.join(baseDir, folderName);
|
|
47
|
+
} else {
|
|
48
|
+
folderPath = path.join(baseDir, String(agentFolder));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(folderPath);
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error(`Folder not found: ${folderPath}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const files = await getJsFilesRecursively(folderPath);
|
|
58
|
+
|
|
59
|
+
const results = await Promise.all(
|
|
60
|
+
files.map(async filePath => {
|
|
61
|
+
if (!(await isRelevantFile(filePath))) return [];
|
|
62
|
+
try {
|
|
63
|
+
const absolutePath = path.resolve(filePath);
|
|
64
|
+
delete require.cache[require.resolve(absolutePath)];
|
|
65
|
+
const mod = require(absolutePath);
|
|
66
|
+
|
|
67
|
+
return extractExportedFunctions(mod)
|
|
68
|
+
.map(fn => {
|
|
69
|
+
const meta = parser(fn);
|
|
70
|
+
if (!meta) return null;
|
|
71
|
+
return mapFn(meta);
|
|
72
|
+
})
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.warn(`Erro ao registrar em ${filePath}:`, err.message);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return results.flat();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function registerTools(agentFolder) {
|
|
85
|
+
return registerGeneric(agentFolder, parseTool, meta => {
|
|
86
|
+
const tool = {
|
|
87
|
+
name: meta.name,
|
|
88
|
+
description: meta.description,
|
|
89
|
+
parameters: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: (meta.parameters || []).reduce((acc, p) => {
|
|
92
|
+
acc[p.name] = { type: p.type === 'array' ? 'array' : p.type, description: p.description };
|
|
93
|
+
if (p.type === 'array') acc[p.name].items = { type: 'object' };
|
|
94
|
+
return acc;
|
|
95
|
+
}, {}),
|
|
96
|
+
required: (meta.parameters || []).filter(p => p.required).map(p => p.name),
|
|
97
|
+
},
|
|
98
|
+
call: meta.call,
|
|
99
|
+
};
|
|
100
|
+
if (meta.condition) tool.condition = meta.condition;
|
|
101
|
+
return tool;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function registerHooks(agentFolder) {
|
|
106
|
+
return registerGeneric(agentFolder, parseHook, meta => {
|
|
107
|
+
const hook = { event: meta.event, call: meta.call };
|
|
108
|
+
if (meta.condition) hook.condition = meta.condition;
|
|
109
|
+
return hook;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function registerCognites(agentFolder) {
|
|
114
|
+
return registerGeneric(agentFolder, parseCognite, meta => ({
|
|
115
|
+
name: meta.name,
|
|
116
|
+
description: meta.description,
|
|
117
|
+
execute: meta.execute,
|
|
118
|
+
options: meta.options || {},
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function registerRoutes(agentFolder) {
|
|
123
|
+
return registerGeneric(agentFolder, parseRouter, meta => ({
|
|
124
|
+
path: meta.path,
|
|
125
|
+
method: meta.method || 'get',
|
|
126
|
+
handler: meta.handler,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function registerPredicates(agentFolder) {
|
|
131
|
+
const preds = await registerGeneric(agentFolder, parsePredicate, meta => meta);
|
|
132
|
+
return Object.fromEntries(preds.map(p => [p.name, p.call]));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function registerMiddlewares(agentFolder) {
|
|
136
|
+
return registerGeneric(agentFolder, parseMiddleware, meta => ({
|
|
137
|
+
name: meta.name,
|
|
138
|
+
call: meta.call,
|
|
139
|
+
predicates: meta.predicates || []
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
registerTools,
|
|
145
|
+
registerHooks,
|
|
146
|
+
registerCognites,
|
|
147
|
+
registerRoutes,
|
|
148
|
+
registerPredicates,
|
|
149
|
+
registerMiddlewares
|
|
150
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const { registerTools } = require('./decorators');
|
|
5
|
+
|
|
6
|
+
vi.mock('fs/promises');
|
|
7
|
+
|
|
8
|
+
describe('decorators register', () => {
|
|
9
|
+
it('should throw error if folder does not exist', async () => {
|
|
10
|
+
fs.access.mockRejectedValue(new Error());
|
|
11
|
+
await expect(registerTools('invalid')).rejects.toThrow('Folder not found');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
|
|
5
|
+
const { getPackages, registerPackage } = require('./package');
|
|
6
|
+
const { registerRoutes } = require('./routes');
|
|
7
|
+
const { log } = console;
|
|
8
|
+
|
|
9
|
+
const registerPackages = async (app) => {
|
|
10
|
+
try {
|
|
11
|
+
const packagesList = await getPackages();
|
|
12
|
+
|
|
13
|
+
for (const pkg of packagesList) {
|
|
14
|
+
const ymlPath = path.join(__dirname, '..', 'packages', pkg.folder, 'agent.yml');
|
|
15
|
+
|
|
16
|
+
const exists = fs.existsSync(ymlPath);
|
|
17
|
+
if (exists) {
|
|
18
|
+
try {
|
|
19
|
+
const fileContents = fs.readFileSync(ymlPath, 'utf8');
|
|
20
|
+
const ymlObject = yaml.load(fileContents);
|
|
21
|
+
pkg.config = ymlObject;
|
|
22
|
+
|
|
23
|
+
log(`Loaded YAML for package: ${JSON.stringify(pkg.config)}`);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
log(`Error reading YAML for ${pkg.folder}: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await registerRoutes(app, pkg);
|
|
30
|
+
await registerPackage(app, pkg);
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
log(JSON.stringify(err), "red");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = { registerPackages };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { registerRoutes: routesDecorators, registerCognites, registerPredicates, registerMiddlewares } = require("./register/decorators");
|
|
4
|
+
|
|
5
|
+
const { log } = console;
|
|
6
|
+
|
|
7
|
+
const getRoutes = () => {
|
|
8
|
+
const routes = [];
|
|
9
|
+
const baseDir = process.env.PACKAGES_PATH || path.resolve(__dirname, '..', 'packages');
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
fs.readdirSync(baseDir).forEach(packageDir => {
|
|
13
|
+
const routesPath = path.join(baseDir, packageDir, 'routes.js');
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(routesPath)) {
|
|
16
|
+
const routeModule = require(path.resolve(routesPath));
|
|
17
|
+
routeModule.map(router => router.dir = packageDir);
|
|
18
|
+
|
|
19
|
+
routes.push(...routeModule);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error("Error with Routes:", error);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return routes;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const registerRoutes = async (app, pkg) => {
|
|
31
|
+
const basePackagesPath = process.env.PACKAGES_PATH || path.resolve(__dirname, '..', 'packages');
|
|
32
|
+
const packageDir = path.join(basePackagesPath, pkg.folder);
|
|
33
|
+
const routesRegex = /^routes\.(js|tsx)$/;
|
|
34
|
+
const files = fs.readdirSync(packageDir);
|
|
35
|
+
const routesFile = files.find((file) => routesRegex.test(file));
|
|
36
|
+
|
|
37
|
+
const _routesDecorators = await routesDecorators(pkg.folder);
|
|
38
|
+
|
|
39
|
+
const predicates = await registerPredicates(pkg.folder);
|
|
40
|
+
const middlewares = await registerMiddlewares(pkg.folder);
|
|
41
|
+
|
|
42
|
+
let allow_middlewares = [];
|
|
43
|
+
|
|
44
|
+
middlewares.map(middleware => {
|
|
45
|
+
middleware.predicates.map(predicate => {
|
|
46
|
+
if (predicates[predicate]) {
|
|
47
|
+
try {
|
|
48
|
+
if ( predicates[predicate]() ) {
|
|
49
|
+
allow_middlewares.push( middleware.call );
|
|
50
|
+
}
|
|
51
|
+
} catch(err) {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let _routesFile = [];
|
|
58
|
+
|
|
59
|
+
const allRoutes = [..._routesDecorators, ..._routesFile];
|
|
60
|
+
|
|
61
|
+
if (allRoutes.length === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
allRoutes.forEach((route) => {
|
|
66
|
+
if (route.handler && route.method && route.path) {
|
|
67
|
+
app[route.method](`/${pkg.folder}/${route.path}`, [...allow_middlewares, async (req, res, next) => {
|
|
68
|
+
try {
|
|
69
|
+
const payload = await route.handler(req, res, next);
|
|
70
|
+
const { status = 200, data = payload } = payload || {};
|
|
71
|
+
res.status(status).json(data);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
next(err);
|
|
74
|
+
}
|
|
75
|
+
}]);
|
|
76
|
+
|
|
77
|
+
log(`${pkg.name ?? pkg.folder} registered decorator route [${route.method.toUpperCase()}] /${pkg.folder}/${route.path}`, 'gray');
|
|
78
|
+
}
|
|
79
|
+
else if (route.call && route.method && route.router) {
|
|
80
|
+
app[route.method](`/${pkg.folder}/${route.router}`, route.call);
|
|
81
|
+
log(`${pkg.name} registered file route [${route.method.toUpperCase()}] /${pkg.folder}/${route.router}`, 'gray');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
log(`Invalid route configuration for ${pkg.name}: ${JSON.stringify(route)}`, 'error');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
module.exports = { getRoutes, registerRoutes }
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import http from 'http'
|
|
3
|
+
import app from './app'
|
|
4
|
+
import { dispatchEvent } from './core/event/dispatch'
|
|
5
|
+
import type { BootstrapConfig, JohankitApp } from './types'
|
|
6
|
+
|
|
7
|
+
export async function bootstrap(config: BootstrapConfig = {}) {
|
|
8
|
+
const customPath = config.workspace || path.join(__dirname, 'packages')
|
|
9
|
+
const resolvedWorkspace = path.isAbsolute(customPath)
|
|
10
|
+
? customPath
|
|
11
|
+
: path.resolve(process.cwd(), customPath)
|
|
12
|
+
|
|
13
|
+
process.env.PACKAGES_PATH = resolvedWorkspace
|
|
14
|
+
|
|
15
|
+
const result: any = {}
|
|
16
|
+
|
|
17
|
+
await app.setup()
|
|
18
|
+
result.app = app as JohankitApp
|
|
19
|
+
|
|
20
|
+
if (config.mcp?.enabled) {
|
|
21
|
+
const { bootstrapMcp } = require('./core/mcp/tools-mcp')
|
|
22
|
+
result.mcp = await bootstrapMcp(resolvedWorkspace)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (config.mcp?.http?.enabled) {
|
|
26
|
+
const { createMcpHttpServer } = require('./core/mcp/http-server')
|
|
27
|
+
result.mcpHttp = await createMcpHttpServer({
|
|
28
|
+
workspace: resolvedWorkspace,
|
|
29
|
+
port: config.mcp.http.port
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (config.http?.enabled !== false) {
|
|
34
|
+
const port = config.http?.port || (process.env.PORT ? Number(process.env.PORT) : 5040)
|
|
35
|
+
result.server = http.createServer(app)
|
|
36
|
+
result.server.listen(port)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { dispatchEvent }
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import http from 'http'
|
|
2
|
+
import app from './app'
|
|
3
|
+
|
|
4
|
+
const port = process.env.PORT ? Number(process.env.PORT) : 5040
|
|
5
|
+
|
|
6
|
+
app
|
|
7
|
+
.setup()
|
|
8
|
+
.then(() => {
|
|
9
|
+
http.createServer(app).listen(port, () => {
|
|
10
|
+
console.log(`Server is ready on port: ${port}.\n`)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
.catch(err => {
|
|
14
|
+
console.error('Error during app setup:', err)
|
|
15
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Application, Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export interface BootstrapConfig {
|
|
4
|
+
workspace?: string;
|
|
5
|
+
http?: {
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
port?: number;
|
|
8
|
+
};
|
|
9
|
+
mcp?: {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
http?: {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
port?: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RoutePayload {
|
|
19
|
+
status?: number;
|
|
20
|
+
data?: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type RouteHandler = (
|
|
24
|
+
req: Request,
|
|
25
|
+
res: Response,
|
|
26
|
+
next: NextFunction
|
|
27
|
+
) => Promise<RoutePayload | any> | RoutePayload | any;
|
|
28
|
+
|
|
29
|
+
export interface RegisteredRoute {
|
|
30
|
+
path: string;
|
|
31
|
+
method: string;
|
|
32
|
+
handler: RouteHandler;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface HookCondition {
|
|
36
|
+
only?: string[];
|
|
37
|
+
never?: string[];
|
|
38
|
+
when?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RegisteredHook<T = any> {
|
|
42
|
+
event: string;
|
|
43
|
+
call: (payload: T) => Promise<T | void | null> | T | void | null;
|
|
44
|
+
condition?: HookCondition;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RegisteredPredicate {
|
|
48
|
+
name: string;
|
|
49
|
+
call: () => boolean | Promise<boolean>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RegisteredMiddleware {
|
|
53
|
+
name: string;
|
|
54
|
+
call: (req: Request, res: Response, next: NextFunction) => any;
|
|
55
|
+
predicates?: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ToolParameter {
|
|
59
|
+
name: string;
|
|
60
|
+
type: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
required?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RegisteredTool {
|
|
66
|
+
name: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
parameters: {
|
|
69
|
+
type: 'object';
|
|
70
|
+
properties: Record<string, any>;
|
|
71
|
+
required?: string[];
|
|
72
|
+
};
|
|
73
|
+
call: (args: any) => any;
|
|
74
|
+
condition?: HookCondition;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface RegisteredCognite {
|
|
78
|
+
name: string;
|
|
79
|
+
execute: (...args: any[]) => any;
|
|
80
|
+
description?: string;
|
|
81
|
+
options?: Record<string, any>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PackageManifest {
|
|
85
|
+
name?: string;
|
|
86
|
+
version?: string;
|
|
87
|
+
entry?: string;
|
|
88
|
+
folder: string;
|
|
89
|
+
dir?: string;
|
|
90
|
+
config?: any;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface JohankitApp extends Application {
|
|
94
|
+
setup: () => Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export declare function bootstrap(
|
|
98
|
+
config?: BootstrapConfig
|
|
99
|
+
): Promise<any>;
|
|
100
|
+
|
|
101
|
+
export declare function dispatchEvent<T = any>(
|
|
102
|
+
eventName: string,
|
|
103
|
+
payload: T
|
|
104
|
+
): Promise<T>;
|