@qelos/plugins-cli 0.0.20 → 0.0.22

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/commands/pull.mjs CHANGED
@@ -8,7 +8,7 @@ export default function pullCommand(program) {
8
8
  .positional('type', {
9
9
  describe: 'Type of the resource to pull. Can be components, blueprints, configurations, plugins, blocks, or all.',
10
10
  type: 'string',
11
- choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'integrations', 'all', '*'],
11
+ choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'integrations', 'connections', 'all', '*'],
12
12
  required: true
13
13
  })
14
14
  .positional('path', {
package/commands/push.mjs CHANGED
@@ -8,7 +8,7 @@ export default function createCommand(program) {
8
8
  .positional('type', {
9
9
  describe: 'Type of the resource to push. Can be components, blueprints, configurations, plugins, blocks, or all.',
10
10
  type: 'string',
11
- choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'integrations', 'all', '*'],
11
+ choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'integrations', 'connections', 'all', '*'],
12
12
  required: true
13
13
  })
14
14
  .positional('path', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qelos/plugins-cli",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "CLI to manage QELOS plugins",
5
5
  "main": "cli.mjs",
6
6
  "bin": {
@@ -50,22 +50,29 @@ export async function pushComponents(sdk, path, options = {}) {
50
50
  component => component.identifier === targetIdentifier || component.componentName === componentName
51
51
  );
52
52
 
53
- if (existingComponent) {
54
- await sdk.components.update(existingComponent._id, {
55
- identifier: targetIdentifier,
56
- componentName: componentName,
57
- content,
58
- description: info.description || existingComponent.description || 'Component description'
59
- });
60
- logger.success(`Updated: ${componentName}`);
61
- } else {
62
- await sdk.components.create({
63
- identifier: targetIdentifier,
64
- componentName: componentName,
65
- content,
66
- description: targetDescription
67
- });
68
- logger.success(`Created: ${componentName}`);
53
+ try {
54
+ if (existingComponent) {
55
+ await sdk.components.update(existingComponent._id, {
56
+ identifier: targetIdentifier,
57
+ componentName: componentName,
58
+ content,
59
+ description: info.description || existingComponent.description || 'Component description'
60
+ });
61
+ logger.success(`Updated: ${componentName}`);
62
+ } else {
63
+ await sdk.components.create({
64
+ identifier: targetIdentifier,
65
+ componentName: componentName,
66
+ content,
67
+ description: targetDescription
68
+ });
69
+ logger.success(`Created: ${componentName}`);
70
+ }
71
+ } catch (error) {
72
+ // Extract reason from error details if available
73
+ const reason = error.details?.reason || error.message;
74
+ logger.error(`Failed to push component: ${componentName} - ${reason}`);
75
+ throw error;
69
76
  }
70
77
  }
71
78
  }));
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { logger } from './logger.mjs';
4
4
  import { appUrl } from './sdk.mjs';
5
+ import { extractConfigContent, resolveReferences } from './file-refs.mjs';
5
6
 
6
7
  /**
7
8
  * Push configurations from local directory to remote
@@ -45,6 +46,9 @@ export async function pushConfigurations(sdk, path, options = {}) {
45
46
 
46
47
  logger.step(`Pushing configuration: ${key}`);
47
48
 
49
+ // Resolve any $ref references in the configuration
50
+ configData = await resolveReferences(configData, path);
51
+
48
52
  // Special handling for app-configuration: ensure QELOS_URL hostname is in websiteUrls
49
53
  if (key === 'app-configuration') {
50
54
  try {
@@ -160,7 +164,10 @@ export async function pullConfigurations(sdk, targetPath) {
160
164
  // Remove fields that shouldn't be in the file
161
165
  const { _id, tenant, created, updated, ...relevantFields } = fullConfig;
162
166
 
163
- fs.writeFileSync(filePath, JSON.stringify(relevantFields, null, 2), 'utf-8');
167
+ // Extract content to files for supported config types
168
+ const processedConfig = extractConfigContent(relevantFields, targetPath);
169
+
170
+ fs.writeFileSync(filePath, JSON.stringify(processedConfig, null, 2), 'utf-8');
164
171
  logger.step(`Pulled: ${config.key}`);
165
172
  }));
166
173
 
@@ -0,0 +1,263 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { logger } from './logger.mjs';
4
+
5
+ /**
6
+ * Configuration mapping for which fields should be extracted to files for each config type
7
+ */
8
+ const CONFIG_EXTRACTION_MAP = {
9
+ 'ssr-scripts': {
10
+ fields: ['head', 'body'],
11
+ fileExtension: '.html',
12
+ subdirectory: 'html'
13
+ },
14
+ 'users-header': {
15
+ fields: ['html'],
16
+ fileExtension: '.html',
17
+ subdirectory: 'html'
18
+ }
19
+ };
20
+
21
+ /**
22
+ * Check if an integration is an AI agent that should have its pre_messages extracted
23
+ * @param {Object} integration - Integration object
24
+ * @returns {boolean} True if this is an AI agent with chatCompletion
25
+ */
26
+ function isAiAgent(integration) {
27
+ return (
28
+ integration &&
29
+ Array.isArray(integration.kind) &&
30
+ integration.kind.includes('qelos') &&
31
+ integration.trigger?.operation === 'chatCompletion' &&
32
+ integration.target?.operation === 'chatCompletion' &&
33
+ integration.target?.details?.pre_messages?.length > 0
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Convert a string to kebab-case for filenames
39
+ * @param {string} str - String to convert
40
+ * @returns {string} Kebab-cased string
41
+ */
42
+ function toKebabCase(str) {
43
+ return str
44
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
45
+ .replace(/[\s_]+/g, '-')
46
+ .toLowerCase();
47
+ }
48
+
49
+ /**
50
+ * Load content from a $ref reference
51
+ * @param {string} ref - Reference path (relative, absolute, or URL)
52
+ * @param {string} basePath - Base path for resolving relative references
53
+ * @returns {Promise<string>} Loaded content
54
+ */
55
+ export async function loadReference(ref, basePath) {
56
+ // Handle HTTP/HTTPS URLs
57
+ if (ref.startsWith('http://') || ref.startsWith('https://')) {
58
+ logger.debug(`Loading reference from URL: ${ref}`);
59
+ try {
60
+ const response = await fetch(ref);
61
+ if (!response.ok) {
62
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
63
+ }
64
+ return await response.text();
65
+ } catch (error) {
66
+ throw new Error(`Failed to load from URL ${ref}: ${error.message}`);
67
+ }
68
+ }
69
+
70
+ // Handle absolute and relative paths
71
+ const filePath = path.isAbsolute(ref)
72
+ ? ref
73
+ : path.resolve(basePath, ref);
74
+
75
+ logger.debug(`Loading reference from file: ${filePath}`);
76
+
77
+ if (!fs.existsSync(filePath)) {
78
+ throw new Error(`Referenced file does not exist: ${filePath}`);
79
+ }
80
+
81
+ return fs.readFileSync(filePath, 'utf-8');
82
+ }
83
+
84
+ /**
85
+ * Recursively resolve all $ref references in an object
86
+ * @param {any} obj - Object to resolve references in
87
+ * @param {string} basePath - Base path for resolving relative references
88
+ * @returns {Promise<any>} Object with all references resolved
89
+ */
90
+ export async function resolveReferences(obj, basePath) {
91
+ if (Array.isArray(obj)) {
92
+ return await Promise.all(obj.map(item => resolveReferences(item, basePath)));
93
+ }
94
+
95
+ if (obj && typeof obj === 'object') {
96
+ // Check if this is a $ref object
97
+ if (obj.$ref && typeof obj.$ref === 'string') {
98
+ return await loadReference(obj.$ref, basePath);
99
+ }
100
+
101
+ // Otherwise, recursively resolve all properties
102
+ const resolved = {};
103
+ for (const [key, value] of Object.entries(obj)) {
104
+ resolved[key] = await resolveReferences(value, basePath);
105
+ }
106
+ return resolved;
107
+ }
108
+
109
+ // Primitive value, return as-is
110
+ return obj;
111
+ }
112
+
113
+ /**
114
+ * Extract content from integration pre_messages to separate files
115
+ * @param {Object} integration - Integration object
116
+ * @param {string} integrationPath - Path where integration files are stored
117
+ * @param {string} fileName - Name of the integration file (without extension)
118
+ * @returns {Object} Updated integration with $ref objects
119
+ */
120
+ export function extractIntegrationContent(integration, integrationPath, fileName) {
121
+ // Check if this is an AI agent
122
+ if (!isAiAgent(integration)) {
123
+ return integration;
124
+ }
125
+
126
+ const updatedIntegration = JSON.parse(JSON.stringify(integration)); // Deep clone
127
+
128
+ // Create prompts subdirectory if needed
129
+ const extractDir = path.join(integrationPath, 'prompts');
130
+ if (!fs.existsSync(extractDir)) {
131
+ fs.mkdirSync(extractDir, { recursive: true });
132
+ logger.debug(`Created directory: ${extractDir}`);
133
+ }
134
+
135
+ // Extract the first pre_message content
136
+ const preMessage = updatedIntegration.target.details.pre_messages[0];
137
+ if (preMessage && preMessage.content) {
138
+ // Check if content is already a $ref
139
+ if (typeof preMessage.content === 'object' && preMessage.content.$ref) {
140
+ return updatedIntegration;
141
+ }
142
+
143
+ // Only extract if content is a string
144
+ if (typeof preMessage.content === 'string') {
145
+ const content = preMessage.content;
146
+
147
+ // Skip if content is empty or just whitespace
148
+ if (!content.trim()) {
149
+ return updatedIntegration;
150
+ }
151
+
152
+ // Generate filename
153
+ const baseName = path.basename(fileName, '.integration.json');
154
+ const mdFileName = `${baseName}.md`;
155
+ const mdFilePath = path.join(extractDir, mdFileName);
156
+ const relativeRef = `./prompts/${mdFileName}`;
157
+
158
+ // Write content to file
159
+ fs.writeFileSync(mdFilePath, content, 'utf-8');
160
+
161
+ // Replace with $ref
162
+ updatedIntegration.target.details.pre_messages[0].content = { $ref: relativeRef };
163
+ }
164
+ }
165
+
166
+ return updatedIntegration;
167
+ }
168
+
169
+ /**
170
+ * Extract all integrations that have content to be externalized
171
+ * @param {Array} integrations - Array of integration objects
172
+ * @param {string} integrationPath - Path where integration files are stored
173
+ * @returns {Array} Updated integrations with $ref objects
174
+ */
175
+ export function extractAllIntegrationContent(integrations, integrationPath) {
176
+ return integrations.map((integration, index) => {
177
+ // Generate a filename for this integration
178
+ const displayName = integration?.trigger?.details?.name ||
179
+ integration?.target?.details?.name ||
180
+ integration?._id ||
181
+ `integration-${index + 1}`;
182
+ const fileName = `${toKebabCase(displayName)}.integration.json`;
183
+
184
+ return extractIntegrationContent(integration, integrationPath, fileName);
185
+ });
186
+ }
187
+ export function extractConfigContent(config, configPath) {
188
+ const extractionConfig = CONFIG_EXTRACTION_MAP[config.key];
189
+
190
+ if (!extractionConfig || !config.metadata) {
191
+ return config;
192
+ }
193
+
194
+ const updatedConfig = JSON.parse(JSON.stringify(config)); // Deep clone
195
+ const { fields, fileExtension, subdirectory } = extractionConfig;
196
+
197
+ // Create subdirectory if needed
198
+ const extractDir = path.join(configPath, subdirectory);
199
+ if (!fs.existsSync(extractDir)) {
200
+ fs.mkdirSync(extractDir, { recursive: true });
201
+ logger.debug(`Created directory: ${extractDir}`);
202
+ }
203
+
204
+ // Extract each field
205
+ for (const field of fields) {
206
+ if (updatedConfig.metadata[field] && typeof updatedConfig.metadata[field] === 'string') {
207
+ const content = updatedConfig.metadata[field];
208
+
209
+ // Skip if content is empty or just whitespace
210
+ if (!content.trim()) {
211
+ continue;
212
+ }
213
+
214
+ // Generate filename - use config key if only one field, otherwise include field name
215
+ let fileName;
216
+ if (fields.length === 1) {
217
+ fileName = `${config.key}${fileExtension}`;
218
+ } else {
219
+ fileName = `${config.key}-${field}${fileExtension}`;
220
+ }
221
+ const filePath = path.join(extractDir, fileName);
222
+ const relativeRef = `./${subdirectory}/${fileName}`;
223
+
224
+ // Write content to file
225
+ fs.writeFileSync(filePath, content, 'utf-8');
226
+ logger.debug(`Extracted ${field} to: ${relativeRef}`);
227
+
228
+ // Replace with $ref
229
+ updatedConfig.metadata[field] = { $ref: relativeRef };
230
+ }
231
+ }
232
+
233
+ return updatedConfig;
234
+ }
235
+
236
+ /**
237
+ * Extract all configurations that have content to be externalized
238
+ * @param {Array} configs - Array of configuration objects
239
+ * @param {string} configPath - Path where config files are stored
240
+ * @returns {Array} Updated configurations with $ref objects
241
+ */
242
+ export function extractAllConfigContent(configs, configPath) {
243
+ return configs.map(config => extractConfigContent(config, configPath));
244
+ }
245
+
246
+ /**
247
+ * Recursively find all $ref objects in an object
248
+ * @param {any} obj - Object to search
249
+ * @param {Array} refs - Array to collect found references (used internally)
250
+ * @returns {Array} Array of found $ref paths
251
+ */
252
+ export function findAllRefs(obj, refs = []) {
253
+ if (Array.isArray(obj)) {
254
+ obj.forEach(item => findAllRefs(item, refs));
255
+ } else if (obj && typeof obj === 'object') {
256
+ if (obj.$ref && typeof obj.$ref === 'string') {
257
+ refs.push(obj.$ref);
258
+ } else {
259
+ Object.values(obj).forEach(value => findAllRefs(value, refs));
260
+ }
261
+ }
262
+ return refs;
263
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { logger } from './logger.mjs';
4
+ import { extractIntegrationContent, resolveReferences } from './file-refs.mjs';
4
5
 
5
6
  const INTEGRATION_FILE_EXTENSION = '.integration.json';
6
7
  const INTEGRATIONS_API_PATH = '/api/integrations';
@@ -75,6 +76,24 @@ function sanitizeIntegrationForFile(integration) {
75
76
  delete sanitized[field];
76
77
  }
77
78
  });
79
+
80
+ // Remove _id from internal objects
81
+ if (sanitized.trigger && sanitized.trigger._id) {
82
+ delete sanitized.trigger._id;
83
+ }
84
+
85
+ if (sanitized.target && sanitized.target._id) {
86
+ delete sanitized.target._id;
87
+ }
88
+
89
+ if (Array.isArray(sanitized.dataManipulation)) {
90
+ sanitized.dataManipulation.forEach(item => {
91
+ if (item._id) {
92
+ delete item._id;
93
+ }
94
+ });
95
+ }
96
+
78
97
  return sanitized;
79
98
  }
80
99
 
@@ -123,7 +142,11 @@ export async function pullIntegrations(sdk, targetPath) {
123
142
  integrations.forEach((integration, index) => {
124
143
  const fileName = buildFileName(integration, index, usedNames);
125
144
  const filePath = join(targetPath, fileName);
126
- writeIntegrationFile(filePath, sanitizeIntegrationForFile(integration));
145
+
146
+ // Extract content to files for AI agents
147
+ const processedIntegration = extractIntegrationContent(integration, targetPath, fileName);
148
+
149
+ writeIntegrationFile(filePath, sanitizeIntegrationForFile(processedIntegration));
127
150
  logger.step(`Pulled: ${getIntegrationDisplayName(integration) || integration._id || fileName}`);
128
151
  });
129
152
 
@@ -154,8 +177,12 @@ export async function pushIntegrations(sdk, path, options = {}) {
154
177
  try {
155
178
  const integrationData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
156
179
  validateIntegrationPayload(integrationData, file);
157
- const payload = toRequestPayload(integrationData);
158
- const displayName = getIntegrationDisplayName(integrationData) || file.replace(INTEGRATION_FILE_EXTENSION, '');
180
+
181
+ // Resolve any $ref references in the integration
182
+ const resolvedIntegration = await resolveReferences(integrationData, path);
183
+
184
+ const payload = toRequestPayload(resolvedIntegration);
185
+ const displayName = getIntegrationDisplayName(resolvedIntegration) || file.replace(INTEGRATION_FILE_EXTENSION, '');
159
186
 
160
187
  logger.step(`Pushing integration: ${displayName}`);
161
188
 
@@ -168,8 +195,13 @@ export async function pushIntegrations(sdk, path, options = {}) {
168
195
  logger.success(`Created: ${displayName}`);
169
196
  }
170
197
 
198
+ // Re-extract content to files to maintain $ref structure
199
+ // This ensures pre_messages are stored as prompt md files after pushing
200
+ const processedResponse = extractIntegrationContent(response, path, file);
201
+
171
202
  // Persist returned integration (with _id) back to disk
172
- writeIntegrationFile(filePath, sanitizeIntegrationForFile(response));
203
+ writeIntegrationFile(filePath, sanitizeIntegrationForFile(processedResponse));
204
+
173
205
  results.push({ status: 'fulfilled' });
174
206
  } catch (error) {
175
207
  logger.error(`Failed to push integration file ${file}`, error);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { logger } from './logger.mjs';
4
+ import { loadReference } from './file-refs.mjs';
4
5
 
5
6
  /**
6
7
  * Convert a string to kebab-case
@@ -59,41 +60,6 @@ export function extractMicroFrontendStructures(microFrontends, pluginPath) {
59
60
  });
60
61
  }
61
62
 
62
- /**
63
- * Load content from a $ref reference
64
- * @param {string} ref - Reference path (relative, absolute, or URL)
65
- * @param {string} basePath - Base path for resolving relative references
66
- * @returns {Promise<string>} Loaded content
67
- */
68
- async function loadReference(ref, basePath) {
69
- // Handle HTTP/HTTPS URLs
70
- if (ref.startsWith('http://') || ref.startsWith('https://')) {
71
- logger.debug(`Loading structure from URL: ${ref}`);
72
- try {
73
- const response = await fetch(ref);
74
- if (!response.ok) {
75
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
76
- }
77
- return await response.text();
78
- } catch (error) {
79
- throw new Error(`Failed to load from URL ${ref}: ${error.message}`);
80
- }
81
- }
82
-
83
- // Handle absolute and relative paths
84
- const filePath = path.isAbsolute(ref)
85
- ? ref
86
- : path.resolve(basePath, ref);
87
-
88
- logger.debug(`Loading structure from file: ${filePath}`);
89
-
90
- if (!fs.existsSync(filePath)) {
91
- throw new Error(`Referenced file does not exist: ${filePath}`);
92
- }
93
-
94
- return fs.readFileSync(filePath, 'utf-8');
95
- }
96
-
97
63
  /**
98
64
  * Resolve micro-frontend structures from $ref references
99
65
  * @param {Array} microFrontends - Array of micro-frontend objects
@@ -4,6 +4,48 @@ import { join } from 'node:path';
4
4
  import { logger } from './logger.mjs';
5
5
  import { extractMicroFrontendStructures, resolveMicroFrontendStructures } from './micro-frontends.mjs';
6
6
 
7
+ function sanitizePluginForFile(plugin) {
8
+ const sanitized = JSON.parse(JSON.stringify(plugin));
9
+
10
+ // Remove _id from internal objects in arrays
11
+ if (Array.isArray(sanitized.subscribedEvents)) {
12
+ sanitized.subscribedEvents.forEach(item => {
13
+ if (item._id) delete item._id;
14
+ });
15
+ }
16
+
17
+ if (Array.isArray(sanitized.microFrontends)) {
18
+ sanitized.microFrontends.forEach(mfe => {
19
+ if (mfe._id) delete mfe._id;
20
+ if (Array.isArray(mfe.requires)) {
21
+ mfe.requires.forEach(item => {
22
+ if (item._id) delete item._id;
23
+ });
24
+ }
25
+ });
26
+ }
27
+
28
+ if (Array.isArray(sanitized.injectables)) {
29
+ sanitized.injectables.forEach(item => {
30
+ if (item._id) delete item._id;
31
+ });
32
+ }
33
+
34
+ if (Array.isArray(sanitized.navBarGroups)) {
35
+ sanitized.navBarGroups.forEach(item => {
36
+ if (item._id) delete item._id;
37
+ });
38
+ }
39
+
40
+ if (Array.isArray(sanitized.cruds)) {
41
+ sanitized.cruds.forEach(item => {
42
+ if (item._id) delete item._id;
43
+ });
44
+ }
45
+
46
+ return sanitized;
47
+ }
48
+
7
49
  /**
8
50
  * Push plugins from local directory to remote
9
51
  * @param {Object} sdk - Initialized SDK instance
@@ -179,7 +221,10 @@ export async function pullPlugins(sdk, targetPath) {
179
221
  cruds: (fullPlugin.cruds || []).map(removeIdFromObject),
180
222
  }
181
223
 
182
- fs.writeFileSync(filePath, JSON.stringify(relevantFields, null, 2), 'utf-8');
224
+ // Sanitize the plugin to remove any remaining _id fields from internal objects
225
+ const sanitizedPlugin = sanitizePluginForFile(relevantFields);
226
+
227
+ fs.writeFileSync(filePath, JSON.stringify(sanitizedPlugin, null, 2), 'utf-8');
183
228
  logger.step(`Pulled: ${plugin.apiPath}`);
184
229
  }));
185
230