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 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>;