@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 ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "printWidth": 80,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "semi": true
6
+ }
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
+ }