@kadi.build/core 0.0.1-alpha.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/.prettierrc +6 -0
- package/README.md +306 -0
- package/agent.json +18 -0
- package/broker.js +214 -0
- package/index.js +370 -0
- package/ipc.js +220 -0
- package/ipcInterfaces/pythonAbilityIPC.py +177 -0
- package/package.json +26 -0
package/.prettierrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# @kadi.build/core
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The `@kadi.build/core` module is a comprehensive toolkit for developers integrating with the KADI infrastructure. This module simplifies tasks such as managing `agent.json` files, spawning processes, interacting with brokers, and handling interprocess communication (IPC).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Agent JSON Management**: Manage configurations for tools, agents, or systems using KADI.
|
|
10
|
+
- **Process Management**: Support launching and managing subprocesses.
|
|
11
|
+
- **Multi-Broker Support**: Dynamic broker selection and management with environment/runtime configuration.
|
|
12
|
+
- **Broker Interaction**: Facilitate communications with WebSocket brokers.
|
|
13
|
+
- **IPC Support**: Tools for interprocess communication across various programming languages.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install `@kadi.build/core` using npm:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @kadi.build/core
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
### Agent JSON Functions
|
|
26
|
+
|
|
27
|
+
- **`getAbilityJSON(abilityName, abilityVersion)`**: Retrieves the `agent.json` for a specified ability.
|
|
28
|
+
- **`getAbilityJSONPath(abilityName, abilityVersion)`**: Provides the file path to the `agent.json` for a specific ability.
|
|
29
|
+
- **`getAbilityVersionFromArray(abilities, name)`**: Searches ability array provided, and returns the version number for name provided.
|
|
30
|
+
- **`getAbilitiesDir()`**: Returns the directory path where abilities are stored.
|
|
31
|
+
- **`getProjectJSON()`**: Fetches the `agent.json` for the current project.
|
|
32
|
+
- **`getProjectJSONPath()`**: Gets the file path for the project's `agent.json`.
|
|
33
|
+
- **`getKadiCoreJSON()`**: Retrieves the `agent.json` for the Kadi core.
|
|
34
|
+
- **`getKadiCoreJSONPath()`**: Provides the file path to the Kadi core's `agent.json`.
|
|
35
|
+
- **`getKadiJSON()`**: Fetches the `agent.json` for the Kadi system.
|
|
36
|
+
- **`getKadiJSONPath()`**: Returns the file path for the Kadi system's `agent.json`.
|
|
37
|
+
|
|
38
|
+
### Broker Management Functions
|
|
39
|
+
|
|
40
|
+
The broker management system allows dynamic selection from multiple configured brokers defined in `agent.json`:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"brokers": {
|
|
45
|
+
"local": "ws://127.0.0.1:8080",
|
|
46
|
+
"remote": "ws://production.example.com:8080",
|
|
47
|
+
"staging": "ws://staging.example.com:8080"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Available functions:
|
|
53
|
+
|
|
54
|
+
- **`KADI_BROKERS`**: Object containing all configured brokers with their parsed URLs.
|
|
55
|
+
- **`KADI_BROKER_URL`**: Default broker URL (first one defined, for backward compatibility).
|
|
56
|
+
- **`getBrokerUrl(brokerName)`**: Get URL for a specific broker by name. Returns `null` if not found.
|
|
57
|
+
- **`getBrokerNames()`**: Get array of all available broker names.
|
|
58
|
+
- **`setActiveBroker(brokerName)`**: Set the active broker for the session. Returns `true` if successful.
|
|
59
|
+
- **`getActiveBrokerName()`**: Get the name of the currently active broker.
|
|
60
|
+
- **`getActiveBrokerUrl()`**: Get the URL of the currently active broker.
|
|
61
|
+
- **`getDefaultBrokerName()`**: Get the name of the default broker (first one defined).
|
|
62
|
+
- **`selectBrokerFromEnv()`**: Set active broker from `KADI_BROKER` environment variable.
|
|
63
|
+
|
|
64
|
+
### Process Management Functions
|
|
65
|
+
|
|
66
|
+
- **`runExecCommand(name, version, command)`**: Executes a command for initializing abilities.
|
|
67
|
+
- **`runSpawnCommand(name, version, command)`**: Uses `spawn` to execute commands for subprocesses.
|
|
68
|
+
|
|
69
|
+
## Usage Examples
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
import {
|
|
73
|
+
getProjectJSON,
|
|
74
|
+
runExecCommand,
|
|
75
|
+
IPCManager,
|
|
76
|
+
Broker
|
|
77
|
+
} from '@kadi.build/core';
|
|
78
|
+
|
|
79
|
+
async function setupProject() {
|
|
80
|
+
const projectConfig = getProjectJSON();
|
|
81
|
+
console.log(projectConfig);
|
|
82
|
+
|
|
83
|
+
await runExecCommand('example', '1.0', 'npm install');
|
|
84
|
+
|
|
85
|
+
// Broker management examples
|
|
86
|
+
console.log('Available brokers:', getBrokerNames());
|
|
87
|
+
console.log('All brokers:', KADI_BROKERS);
|
|
88
|
+
|
|
89
|
+
// Set active broker
|
|
90
|
+
setActiveBroker('remote');
|
|
91
|
+
console.log('Active broker:', getActiveBrokerUrl());
|
|
92
|
+
|
|
93
|
+
// IPC setup
|
|
94
|
+
const ipc = new IPCManager();
|
|
95
|
+
ipc.createInstance('python', 'pythonScript.py', 'pythonInstance');
|
|
96
|
+
|
|
97
|
+
// Traditional broker setup
|
|
98
|
+
Broker.addBroker('ws://example.com', 'exampleBroker');
|
|
99
|
+
let broker = Broker.getBroker('default');
|
|
100
|
+
console.log(`Connected to ${broker.url}`);
|
|
101
|
+
broker.send(
|
|
102
|
+
BrokerMessageBuilder.setup(
|
|
103
|
+
'TestAgent',
|
|
104
|
+
'A Broker Testing Agent',
|
|
105
|
+
null,
|
|
106
|
+
null
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Broker Functions
|
|
113
|
+
|
|
114
|
+
- **`Broker`**: Manages interactions with broker systems.
|
|
115
|
+
- **`IBroker`**: Broker instance interface with methods to manage its lifecycle and communications.
|
|
116
|
+
- **`BrokerMessageBuilder`**: Assists in constructing messages for broker communication.
|
|
117
|
+
|
|
118
|
+
## Broker Functions
|
|
119
|
+
|
|
120
|
+
The Broker system facilitates interaction between different agents and services within the Kadi environment.
|
|
121
|
+
|
|
122
|
+
### `Broker` Interface
|
|
123
|
+
|
|
124
|
+
- **`addBroker(url, name = 'default')`**: Adds a new broker connection. If a broker with the same name exists, it will be reused.
|
|
125
|
+
- **`disconnect(name = 'default')`**: Disconnects the broker specified by the name.
|
|
126
|
+
- **`deleteBroker(name = 'default')`**: Deletes the broker specified by the name and closes its connection.
|
|
127
|
+
- **`send(message, brokerName = 'default')`**: Sends a message through the broker specified by the name.
|
|
128
|
+
- **`addEventListener(event, listener, brokerName = 'default')`**: Adds an event listener to the specified broker.
|
|
129
|
+
- **`removeEventListener(event, listener, brokerName = 'default')`**: Removes an event listener from the specified broker.
|
|
130
|
+
- **`removeAllListeners(brokerName = 'default')`**: Removes all event listeners from the specified broker.
|
|
131
|
+
- **`getBroker(brokerName = 'default')`**: Retrieves a broker instance by name.
|
|
132
|
+
|
|
133
|
+
### `IBroker` Instance
|
|
134
|
+
|
|
135
|
+
This class extends `EventEmitter` and manages individual broker connections:
|
|
136
|
+
|
|
137
|
+
- **`constructor(url, name)`**: Initializes a new broker connection.
|
|
138
|
+
- **`send(message)`**: Sends a message through the WebSocket connection.
|
|
139
|
+
- **`getConnectedAgents()`**: Retrieves a list of currently connected agents.
|
|
140
|
+
|
|
141
|
+
### `BrokerMessageBuilder`
|
|
142
|
+
|
|
143
|
+
Utility class for constructing broker messages:
|
|
144
|
+
|
|
145
|
+
- **`create_message(type, data)`**: Creates a JSON string with the specified type and data.
|
|
146
|
+
- **`message(to, content)`**: Constructs a message directed to a specific peer.
|
|
147
|
+
- **`setup(name, description, limit, uuid)`**: Creates a setup message for registering the broker.
|
|
148
|
+
- **`suspend()`**: Constructs a suspend message.
|
|
149
|
+
- **`finish()`**: Constructs a finish message.
|
|
150
|
+
- **`list()`**: Constructs a list message to request a list of connected agents.
|
|
151
|
+
|
|
152
|
+
### IPC Functions
|
|
153
|
+
|
|
154
|
+
- **`IPCMessageBuilder`**: Constructs messages for IPC interactions.
|
|
155
|
+
- **`IPCManager`**: Manages setup and lifecycle of IPC connections.
|
|
156
|
+
- **`IAbilityIPC`**: Represents an IPC ability instance with methods to manage its lifecycle and communications.
|
|
157
|
+
|
|
158
|
+
### `IPCManager`
|
|
159
|
+
|
|
160
|
+
Manages the lifecycle and setup of interprocess communications.
|
|
161
|
+
|
|
162
|
+
- **`createInstance(language, commandString, name = 'default')`**: Creates and manages a new IPC instance for the specified language.
|
|
163
|
+
- **`getInstance(name)`**: Retrieves an existing IPC instance by name.
|
|
164
|
+
- **`shutdownInstance(name)`**: Shuts down an existing IPC instance by name.
|
|
165
|
+
|
|
166
|
+
### `IAbilityIPC` Instance
|
|
167
|
+
|
|
168
|
+
Manages an individual interprocess communication instance:
|
|
169
|
+
|
|
170
|
+
- **`start()`**: Starts the child process associated with this IPC instance.
|
|
171
|
+
- **`launch()`**: Sends the launch command to the associated process.
|
|
172
|
+
- **`sendMessage(message)`**: Sends a JSON-formatted message to the child process.
|
|
173
|
+
- **`shutdown()`**: Sends a shutdown command to the child process and terminates it.
|
|
174
|
+
|
|
175
|
+
### `IPCMessageBuilder`
|
|
176
|
+
|
|
177
|
+
Utility class for constructing IPC messages:
|
|
178
|
+
|
|
179
|
+
- **`createMessage(type, contents)`**: Creates a JSON string with the specified type and contents.
|
|
180
|
+
- **`launch(commandString)`**: Constructs a launch message with the specified command string.
|
|
181
|
+
- **`shutdown()`**: Constructs a shutdown message.
|
|
182
|
+
- **`message(content)`**: Constructs a general message with the specified content.
|
|
183
|
+
|
|
184
|
+
## Usage Examples
|
|
185
|
+
|
|
186
|
+
Here's how you might use the IPC and Broker functionalities:
|
|
187
|
+
|
|
188
|
+
### Interacting with IPC
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
console.log('Starting IPC Ability Interface test...');
|
|
192
|
+
|
|
193
|
+
// Create Python IPC instance
|
|
194
|
+
const pythonIPC = IPCManager.createInstance(
|
|
195
|
+
'python',
|
|
196
|
+
'echo "Hello, World!"',
|
|
197
|
+
'pythonTest'
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
pythonIPC.on('error', (error) => {
|
|
201
|
+
console.error('Handled Python Error:', error);
|
|
202
|
+
// Consider appropriate error handling or recovery actions here
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
pythonIPC.on('critical-error', (error) => {
|
|
206
|
+
console.error('Critical Error from Python:', error);
|
|
207
|
+
// Handle critical errors, potentially restarting the process or alerting administrators
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
pythonIPC.on('message', (msg) => {
|
|
211
|
+
console.log('Message from Python:', msg);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
pythonIPC.on('broker', (msg) => {
|
|
215
|
+
console.log('Broker message from Python:', msg);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
pythonIPC.on('shutdown', () => {
|
|
219
|
+
console.log('Python IPC has shut down.');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
Broker.addBroker('http://your.broker.com', 'default');
|
|
223
|
+
let brk = Broker.getBroker('default');
|
|
224
|
+
broker = brk;
|
|
225
|
+
|
|
226
|
+
pythonIPC.attachBroker(brk);
|
|
227
|
+
|
|
228
|
+
brk.on('open', function open() {
|
|
229
|
+
console.log(`Connected to ${brk.url}`);
|
|
230
|
+
const jsonObject = BrokerMessageBuilder.setup(
|
|
231
|
+
'TestAgent',
|
|
232
|
+
'A Broker Testing Agent',
|
|
233
|
+
null,
|
|
234
|
+
null
|
|
235
|
+
);
|
|
236
|
+
brk.send(jsonObject);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
brk.on('message', function incoming(data) {
|
|
240
|
+
let msg = JSON.parse(data);
|
|
241
|
+
if (msg.type == 'setup') {
|
|
242
|
+
(async () => {
|
|
243
|
+
try {
|
|
244
|
+
await GetLLMProxy();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('Error during GetLLMProxy:', error.message);
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
249
|
+
}
|
|
250
|
+
// console.log('Received:', formatJSON(data));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
brk.on('error', function error(error) {
|
|
254
|
+
console.error('WebSocket error:', error);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
brk.on('close', function close() {
|
|
258
|
+
console.log('Disconnected from the server');
|
|
259
|
+
process.exit(0); // Exit the process when the WebSocket connection is closed
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Send launch command
|
|
263
|
+
|
|
264
|
+
//Utilize log files
|
|
265
|
+
// pythonIPC.sendMessage(IPCMessageBuilder.launch(pythonIPC.commandString, process.cwd() + "/logs"));
|
|
266
|
+
|
|
267
|
+
//No log files created
|
|
268
|
+
pythonIPC.sendMessage(IPCMessageBuilder.launch(pythonIPC.commandString));
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Handling IPC
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
const ipcManager = new IPCManager();
|
|
275
|
+
const ipcInstance = ipcManager.createInstance('node', 'app.js', 'nodeApp');
|
|
276
|
+
ipcInstance.on('message', (message) => console.log(message));
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## TODO:
|
|
282
|
+
|
|
283
|
+
- Once @kadi.build/core and dependencies are done, @kadi.build/cli (and all commands), need to be rewritten, using the @kadi.build/core module. this will be v1.0 release
|
|
284
|
+
|
|
285
|
+
* [ ] add getConnectedAgents to the Broker, it should call the IBroker method
|
|
286
|
+
|
|
287
|
+
* [x] add key to IBroker object
|
|
288
|
+
* [ ] IBroker on 'message' should listen for setup event, and update teh uuid and key for the IBroker object
|
|
289
|
+
|
|
290
|
+
* [ ] Extract Broker and IPC to their own node module and host on npm
|
|
291
|
+
* [x] Publish @kadi.build/core to npm
|
|
292
|
+
* [x] Setup of python environment needs to happen once an IPC Inteface is created
|
|
293
|
+
This include setting up the virtual environment and installing the required packages
|
|
294
|
+
* [x] Add method to IPCManger to set IPCInterfaces from agent.json. If IPCManger has empty list, it thorws an error that no IAbilityIPC's can be created. IPCManger passes the proper interface command to IAbilityIPC on creation from this list, IAbilityIPC does not need access directly to agent.json. Controlling app can update this list anytime a new IAbilityInterface is created, allowing the agent.json to be updated during runtime.
|
|
295
|
+
* [x] Need to adapt ipc.js to read from the @kadi.build/core folder when trying to open IPC instances, instead of project folder. @kadi.build/core should be in the npm_modules folder (when deployed)
|
|
296
|
+
* [x] The test code needs to fully test the IPCManger interface, and not just the IPCAbility
|
|
297
|
+
* [x] If node fails, we need to make sure python IPC interface and child processes also close out.
|
|
298
|
+
* [x] Add buffer to error handeling in IAbilityIPC, mimic same process used in std input
|
|
299
|
+
* [x] Verify if Python IAbilityIPC needs to utilize buffering, and node passing '\n' at end of jso
|
|
300
|
+
* [x] Setup passthrough for broker messages to/from Language IPC interface
|
|
301
|
+
* [x] Add broker message builder/parser in python IAbilityIPC
|
|
302
|
+
* [x] Update BrokerMessageBuilder to use same format at IPCMessageBuilder
|
|
303
|
+
* [x] Update Broker so it extends EventEmitter, rather than have a property of event emitter (similar to IAbilityIPC)
|
|
304
|
+
* [x] update python IAbility so that it creates a timestamped command_output.log file, new file for each run, in the project root directory... not the @kadi.build/core directory
|
|
305
|
+
* [x] Comment out debug messages
|
|
306
|
+
* [x] Add ability to allow for launched process to be piped to event handler (launch command should default to process, but allow user to set flag that will save all outputs to file in the event handler)
|
package/agent.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kadi-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "base install ability for kadi",
|
|
6
|
+
"repo": "https://gitlab.com/humin-game-lab/agent-abilities/kadi-core.git",
|
|
7
|
+
"lib": "https://gitlab.com/humin-game-lab/agent-abilities/kadi-core/-/archive/v0.0.1/kadi-core-v0.0.1.zip",
|
|
8
|
+
"abilities": {},
|
|
9
|
+
|
|
10
|
+
"scripts": {
|
|
11
|
+
"preflight": "echo 'kadi-core v0.0.1 is starting...'",
|
|
12
|
+
"setup": "echo 'kadi-core v0.0.1 is ready...'",
|
|
13
|
+
"start": "npm install",
|
|
14
|
+
"stop": "echo 'kadi-core v0.0.1 is ready...'",
|
|
15
|
+
"python": "python3 ./node_modules/kadi-core/ipcInterfaces/pythonAbilityIPC.py",
|
|
16
|
+
"rust": "cargo run --bin path/to/rustAbilityIPC"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/broker.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
function formatJSON(jsonString) {
|
|
6
|
+
try {
|
|
7
|
+
const jsonObj = JSON.parse(jsonString);
|
|
8
|
+
return JSON.stringify(jsonObj, null, 2);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return `Error parsing JSON: ${error}`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//List Message
|
|
15
|
+
// {
|
|
16
|
+
// "type": "list",
|
|
17
|
+
// "data": {
|
|
18
|
+
// "clients": [
|
|
19
|
+
// {
|
|
20
|
+
// "id": "9143a10c-5b4f-11ef-bb3a-2250bcde1118",
|
|
21
|
+
// "name": "TestAgent",
|
|
22
|
+
// "description": "A Broker Testing Agent"
|
|
23
|
+
// }
|
|
24
|
+
// ]
|
|
25
|
+
// }
|
|
26
|
+
// }
|
|
27
|
+
|
|
28
|
+
class IBroker extends EventEmitter {
|
|
29
|
+
constructor(url, name) {
|
|
30
|
+
super();
|
|
31
|
+
this.url = url;
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.uuid = null; //public id for the broker
|
|
34
|
+
this.key = null; //private id for the broker
|
|
35
|
+
this.queryPending = null; //used for Syncronous queries, disables forwarding to onMessage
|
|
36
|
+
this.ws = new WebSocket(url);
|
|
37
|
+
|
|
38
|
+
this.ws.on('open', () => {
|
|
39
|
+
this.emit('open');
|
|
40
|
+
console.log(`Connected to ${this.url}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.ws.on('message', (data) => {
|
|
44
|
+
let msg = JSON.parse(data);
|
|
45
|
+
|
|
46
|
+
//Emit messages to listeners
|
|
47
|
+
if (msg.type != this.queryPending) {
|
|
48
|
+
//A syncronous query is pending
|
|
49
|
+
this.emit('message', data);
|
|
50
|
+
} else {
|
|
51
|
+
// Emit synQuery to listener, to proces event
|
|
52
|
+
this.queryPending = null;
|
|
53
|
+
this.emit('syncQuery', data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// console.log('Received:', data);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.ws.on('error', (error) => {
|
|
60
|
+
this.emit('error', error);
|
|
61
|
+
console.error('WebSocket error:', error);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.ws.on('close', () => {
|
|
65
|
+
this.emit('close');
|
|
66
|
+
console.log('Disconnected from the server');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getConnectedAgents() {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
// Set the query pending to list
|
|
73
|
+
this.queryPending = 'list';
|
|
74
|
+
|
|
75
|
+
// Set up a timeout to reject if no response is received in 10 seconds
|
|
76
|
+
const timeout = setTimeout(() => {
|
|
77
|
+
reject(new Error('Get Connected Agents timed out after 10 seconds'));
|
|
78
|
+
}, 10000);
|
|
79
|
+
|
|
80
|
+
const handleListRequest = (data) => {
|
|
81
|
+
const msg = JSON.parse(data);
|
|
82
|
+
|
|
83
|
+
if (msg.type === 'list') {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
this.off('syncQuery', handleListRequest);
|
|
86
|
+
resolve(msg.data.clients);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const jsonObject = BrokerMessageBuilder.list();
|
|
91
|
+
this.send(jsonObject);
|
|
92
|
+
// console.log("Requested List of Agents");
|
|
93
|
+
this.on('syncQuery', handleListRequest);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
send(message) {
|
|
98
|
+
// console.log("WS Ready State: ", this.ws.readyState)
|
|
99
|
+
// console.log("WS OPEN: ", WebSocket.OPEN)
|
|
100
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
101
|
+
// console.log("Sending: ", JSON.stringify(message, null, 2));
|
|
102
|
+
this.ws.send(message);
|
|
103
|
+
// console.log(`Sending message to ${this.url}: ${message}`);
|
|
104
|
+
} else {
|
|
105
|
+
// console.log('WebSocket is not open.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let brokers = new Map();
|
|
111
|
+
|
|
112
|
+
export const Broker = {
|
|
113
|
+
addBroker: function (url, name = 'default') {
|
|
114
|
+
if (!brokers.has(name)) {
|
|
115
|
+
let broker = new IBroker(url, name);
|
|
116
|
+
brokers.set(name, broker);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(`Broker ${name} already exists.`);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
disconnect: function (name = 'default') {
|
|
123
|
+
let broker = brokers.get(name);
|
|
124
|
+
if (broker) {
|
|
125
|
+
broker.ws.close();
|
|
126
|
+
} else {
|
|
127
|
+
console.error(`Broker ${name} not found`);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
deleteBroker: function (name = 'default') {
|
|
132
|
+
let broker = brokers.get(name);
|
|
133
|
+
if (broker) {
|
|
134
|
+
broker.ws.close();
|
|
135
|
+
brokers.delete(name);
|
|
136
|
+
} else {
|
|
137
|
+
console.error(`Broker ${name} not found`);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
send: function (message, brokerName = 'default') {
|
|
142
|
+
let broker = brokers.get(brokerName);
|
|
143
|
+
if (broker) {
|
|
144
|
+
broker.send(message);
|
|
145
|
+
} else {
|
|
146
|
+
console.error(`Broker ${brokerName} not found`);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
addEventListener: function (event, listener, brokerName = 'default') {
|
|
151
|
+
let broker = brokers.get(brokerName);
|
|
152
|
+
if (broker) {
|
|
153
|
+
broker.on(event, listener);
|
|
154
|
+
} else {
|
|
155
|
+
console.error(`Broker ${brokerName} not found`);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
removeEventListener: function (event, listener, brokerName = 'default') {
|
|
160
|
+
let broker = brokers.get(brokerName);
|
|
161
|
+
if (broker) {
|
|
162
|
+
broker.off(event, listener);
|
|
163
|
+
} else {
|
|
164
|
+
console.error(`Broker ${brokerName} not found`);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
removeAllListeners: function (brokerName = 'default') {
|
|
169
|
+
let broker = brokers.get(brokerName);
|
|
170
|
+
if (broker) {
|
|
171
|
+
broker.removeAllListeners();
|
|
172
|
+
} else {
|
|
173
|
+
console.error(`Broker ${brokerName} not found`);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
getBroker: function (brokerName = 'default') {
|
|
178
|
+
return brokers.get(brokerName);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
//Need to add a message that will return a list of agents connected
|
|
183
|
+
// for a specific name/type
|
|
184
|
+
export const BrokerMessageBuilder = {
|
|
185
|
+
create_message: function (type, data) {
|
|
186
|
+
return JSON.stringify({
|
|
187
|
+
type: type,
|
|
188
|
+
data: data
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
message: function (to, data) {
|
|
192
|
+
return BrokerMessageBuilder.create_message('sendmessage', {
|
|
193
|
+
peer: to,
|
|
194
|
+
data: data
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
setup: function (name, description, limit, uuid) {
|
|
198
|
+
return BrokerMessageBuilder.create_message('setup', {
|
|
199
|
+
name: name,
|
|
200
|
+
description: description,
|
|
201
|
+
limit: limit,
|
|
202
|
+
key: uuid
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
suspend: function () {
|
|
206
|
+
return BrokerMessageBuilder.create_message('suspend', {});
|
|
207
|
+
},
|
|
208
|
+
finish: function () {
|
|
209
|
+
return BrokerMessageBuilder.create_message('finish', {});
|
|
210
|
+
},
|
|
211
|
+
list: function () {
|
|
212
|
+
return BrokerMessageBuilder.create_message('list', {});
|
|
213
|
+
}
|
|
214
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
// import { promises as fsPromises } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { exec, execSync, spawn } from 'child_process';
|
|
6
|
+
import { Broker, BrokerMessageBuilder } from './broker.js';
|
|
7
|
+
import { IPCManager, IPCMessageBuilder } from './ipc.js';
|
|
8
|
+
import { createRequire } from 'module'; // Import createRequire to use require in ES Modules
|
|
9
|
+
|
|
10
|
+
// Create a require function scoped to the current module
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
function resolveKadiExecPath() {
|
|
14
|
+
const command = process.platform === 'win32' ? 'where kadi' : 'which kadi';
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const kadiPath = execSync(command, { encoding: 'utf8' }).trim();
|
|
18
|
+
if (kadiPath) {
|
|
19
|
+
return kadiPath;
|
|
20
|
+
} else {
|
|
21
|
+
throw new Error('Kadi CLI tool not found in the system PATH');
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error resolving Kadi path:', error);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveKadiInstallPath() {
|
|
30
|
+
try {
|
|
31
|
+
// Step 1: Get the path to the kadi binary
|
|
32
|
+
const command = process.platform === 'win32' ? 'where kadi' : 'which kadi';
|
|
33
|
+
const kadiBinaryPath = execSync(command, { encoding: 'utf8' }).trim();
|
|
34
|
+
|
|
35
|
+
if (!kadiBinaryPath) {
|
|
36
|
+
throw new Error('Kadi CLI tool not found in the system PATH');
|
|
37
|
+
}
|
|
38
|
+
// console.log('Resolved Kadi binary path:', kadiBinaryPath);
|
|
39
|
+
|
|
40
|
+
// Step 2: Resolve the symlink to find the actual location of the binary
|
|
41
|
+
const resolvedBinaryPath = fs.realpathSync(kadiBinaryPath);
|
|
42
|
+
// console.log('Resolved binary real path:', resolvedBinaryPath);
|
|
43
|
+
|
|
44
|
+
// Step 3: Correctly determine the root directory of the kadi package
|
|
45
|
+
const kadiDir = path.dirname(resolvedBinaryPath); // Move up one level to get the kadi directory
|
|
46
|
+
// console.log('Kadi directory:', kadiDir);
|
|
47
|
+
|
|
48
|
+
// Step 4: Verify if this directory contains the kadi package.json
|
|
49
|
+
const packageJsonPath = path.join(kadiDir, 'package.json');
|
|
50
|
+
// console.log('Checking for package.json at:', packageJsonPath);
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
53
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
54
|
+
|
|
55
|
+
if (packageJson.name === 'kadi') {
|
|
56
|
+
// console.log('Kadi package.json found:', packageJsonPath);
|
|
57
|
+
return kadiDir;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error('Package.json found, but it is not for kadi.');
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error('Kadi package.json not found in the resolved path.');
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error resolving Kadi CLI install path:', error);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
71
|
+
const __dirname = path.dirname(__filename);
|
|
72
|
+
|
|
73
|
+
const kadiExecDir = resolveKadiExecPath();
|
|
74
|
+
const kadiRootDir = resolveKadiInstallPath(); //path.join(kadiCLIInstallPath);
|
|
75
|
+
const kadiAgentPath = path.join(kadiRootDir, 'agent.json');
|
|
76
|
+
|
|
77
|
+
const rootDir = process.cwd();
|
|
78
|
+
const abilitiesDir = path.join(rootDir, 'abilities');
|
|
79
|
+
const projectAgentPath = path.join(rootDir, 'agent.json');
|
|
80
|
+
const kadiCoreAgentPath = path.join(__dirname, 'agent.json');
|
|
81
|
+
|
|
82
|
+
//Gets the agent.json for the ability, so dependencies can be installed
|
|
83
|
+
export function getAbilityJSON(abilityName, abilityVersion) {
|
|
84
|
+
//Get the agent.json for the ability
|
|
85
|
+
let abilityPath = path.join(
|
|
86
|
+
rootDir,
|
|
87
|
+
'abilities',
|
|
88
|
+
abilityName,
|
|
89
|
+
abilityVersion,
|
|
90
|
+
'agent.json'
|
|
91
|
+
);
|
|
92
|
+
let abilityJSON = fs.readFileSync(abilityPath, 'utf8');
|
|
93
|
+
abilityJSON = JSON.parse(abilityJSON);
|
|
94
|
+
return abilityJSON;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//Gets the file path for the ability agent.json
|
|
98
|
+
export function getAbilityJSONPath(abilityName, abilityVersion) {
|
|
99
|
+
return path.join(
|
|
100
|
+
rootDir,
|
|
101
|
+
'abilities',
|
|
102
|
+
abilityName,
|
|
103
|
+
abilityVersion,
|
|
104
|
+
'agent.json'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//Gets the abilities directory
|
|
109
|
+
export function getAbilitiesDir() {
|
|
110
|
+
return abilitiesDir;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//Gets the agent.json for the ability, so dependencies can be installed
|
|
114
|
+
export function getProjectJSON() {
|
|
115
|
+
//Get the agent.json for kadi
|
|
116
|
+
let projectJSON = fs.readFileSync(projectAgentPath, 'utf8');
|
|
117
|
+
projectJSON = JSON.parse(projectJSON);
|
|
118
|
+
return projectJSON;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//Get the file path for the project agent.json
|
|
122
|
+
export function getProjectJSONPath() {
|
|
123
|
+
return projectAgentPath;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//Get Kadi Core agent.json
|
|
127
|
+
export function getKadiCoreJSON() {
|
|
128
|
+
//Get the agent.json for kadi
|
|
129
|
+
let kadiCoreJSON = fs.readFileSync(kadiCoreAgentPath, 'utf8');
|
|
130
|
+
kadiCoreJSON = JSON.parse(kadiCoreJSON);
|
|
131
|
+
return kadiCoreJSON;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//Get the file path for the KADI Core Agent.json
|
|
135
|
+
export function getKadiCoreJSONPath() {
|
|
136
|
+
return kadiCoreAgentPath;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//Gets the agent.json for KADI system
|
|
140
|
+
export function getKadiJSON() {
|
|
141
|
+
//Get the agent.json for kadi
|
|
142
|
+
let kadiJSON = fs.readFileSync(kadiAgentPath, 'utf8');
|
|
143
|
+
kadiJSON = JSON.parse(kadiJSON);
|
|
144
|
+
return kadiJSON;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//Gets the file path for the KADI Agent.json
|
|
148
|
+
export function getKadiJSONPath() {
|
|
149
|
+
return kadiAgentPath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//Get the exec path of the KADI system
|
|
153
|
+
export function getKadiExecPath() {
|
|
154
|
+
return kadiExecDir;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//Get the Install directory of KADI system
|
|
158
|
+
export function getKadiInstallPath() {
|
|
159
|
+
return kadiRootDir;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//Save agent.json file
|
|
163
|
+
export async function saveAgentJSON(agentJSON, agentJSONPath) {
|
|
164
|
+
// Convert the JSON object to a string with pretty-print formatting
|
|
165
|
+
const jsonString = JSON.stringify(agentJSON, null, 2);
|
|
166
|
+
|
|
167
|
+
// Write the JSON string to the specified file
|
|
168
|
+
fs.writeFile(agentJSONPath, jsonString, 'utf8', (err) => {
|
|
169
|
+
if (err) {
|
|
170
|
+
console.error('Error saving JSON to file:', err);
|
|
171
|
+
}
|
|
172
|
+
// else {
|
|
173
|
+
// console.log('JSON saved successfully to', filePath);
|
|
174
|
+
// }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getAbilityVersionFromArray(abilities, name) {
|
|
179
|
+
const ability = abilities.find((ability) => ability.name === name);
|
|
180
|
+
if (ability) {
|
|
181
|
+
return ability.version;
|
|
182
|
+
} else {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Get the API URL from the agent.json
|
|
188
|
+
let kadijson = getKadiJSON();
|
|
189
|
+
|
|
190
|
+
// Define the API URL (Adjust according to your API)
|
|
191
|
+
export const KADI_API_URL = kadijson.api;
|
|
192
|
+
export const SEARCH_API_URL = kadijson.api + '/search';
|
|
193
|
+
export const GET_API_URL = kadijson.api + '/get';
|
|
194
|
+
|
|
195
|
+
// Export all broker URLs
|
|
196
|
+
const brokers = {};
|
|
197
|
+
for (const [name, url] of Object.entries(kadijson.brokers)) {
|
|
198
|
+
const brokerUrl = new URL(url);
|
|
199
|
+
brokers[name] =
|
|
200
|
+
`${brokerUrl.hostname}:${brokerUrl.port}${brokerUrl.pathname}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const KADI_BROKERS = brokers;
|
|
204
|
+
|
|
205
|
+
// Get default broker (first one defined)
|
|
206
|
+
const defaultBrokerName = Object.keys(kadijson.brokers)[0];
|
|
207
|
+
export const KADI_BROKER_URL = brokers[defaultBrokerName]; // Backward compatibility
|
|
208
|
+
|
|
209
|
+
// Utility functions
|
|
210
|
+
export function getBrokerUrl(brokerName) {
|
|
211
|
+
return brokers[brokerName] || null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function getBrokerNames() {
|
|
215
|
+
return Object.keys(brokers);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getDefaultBrokerName() {
|
|
219
|
+
return defaultBrokerName;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Active broker selection
|
|
223
|
+
let activeBrokerName = defaultBrokerName;
|
|
224
|
+
|
|
225
|
+
export function setActiveBroker(brokerName) {
|
|
226
|
+
if (brokers[brokerName]) {
|
|
227
|
+
activeBrokerName = brokerName;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function getActiveBrokerName() {
|
|
234
|
+
return activeBrokerName;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function getActiveBrokerUrl() {
|
|
238
|
+
return brokers[activeBrokerName];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//Runs a command line string, used for abilities init functions
|
|
242
|
+
export async function runExecCommand(name, version, command) {
|
|
243
|
+
let execDirectory;
|
|
244
|
+
if (name === '' || version === '') {
|
|
245
|
+
execDirectory = path.join(rootDir);
|
|
246
|
+
} else {
|
|
247
|
+
execDirectory = path.join(rootDir, 'abilities', name, version);
|
|
248
|
+
}
|
|
249
|
+
console.log('execDirectory: ', execDirectory);
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
exec(
|
|
252
|
+
command,
|
|
253
|
+
{ cwd: execDirectory, maxBuffer: 1024 * 1024 },
|
|
254
|
+
(error, stdout, stderr) => {
|
|
255
|
+
if (error) {
|
|
256
|
+
console.error(`exec error: ${error}`);
|
|
257
|
+
reject(error); // Reject the promise on error
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (stderr.trim()) {
|
|
261
|
+
console.error(stderr);
|
|
262
|
+
}
|
|
263
|
+
resolve(stdout.trim()); // Resolve the promise when exec completes successfully
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function runSpawnCommand(name, version, command) {
|
|
270
|
+
let execDirectory;
|
|
271
|
+
if (name === '' || version === '') {
|
|
272
|
+
execDirectory = path.join(rootDir);
|
|
273
|
+
} else {
|
|
274
|
+
execDirectory = path.join(rootDir, 'abilities', name, version);
|
|
275
|
+
}
|
|
276
|
+
console.log('execDirectory: ', execDirectory);
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const child = spawn(command, {
|
|
279
|
+
cwd: execDirectory,
|
|
280
|
+
stdio: 'inherit', // This pipes the child process's stdio to the parent
|
|
281
|
+
shell: true // This ensures that the command is run within a shell
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
child.on('error', (error) => {
|
|
285
|
+
console.error(`Error: ${error}`);
|
|
286
|
+
reject(error);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
child.on('close', (code) => {
|
|
290
|
+
if (code !== 0) {
|
|
291
|
+
console.error(`Process exited with code: ${code}`);
|
|
292
|
+
reject(new Error(`Process exited with code: ${code}`));
|
|
293
|
+
} else {
|
|
294
|
+
resolve();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Function to find the version of an ability by name
|
|
301
|
+
export function findAbilityVersionByName(abilities, name) {
|
|
302
|
+
const ability = abilities.find((ability) => ability.name === name);
|
|
303
|
+
if (ability) {
|
|
304
|
+
return ability.version;
|
|
305
|
+
} else {
|
|
306
|
+
return 'No ability found with the specified name.';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function loadAbility(abilityName) {
|
|
311
|
+
let agentJSON = await getProjectJSON();
|
|
312
|
+
let abilityConfig = agentJSON.abilities.find(
|
|
313
|
+
(ability) => ability.name === abilityName
|
|
314
|
+
);
|
|
315
|
+
if (!abilityConfig) {
|
|
316
|
+
// throw new Error(`Ability ${abilityName} not found in configuration`);
|
|
317
|
+
console.error(`Ability ${abilityName} not found in Project configuration`);
|
|
318
|
+
const filePath = __filename;
|
|
319
|
+
console.log('filePath:', filePath);
|
|
320
|
+
// Extract the directory path of the file
|
|
321
|
+
const dirPath = path.dirname(filePath);
|
|
322
|
+
console.log(' dirPath:', dirPath);
|
|
323
|
+
|
|
324
|
+
// Split the directory path into an array of directories
|
|
325
|
+
const pathParts = dirPath.split(path.sep);
|
|
326
|
+
|
|
327
|
+
// Find the index of the "abilities" folder
|
|
328
|
+
const abilitiesIndex = pathParts.indexOf('abilities');
|
|
329
|
+
|
|
330
|
+
// Ensure that the folder above 'phone-ability' is 'abilities'
|
|
331
|
+
if (abilitiesIndex !== -1 && pathParts.length > abilitiesIndex + 2) {
|
|
332
|
+
const ability = pathParts[abilitiesIndex + 1]; // Extract 'phone-ability'
|
|
333
|
+
const version = pathParts[abilitiesIndex + 2]; // Extract '0.0.1'
|
|
334
|
+
|
|
335
|
+
console.log('Loading ability dependency from an ability');
|
|
336
|
+
agentJSON = await getAbilityJSON(ability, version);
|
|
337
|
+
abilityConfig = agentJSON.abilities.find(
|
|
338
|
+
(ability) => ability.name === abilityName
|
|
339
|
+
);
|
|
340
|
+
if (!abilityConfig) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Ability ${abilityName} not found in ${ability} : ${version} configuration`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
throw new Error('Ability not found in Project agent.json');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const moduleJSON = getAbilityJSON(abilityName, abilityConfig.version);
|
|
350
|
+
const modulePath = path.join(
|
|
351
|
+
rootDir,
|
|
352
|
+
`abilities/${abilityName}/${abilityConfig.version}/${moduleJSON.entry}`
|
|
353
|
+
);
|
|
354
|
+
const module = await import(modulePath);
|
|
355
|
+
return module.default || module; // Return the default export directly
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function launchAbility(language, path = '') {}
|
|
359
|
+
|
|
360
|
+
export { Broker };
|
|
361
|
+
|
|
362
|
+
export { BrokerMessageBuilder };
|
|
363
|
+
|
|
364
|
+
export { IPCManager };
|
|
365
|
+
|
|
366
|
+
export { IPCMessageBuilder };
|
|
367
|
+
|
|
368
|
+
//Load the IPC interfaces
|
|
369
|
+
let kadiCoreJSON = getKadiCoreJSON();
|
|
370
|
+
IPCManager.setIPCInterfaces(kadiCoreJSON.IPCInterfaces);
|
package/ipc.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { log } from 'console';
|
|
6
|
+
|
|
7
|
+
//IPC JSON Message Schema
|
|
8
|
+
// {
|
|
9
|
+
// type: 'launch' | 'shutdown' | 'message',
|
|
10
|
+
// contents: any,
|
|
11
|
+
// time: string
|
|
12
|
+
// }
|
|
13
|
+
|
|
14
|
+
export const IPCMessageBuilder = {
|
|
15
|
+
createMessage: function (type, contents) {
|
|
16
|
+
return JSON.stringify({
|
|
17
|
+
type: type,
|
|
18
|
+
contents: contents,
|
|
19
|
+
time: new Date().toISOString()
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
launch: function (commandString, logfilepath = null) {
|
|
24
|
+
return IPCMessageBuilder.createMessage('launch', {
|
|
25
|
+
command: commandString,
|
|
26
|
+
logfilepath: logfilepath
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
shutdown: function () {
|
|
31
|
+
return IPCMessageBuilder.createMessage('shutdown', {});
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
message: function (content) {
|
|
35
|
+
return IPCMessageBuilder.createMessage('message', content);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
broker: function (content) {
|
|
39
|
+
return IPCMessageBuilder.createMessage('broker', content);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const IPCManager = {
|
|
44
|
+
instances: new Map(),
|
|
45
|
+
IPCInterfaces: {},
|
|
46
|
+
createInstance: function (language, commandString, name = 'default') {
|
|
47
|
+
if (!IPCManager.IPCInterfaces[language]) {
|
|
48
|
+
throw new Error('Configuration for this language is not provided');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const instance = new IAbilityIPC(
|
|
52
|
+
language,
|
|
53
|
+
commandString,
|
|
54
|
+
IPCManager.IPCInterfaces,
|
|
55
|
+
name
|
|
56
|
+
);
|
|
57
|
+
this.instances.set(name, instance);
|
|
58
|
+
|
|
59
|
+
instance.on('shutdown', () => {
|
|
60
|
+
this.instances.delete(name);
|
|
61
|
+
console.log(`Instance ${name} removed after shutdown.`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return instance;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
getInstance: function (name) {
|
|
68
|
+
return this.instances.get(name);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
shutdownInstance: function (name) {
|
|
72
|
+
const instance = this.instances.get(name);
|
|
73
|
+
if (instance) {
|
|
74
|
+
instance.shutdown();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
setIPCInterfaces: function (interfaces) {
|
|
79
|
+
IPCManager.IPCInterfaces = interfaces;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
class IAbilityIPC extends EventEmitter {
|
|
84
|
+
constructor(language, commandString, interfaces, name) {
|
|
85
|
+
super();
|
|
86
|
+
this.language = language;
|
|
87
|
+
this.process = null; // Process is initially null
|
|
88
|
+
this.commandString = commandString;
|
|
89
|
+
this.name = name;
|
|
90
|
+
this.input_buffer = '';
|
|
91
|
+
this.error_buffer = '';
|
|
92
|
+
this._broker = null; // Broker used for proxying messages
|
|
93
|
+
|
|
94
|
+
// Read configurations from agent.json
|
|
95
|
+
this.IPCInterfaces = interfaces;
|
|
96
|
+
this.ipcStartupCommand = this.IPCInterfaces[language]; // Command to start the language IPC
|
|
97
|
+
|
|
98
|
+
if (!this.ipcStartupCommand) {
|
|
99
|
+
throw new Error('Unsupported language or missing command configuration');
|
|
100
|
+
} else {
|
|
101
|
+
console.log('IPC Startup Command: ', this.ipcStartupCommand);
|
|
102
|
+
this.start();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
start() {
|
|
107
|
+
// Starts the child process that handles IPC
|
|
108
|
+
this.process = spawn(this.ipcStartupCommand, {
|
|
109
|
+
shell: true
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this.process.stdout.on('data', (data) => {
|
|
113
|
+
this.input_buffer += data.toString();
|
|
114
|
+
let boundary = this.input_buffer.indexOf('\n');
|
|
115
|
+
while (boundary !== -1) {
|
|
116
|
+
let message = this.input_buffer.substring(0, boundary).trim();
|
|
117
|
+
this.input_buffer = this.input_buffer.substring(boundary + 1);
|
|
118
|
+
if (message) {
|
|
119
|
+
try {
|
|
120
|
+
let msg = JSON.parse(message);
|
|
121
|
+
if (msg.type === 'shutdown') {
|
|
122
|
+
this.emit('shutdown');
|
|
123
|
+
if (this._broker) {
|
|
124
|
+
this._broker.ws.close();
|
|
125
|
+
}
|
|
126
|
+
} else if (msg.type === 'broker') {
|
|
127
|
+
this.emit('broker', msg);
|
|
128
|
+
} else {
|
|
129
|
+
this.emit('message', msg);
|
|
130
|
+
}
|
|
131
|
+
// this.emit('message', JSON.parse(message));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Error parsing JSON:', error.message);
|
|
134
|
+
console.error('Raw data received:', message);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
boundary = this.input_buffer.indexOf('\n');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
this.process.stderr.on('data', (data) => {
|
|
142
|
+
this.error_buffer += data.toString();
|
|
143
|
+
let boundary = this.error_buffer.indexOf('\n');
|
|
144
|
+
while (boundary !== -1) {
|
|
145
|
+
let message = this.error_buffer.substring(0, boundary).trim();
|
|
146
|
+
this.error_buffer = this.error_buffer.substring(boundary + 1);
|
|
147
|
+
if (message) {
|
|
148
|
+
try {
|
|
149
|
+
this.emit('error', JSON.parse(message));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Error parsing JSON:', error.message);
|
|
152
|
+
console.error('Raw data received:', message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
boundary = this.error_buffer.indexOf('\n');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.process.on('exit', (code) => {
|
|
160
|
+
this.emit('exit', code);
|
|
161
|
+
// this.emit('shutdown', this); // Emitting a shutdown event for the manager to catch
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
this.process.on('error', (error) => {
|
|
165
|
+
this.emit('error', error);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.on('broker', (msg) => {
|
|
169
|
+
if (this._broker) {
|
|
170
|
+
this._broker.send(JSON.stringify(msg.contents));
|
|
171
|
+
} else {
|
|
172
|
+
console.error('No broker available to forward message to.');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.on('message', (msg) => {
|
|
177
|
+
// console.log('Message received:', msg);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
launch(logfilepath = null) {
|
|
182
|
+
// Send the command string to the language-specific IPC to launch the actual tool
|
|
183
|
+
let msg = IPCMessageBuilder.launch(this.commandString, logfilepath);
|
|
184
|
+
console.log('Sending launch message: ', msg);
|
|
185
|
+
this.sendMessage(msg);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
sendMessage(message) {
|
|
189
|
+
if (this.process && this.process.stdin.writable) {
|
|
190
|
+
this.process.stdin.write(JSON.stringify(message) + '\n');
|
|
191
|
+
} else {
|
|
192
|
+
// throw new Error("Process is not running or stdin is not writable.");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
shutdown() {
|
|
197
|
+
this.sendMessage(IPCMessageBuilder.shutdown());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
attachBroker(broker) {
|
|
201
|
+
this._broker = broker;
|
|
202
|
+
|
|
203
|
+
this._broker.on('message', (data) => {
|
|
204
|
+
let msg = JSON.parse(data);
|
|
205
|
+
console.log('IPC Broker Message Recieved: ', msg);
|
|
206
|
+
this.sendMessage(IPCMessageBuilder.broker(msg));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this._broker.on('error', (error) => {
|
|
210
|
+
let msg = JSON.parse(error);
|
|
211
|
+
|
|
212
|
+
this.sendMessage(IPCMessageBuilder.broker(msg));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
this._broker.on('close', () => {
|
|
216
|
+
this.shutdown();
|
|
217
|
+
// process.exit(0); // Exit the process when the WebSocket connection is closed
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import select
|
|
5
|
+
import threading
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
class MessageBuilder:
|
|
10
|
+
@staticmethod
|
|
11
|
+
def create_message(type, contents):
|
|
12
|
+
return json.dumps({
|
|
13
|
+
"type": type,
|
|
14
|
+
"contents": contents,
|
|
15
|
+
"time": datetime.now(timezone.utc).isoformat()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def shutdown():
|
|
20
|
+
return MessageBuilder.create_message('shutdown', {})
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def message(contents):
|
|
24
|
+
return MessageBuilder.create_message('message', contents)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def broker(contents):
|
|
28
|
+
return MessageBuilder.create_message('broker', contents)
|
|
29
|
+
|
|
30
|
+
def handle_process_output(ipc_instance, output):
|
|
31
|
+
"""Developer-customizable function to handle stdout data."""
|
|
32
|
+
# print("Process Output:", output.strip())
|
|
33
|
+
# ipc_instance.send(MessageBuilder.message({"status": "stdout", "contents": output}))
|
|
34
|
+
|
|
35
|
+
def handle_process_errors(ipc_instance, output):
|
|
36
|
+
"""Developer-customizable function to handle stderr data."""
|
|
37
|
+
# print("Process Error:", output.strip())
|
|
38
|
+
ipc_instance.send(MessageBuilder.message({"status": "stderr", "contents": output}))
|
|
39
|
+
|
|
40
|
+
def handle_broker(ipc_instance, contents):
|
|
41
|
+
"""Developer-customizable function to handle custom Broker messages."""
|
|
42
|
+
# ipc_instance.send(MessageBuilder.broker({"status": "message_received", "contents": contents}))
|
|
43
|
+
|
|
44
|
+
def handle_message(ipc_instance, contents):
|
|
45
|
+
"""Developer-customizable function to handle custom IPC messages."""
|
|
46
|
+
# Placeholder implementation that simply prints the message
|
|
47
|
+
# print(f"Received custom message: {contents}")
|
|
48
|
+
# ipc_instance.send(MessageBuilder.message({"status": "message_received", "contents": contents}))
|
|
49
|
+
# Developers can add their own logic here depending on their requirements
|
|
50
|
+
|
|
51
|
+
class PythonAbilityIPC:
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self.running = True
|
|
54
|
+
self.child_process = None
|
|
55
|
+
self.logfile = False
|
|
56
|
+
|
|
57
|
+
def process_stream(self, stream, handler):
|
|
58
|
+
"""Generic method to process stdout or stderr from child process."""
|
|
59
|
+
for line in iter(stream.readline, ''):
|
|
60
|
+
handler(line)
|
|
61
|
+
stream.close()
|
|
62
|
+
|
|
63
|
+
def read_input(self):
|
|
64
|
+
"""Read input lines from stdin and process each as a JSON message."""
|
|
65
|
+
while self.running:
|
|
66
|
+
try:
|
|
67
|
+
readable, _, _ = select.select([sys.stdin], [], [], 0.1)
|
|
68
|
+
if readable:
|
|
69
|
+
line = sys.stdin.readline()
|
|
70
|
+
if line == '':
|
|
71
|
+
print("Node.js process has terminated or closed the pipe.")
|
|
72
|
+
self.shutdown()
|
|
73
|
+
break
|
|
74
|
+
if line.strip():
|
|
75
|
+
try:
|
|
76
|
+
message = json.loads(line)
|
|
77
|
+
# self.send(MessageBuilder.message({"debug": "Parsing Message", "message": message}))
|
|
78
|
+
self.parse_message(message)
|
|
79
|
+
except json.JSONDecodeError as e:
|
|
80
|
+
self.send_error(f"JSON decoding failed: {str(e)}")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
self.send_error(f"An unexpected error occurred: {str(e)}")
|
|
83
|
+
self.shutdown()
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
def parse_message(self, message):
|
|
87
|
+
"""Parse incoming messages and take appropriate actions."""
|
|
88
|
+
try:
|
|
89
|
+
message = json.loads(message)
|
|
90
|
+
message_type = message.get('type')
|
|
91
|
+
if message_type == 'launch':
|
|
92
|
+
command = message.get('contents').get('command')
|
|
93
|
+
logfilepath = message.get('contents').get('logfilepath', None)
|
|
94
|
+
self.launch_tool(command, logfilepath)
|
|
95
|
+
elif message_type == 'shutdown':
|
|
96
|
+
self.shutdown()
|
|
97
|
+
elif message_type == 'broker':
|
|
98
|
+
handle_broker(self, message.get('contents')) # Handle broker messages
|
|
99
|
+
elif message_type == 'message':
|
|
100
|
+
handle_message(self, message.get('contents')) # Handle ipc messages
|
|
101
|
+
else:
|
|
102
|
+
self.send(MessageBuilder.message({"status": "unknown_message_type", "contents": message}))
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.send_error(f"An error occurred: {e}")
|
|
105
|
+
|
|
106
|
+
def handle_stdout(self, output):
|
|
107
|
+
"""Handles stdout from the child process."""
|
|
108
|
+
if self.logfile and self.stdout_log_path:
|
|
109
|
+
with open(self.stdout_log_path, 'a') as f:
|
|
110
|
+
f.write(output)
|
|
111
|
+
handle_process_output(self, output)
|
|
112
|
+
|
|
113
|
+
def handle_stderr(self, output):
|
|
114
|
+
"""Handles stderr from the child process."""
|
|
115
|
+
if self.logfile and self.stderr_log_path:
|
|
116
|
+
with open(self.stderr_log_path, 'a') as f:
|
|
117
|
+
f.write(output)
|
|
118
|
+
handle_process_errors(self, output)
|
|
119
|
+
|
|
120
|
+
def launch_tool(self, command_string, logfilepath=None):
|
|
121
|
+
if self.child_process:
|
|
122
|
+
self.child_process.terminate()
|
|
123
|
+
|
|
124
|
+
if logfilepath:
|
|
125
|
+
self.logfile = True
|
|
126
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
127
|
+
self.stdout_log_path = os.path.join(logfilepath, f"{timestamp}-stdout.log")
|
|
128
|
+
self.stderr_log_path = os.path.join(logfilepath, f"{timestamp}-stderr.log")
|
|
129
|
+
os.makedirs(logfilepath, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
self.child_process = subprocess.Popen(
|
|
132
|
+
command_string, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
133
|
+
universal_newlines=True, bufsize=1
|
|
134
|
+
)
|
|
135
|
+
# Setup stdout and stderr handling threads
|
|
136
|
+
threading.Thread(target=self.process_stream, args=(self.child_process.stdout, self.handle_stdout)).start()
|
|
137
|
+
threading.Thread(target=self.process_stream, args=(self.child_process.stderr, self.handle_stderr)).start()
|
|
138
|
+
|
|
139
|
+
# Send a message back to KADI using the message builder
|
|
140
|
+
self.send(MessageBuilder.message({"status": "tool_launched", "command": command_string}))
|
|
141
|
+
|
|
142
|
+
def shutdown(self):
|
|
143
|
+
if self.child_process:
|
|
144
|
+
self.child_process.terminate()
|
|
145
|
+
self.child_process = None
|
|
146
|
+
self.running = False
|
|
147
|
+
self.send(MessageBuilder.shutdown())
|
|
148
|
+
sys.exit(0)
|
|
149
|
+
|
|
150
|
+
def send_error(self, message):
|
|
151
|
+
sys.stderr.write(MessageBuilder.message({
|
|
152
|
+
'type': 'error',
|
|
153
|
+
'contents': {'message': message, 'type': 'exception'},
|
|
154
|
+
'time': datetime.now(timezone.utc).isoformat()
|
|
155
|
+
}) + '\n')
|
|
156
|
+
sys.stderr.flush()
|
|
157
|
+
|
|
158
|
+
def send(self, serializedJSONmessage):
|
|
159
|
+
"""Send a JSON message back to Node.js using standardized format."""
|
|
160
|
+
sys.stdout.write(serializedJSONmessage + '\n')
|
|
161
|
+
sys.stdout.flush()
|
|
162
|
+
|
|
163
|
+
def main():
|
|
164
|
+
try:
|
|
165
|
+
ipc = PythonAbilityIPC()
|
|
166
|
+
ipc.read_input()
|
|
167
|
+
except Exception as e:
|
|
168
|
+
# Print exception information to stderr to catch it in Node.js
|
|
169
|
+
print(json.dumps({'type': 'error', 'contents': {'message': str(e), 'type': 'exception'}, 'time': datetime.now(timezone.utc).isoformat()}), file=sys.stderr)
|
|
170
|
+
# Here you could also include more sophisticated error handling mechanisms:
|
|
171
|
+
# - Logging to a file
|
|
172
|
+
# - Sending alerts to a monitoring system
|
|
173
|
+
# - Cleanup resources or attempt recovery
|
|
174
|
+
sys.exit(1) # Exit with a non-zero status to indicate an error
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kadi.build/core",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "A module that is a comprehensive toolkit for developers integrating with the KADI infrastructure.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"format": "prettier --write .",
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"author": "Corey Clark",
|
|
12
|
+
"contributors": [
|
|
13
|
+
{
|
|
14
|
+
"name": "Kassi Bertrand",
|
|
15
|
+
"url": "https://github.com/kassi-bertrand"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"events": "^3.3.0",
|
|
21
|
+
"ws": "^8.16.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"prettier": "^3.6.2"
|
|
25
|
+
}
|
|
26
|
+
}
|