@gitsense/gsc-utils 0.2.3 → 0.2.5
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/dist/gsc-utils.cjs.js +752 -45
- package/dist/gsc-utils.esm.js +752 -45
- package/package.json +1 -1
- package/src/AnalyzerUtils/discovery.js +139 -0
- package/src/AnalyzerUtils/index.js +11 -3
- package/src/AnalyzerUtils/instructionLoader.js +58 -0
- package/src/AnalyzerUtils/management.js +131 -0
- package/src/AnalyzerUtils/schemaLoader.js +163 -0
- package/src/CodeBlockUtils/continuationUtils.js +3 -3
- package/src/ConfigUtils.js +86 -0
- package/src/EnvUtils.js +88 -0
- package/src/GitSenseChatUtils.js +57 -16
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Component: AnalyzerUtils Discovery
|
|
3
|
+
* Block-UUID: 0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5f
|
|
4
|
+
* Parent-UUID: N/A
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
* Description: Provides utility functions for discovering available analyzers.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-08-28T23:48:00.000Z
|
|
9
|
+
* Authors: Gemini 2.5 Flash (v1.0.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const fs = require('fs').promises;
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads and parses the config.json file in a directory.
|
|
18
|
+
* @param {string} dirPath - The path to the directory.
|
|
19
|
+
* @returns {Promise<object|null>} A promise that resolves to the parsed config object
|
|
20
|
+
* or null if the file doesn't exist or is invalid.
|
|
21
|
+
*/
|
|
22
|
+
async function readConfig(dirPath) {
|
|
23
|
+
const configPath = path.join(dirPath, 'config.json');
|
|
24
|
+
try {
|
|
25
|
+
const fileContent = await fs.readFile(configPath, 'utf8');
|
|
26
|
+
return JSON.parse(fileContent);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code !== 'ENOENT') {
|
|
29
|
+
// Log a warning if config.json exists but is malformed
|
|
30
|
+
console.warn(`Warning: Failed to parse config.json in ${dirPath}: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
return null; // Return null if file not found or parsing failed
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checks if a directory name is valid based on the rules in messages/analyze/README.md.
|
|
38
|
+
* Allowed: a-z, A-Z, 0-9, dash (-), underscore (_). Cannot start with underscore or contain dots.
|
|
39
|
+
* @param {string} name - The directory name to check.
|
|
40
|
+
* @returns {boolean} True if the name is valid, false otherwise.
|
|
41
|
+
*/
|
|
42
|
+
function isValidDirName(name) {
|
|
43
|
+
// Exclude names starting with underscore or containing dots
|
|
44
|
+
if (name.startsWith('_') || name.includes('.')) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// Check for allowed characters
|
|
48
|
+
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Discovers and lists all available analyzers by traversing the directory structure.
|
|
53
|
+
* An analyzer is considered valid if a '1.md' file exists in the instructions directory.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} analyzeMessagesBasePath - The absolute or relative path to the base directory containing the analyzer message files (e.g., 'messages/analyze').
|
|
56
|
+
* @returns {Promise<Array<{id: string, label: string}>>} A promise that resolves to an array of analyzer objects.
|
|
57
|
+
*/
|
|
58
|
+
async function getAnalyzers(analyzeMessagesBasePath) {
|
|
59
|
+
const analyzers = [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const analyzerEntries = await fs.readdir(analyzeMessagesBasePath, { withFileTypes: true });
|
|
63
|
+
|
|
64
|
+
for (const analyzerEntry of analyzerEntries) {
|
|
65
|
+
if (analyzerEntry.isDirectory() && isValidDirName(analyzerEntry.name)) {
|
|
66
|
+
const analyzerName = analyzerEntry.name;
|
|
67
|
+
const analyzerPath = path.join(analyzeMessagesBasePath, analyzerName);
|
|
68
|
+
const analyzerConfig = await readConfig(analyzerPath);
|
|
69
|
+
const analyzerLabel = analyzerConfig?.label || analyzerName;
|
|
70
|
+
|
|
71
|
+
const contentEntries = await fs.readdir(analyzerPath, { withFileTypes: true });
|
|
72
|
+
|
|
73
|
+
for (const contentEntry of contentEntries) {
|
|
74
|
+
if (contentEntry.isDirectory() && isValidDirName(contentEntry.name)) {
|
|
75
|
+
const contentType = contentEntry.name;
|
|
76
|
+
const contentPath = path.join(analyzerPath, contentType);
|
|
77
|
+
const contentConfig = await readConfig(contentPath);
|
|
78
|
+
const contentLabel = contentConfig?.label || contentType;
|
|
79
|
+
|
|
80
|
+
const instructionsEntries = await fs.readdir(contentPath, { withFileTypes: true });
|
|
81
|
+
|
|
82
|
+
for (const instructionsEntry of instructionsEntries) {
|
|
83
|
+
if (instructionsEntry.isDirectory() && isValidDirName(instructionsEntry.name)) {
|
|
84
|
+
const instructionsType = instructionsEntry.name;
|
|
85
|
+
const instructionsPath = path.join(contentPath, instructionsType);
|
|
86
|
+
const instructionsConfig = await readConfig(instructionsPath);
|
|
87
|
+
const instructionsLabel = instructionsConfig?.label || instructionsType;
|
|
88
|
+
|
|
89
|
+
// Check for the existence of 1.md to confirm a valid analyzer configuration
|
|
90
|
+
const instructionsFilePath = path.join(instructionsPath, '1.md');
|
|
91
|
+
try {
|
|
92
|
+
await fs.access(instructionsFilePath); // Check if file exists and is accessible
|
|
93
|
+
|
|
94
|
+
// If analyzerName starts with 'tutorial-', check its last modified time.
|
|
95
|
+
if (analyzerName.startsWith('tutorial-')) {
|
|
96
|
+
const stats = await fs.stat(instructionsFilePath);
|
|
97
|
+
const lastModified = stats.mtime.getTime(); // Get timestamp in milliseconds
|
|
98
|
+
const sixtyMinutesAgo = Date.now() - (60 * 60 * 1000); // Current time - 60 minutes in ms
|
|
99
|
+
|
|
100
|
+
if (lastModified < sixtyMinutesAgo) {
|
|
101
|
+
// This tutorial analyzer has expired, skip it.
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Construct the analyzer ID and label
|
|
106
|
+
const analyzerId = `${analyzerName}::${contentType}::${instructionsType}`;
|
|
107
|
+
const analyzerFullLabel = `${analyzerLabel} (${contentLabel} - ${instructionsLabel})`;
|
|
108
|
+
|
|
109
|
+
analyzers.push({
|
|
110
|
+
id: analyzerId,
|
|
111
|
+
label: analyzerFullLabel,
|
|
112
|
+
protected: analyzerConfig?.protected || false
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// If 1.md doesn't exist, this is not a complete analyzer configuration, skip.
|
|
116
|
+
if (error.code !== 'ENOENT') {
|
|
117
|
+
console.warn(`Warning: Error accessing 1.md for ${analyzerId}: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`Error traversing analyze messages directory ${analyzeMessagesBasePath}: ${error.message}`);
|
|
128
|
+
// Depending on requirements, you might want to throw the error or return an empty array
|
|
129
|
+
throw error; // Re-throw to indicate failure
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return analyzers;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
getAnalyzers,
|
|
137
|
+
readConfig,
|
|
138
|
+
isValidDirName,
|
|
139
|
+
};
|
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
* Component: AnalyzerUtils Index
|
|
3
3
|
* Block-UUID: b403b6a1-230b-4247-8cd6-2a3d068f4bbf
|
|
4
4
|
* Parent-UUID: N/A
|
|
5
|
-
* Version: 1.
|
|
5
|
+
* Version: 1.1.0
|
|
6
6
|
* Description: Aggregates and exports all utility functions from the AnalyzerUtils module.
|
|
7
7
|
* Language: JavaScript
|
|
8
8
|
* Created-at: 2025-08-28T15:56:40.319Z
|
|
9
|
-
* Authors: Gemini 2.5 Flash (v1.0.0)
|
|
9
|
+
* Authors: Gemini 2.5 Flash (v1.0.0), Gemini 2.5 Flash (v1.1.0)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
const { buildChatIdToPathMap } = require('./contextMapper');
|
|
14
14
|
const { processLLMAnalysisResponse } = require('./responseProcessor');
|
|
15
15
|
const { validateLLMAnalysisData } = require('./dataValidator');
|
|
16
|
+
const { getAnalyzers } = require('./discovery');
|
|
17
|
+
const { getAnalyzerSchema } = require('./schemaLoader');
|
|
18
|
+
const { deleteAnalyzer } = require('./management');
|
|
19
|
+
const { getAnalyzerInstructionsContent } = require('./instructionLoader');
|
|
16
20
|
|
|
17
21
|
module.exports = {
|
|
18
22
|
buildChatIdToPathMap,
|
|
19
23
|
processLLMAnalysisResponse,
|
|
20
|
-
validateLLMAnalysisData
|
|
24
|
+
validateLLMAnalysisData,
|
|
25
|
+
getAnalyzers,
|
|
26
|
+
getAnalyzerSchema,
|
|
27
|
+
deleteAnalyzer,
|
|
28
|
+
getAnalyzerInstructionsContent,
|
|
21
29
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Component: AnalyzerUtils Instruction Loader
|
|
3
|
+
* Block-UUID: 0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5e
|
|
4
|
+
* Parent-UUID: N/A
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
* Description: Provides utility functions for loading raw analyzer instruction content.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-08-28T23:48:00.000Z
|
|
9
|
+
* Authors: Gemini 2.5 Flash (v1.0.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const fs = require('fs').promises;
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves the raw Markdown content of the analyzer's '1.md' instruction file.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} analyzeMessagesBasePath - The absolute path to the base directory containing the analyzer message files (e.g., 'messages/analyze').
|
|
20
|
+
* @param {string} analyzerId - The unique ID of the analyzer (format: 'analyzer_name::content_type::instructions_type').
|
|
21
|
+
* @returns {Promise<string|null>} A promise that resolves with the full Markdown content of the '1.md' file, or null if not found/invalid.
|
|
22
|
+
*/
|
|
23
|
+
async function getAnalyzerInstructionsContent(analyzeMessagesBasePath, analyzerId) {
|
|
24
|
+
if (typeof analyzeMessagesBasePath !== 'string' || analyzeMessagesBasePath.trim() === '') {
|
|
25
|
+
console.error('Error: analyzeMessagesBasePath is required.');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (typeof analyzerId !== 'string' || analyzerId.trim() === '') {
|
|
29
|
+
console.error('Error: analyzerId is required.');
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parts = analyzerId.split('::');
|
|
34
|
+
if (parts.length !== 3) {
|
|
35
|
+
console.error(`Error: Invalid analyzerId format. Expected 'analyzer_name::content_type::instructions_type', but got '${analyzerId}'.`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const [analyzerName, contentType, instructionsType] = parts;
|
|
39
|
+
|
|
40
|
+
const instructionsFilePath = path.join(analyzeMessagesBasePath, analyzerName, contentType, instructionsType, '1.md');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const fileContent = await fs.readFile(instructionsFilePath, 'utf8');
|
|
44
|
+
return fileContent;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === 'ENOENT') {
|
|
47
|
+
console.warn(`Analyzer instructions file not found: ${instructionsFilePath}`);
|
|
48
|
+
return null;
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`Error reading analyzer instructions file ${instructionsFilePath}: ${error.message}`);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
getAnalyzerInstructionsContent
|
|
58
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Component: AnalyzerUtils Management
|
|
3
|
+
* Block-UUID: 0d1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a
|
|
4
|
+
* Parent-UUID: N/A
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
* Description: Provides utility functions for managing (deleting) analyzer configurations.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-08-28T23:48:00.000Z
|
|
9
|
+
* Authors: Gemini 2.5 Flash (v1.0.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const fs = require('fs').promises;
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { readConfig } = require('./discovery'); // Import helper from discovery
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a directory is empty or only contains a config.json.
|
|
19
|
+
* @param {string} dirPath - The path to the directory.
|
|
20
|
+
* @returns {Promise<boolean>} True if the directory is empty or only contains config.json, false otherwise.
|
|
21
|
+
*/
|
|
22
|
+
async function isDirectoryEmpty(dirPath) {
|
|
23
|
+
try {
|
|
24
|
+
const files = await fs.readdir(dirPath);
|
|
25
|
+
return files.length === 0 || (files.length === 1 && files[0] === 'config.json');
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT') {
|
|
28
|
+
return true; // Directory doesn't exist, so it's "empty" for our purpose
|
|
29
|
+
}
|
|
30
|
+
throw error; // Re-throw other errors
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Deletes a specific analyzer configuration and intelligently cleans up empty directories.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} analyzeMessagesBasePath - The absolute or relative path to the base directory containing the analyzer message files (e.g., 'messages/analyze').
|
|
38
|
+
* @param {string} analyzerId - The unique ID of the analyzer to delete (format: 'analyzer_name::content_type::instructions_type').
|
|
39
|
+
* @returns {Promise<{success: boolean, message: string}>} A promise that resolves with a result object indicating success or failure.
|
|
40
|
+
*/
|
|
41
|
+
async function deleteAnalyzer(analyzeMessagesBasePath, analyzerId) {
|
|
42
|
+
if (typeof analyzeMessagesBasePath !== 'string' || analyzeMessagesBasePath.trim() === '') {
|
|
43
|
+
return { success: false, message: 'analyzeMessagesBasePath is required.' };
|
|
44
|
+
}
|
|
45
|
+
if (typeof analyzerId !== 'string' || analyzerId.trim() === '') {
|
|
46
|
+
return { success: false, message: 'analyzerId is required.' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parts = analyzerId.split('::');
|
|
50
|
+
if (parts.length !== 3) {
|
|
51
|
+
return { success: false, message: `Invalid analyzerId format. Expected 'analyzer_name::content_type::instructions_type', but got '${analyzerId}'.` };
|
|
52
|
+
}
|
|
53
|
+
const [analyzerName, contentType, instructionsType] = parts;
|
|
54
|
+
|
|
55
|
+
const analyzerDir = path.join(analyzeMessagesBasePath, analyzerName);
|
|
56
|
+
const contentDir = path.join(analyzerDir, contentType);
|
|
57
|
+
const instructionsDir = path.join(contentDir, instructionsType);
|
|
58
|
+
const instructionsFilePath = path.join(instructionsDir, '1.md');
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// 1. Check for protection at all levels
|
|
62
|
+
const analyzerConfig = await readConfig(analyzerDir);
|
|
63
|
+
if (analyzerConfig?.protected) {
|
|
64
|
+
return { success: false, message: `Analyzer '${analyzerName}' is protected and cannot be deleted.` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const contentConfig = await readConfig(contentDir);
|
|
68
|
+
if (contentConfig?.protected) {
|
|
69
|
+
return { success: false, message: `Content type '${contentType}' for analyzer '${analyzerName}' is protected and cannot be deleted.` };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const instructionsConfig = await readConfig(instructionsDir);
|
|
73
|
+
if (instructionsConfig?.protected) {
|
|
74
|
+
return { success: false, message: `Instructions type '${instructionsType}' for content type '${contentType}' is protected and cannot be deleted.` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Delete the 1.md file
|
|
78
|
+
try {
|
|
79
|
+
await fs.unlink(instructionsFilePath);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.code === 'ENOENT') {
|
|
82
|
+
return { success: false, message: `Analyzer instructions file not found: ${instructionsFilePath}. It may have already been deleted.` };
|
|
83
|
+
}
|
|
84
|
+
throw error; // Re-throw other errors
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Intelligently delete empty directories, cascading upwards
|
|
88
|
+
let deletedDirs = [];
|
|
89
|
+
|
|
90
|
+
// Check and delete instructions directory
|
|
91
|
+
if (await isDirectoryEmpty(instructionsDir)) {
|
|
92
|
+
try {
|
|
93
|
+
await fs.rmdir(instructionsDir);
|
|
94
|
+
deletedDirs.push(instructionsDir);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn(`Warning: Could not remove empty instructions directory ${instructionsDir}: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check and delete content directory
|
|
101
|
+
if (await isDirectoryEmpty(contentDir)) {
|
|
102
|
+
try {
|
|
103
|
+
await fs.rmdir(contentDir);
|
|
104
|
+
deletedDirs.push(contentDir);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn(`Warning: Could not remove empty content directory ${contentDir}: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check and delete analyzer directory
|
|
111
|
+
if (await isDirectoryEmpty(analyzerDir)) {
|
|
112
|
+
try {
|
|
113
|
+
await fs.rmdir(analyzerDir);
|
|
114
|
+
deletedDirs.push(analyzerDir);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn(`Warning: Could not remove empty analyzer directory ${analyzerDir}: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { success: true, message: `Analyzer '${analyzerId}' deleted successfully. Cleaned up directories: ${deletedDirs.join(', ') || 'None'}.` };
|
|
121
|
+
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Error deleting analyzer '${analyzerId}': ${error.message}`);
|
|
124
|
+
return { success: false, message: `Failed to delete analyzer: ${error.message}` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
deleteAnalyzer,
|
|
130
|
+
isDirectoryEmpty,
|
|
131
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Component: AnalyzerUtils Schema Loader
|
|
3
|
+
* Block-UUID: 0c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f
|
|
4
|
+
* Parent-UUID: N/A
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
* Description: Provides utility functions for retrieving and deducing JSON schemas for analyzers.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-08-28T23:48:00.000Z
|
|
9
|
+
* Authors: Gemini 2.5 Flash (v1.0.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const fs = require('fs').promises;
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const CodeBlockUtils = require('../CodeBlockUtils');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Deduces the JSON schema type and format/items from a string value pattern.
|
|
19
|
+
* Handles patterns like "[string: ...]", "[number: ...]", "[datetime: ...]", "[<string>: ...]",
|
|
20
|
+
* and boolean instructions. Defaults to 'string' for unknown patterns.
|
|
21
|
+
*
|
|
22
|
+
* @param {any} value - The value from the raw JSON (expected to be a string pattern).
|
|
23
|
+
* @param {string} fieldName - The name of the field (for logging warnings).
|
|
24
|
+
* @returns {{type: string, format?: string, items?: object}} An object describing the schema type and format/items.
|
|
25
|
+
*/
|
|
26
|
+
function deduceSchemaType(value, fieldName) {
|
|
27
|
+
const defaultSchema = { type: 'string' }; // Default fallback
|
|
28
|
+
|
|
29
|
+
if (typeof value !== 'string') {
|
|
30
|
+
const jsType = typeof value;
|
|
31
|
+
if (jsType === 'object' && value !== null) {
|
|
32
|
+
console.warn(`Warning: Unexpected non-string, non-null object/array value for field "${fieldName}". Defaulting to type 'string'. Value:`, value);
|
|
33
|
+
return defaultSchema;
|
|
34
|
+
}
|
|
35
|
+
return { type: jsType };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const trimmedValue = value.trim();
|
|
39
|
+
|
|
40
|
+
if (/^\[string:.*\]$/.test(trimmedValue) || (/^\[[^:]+\]$/.test(trimmedValue) && !/^\[(number|datetime|date|<string>):.*\]$/.test(trimmedValue))) {
|
|
41
|
+
return { type: 'string' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (/^\[number:.*\]$/.test(trimmedValue)) {
|
|
45
|
+
return { type: 'number' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (/^\[boolean:.*\]$/.test(trimmedValue)) {
|
|
49
|
+
return { type: 'boolean' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (/^\[date-*time:.*\]$/.test(trimmedValue)) {
|
|
53
|
+
return { type: 'string', format: 'date-time' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (/^\[date:.*\]$/.test(trimmedValue)) {
|
|
57
|
+
return { type: 'string', format: 'date' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (/^\[<string>:.*\]$/.test(trimmedValue) || trimmedValue.toLowerCase().includes('array of strings')) {
|
|
61
|
+
return { type: 'array', items: { type: 'string' } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (trimmedValue.toLowerCase().includes("output 'true' or 'false'") || trimmedValue.toLowerCase().includes("determine if") && (trimmedValue.toLowerCase().includes("true") || trimmedValue.toLowerCase().includes("false"))) {
|
|
65
|
+
return { type: 'boolean' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.warn(`Warning: Unknown metadata value pattern for field "${fieldName}". Defaulting to type 'string'. Value: "${value}"`);
|
|
69
|
+
return defaultSchema;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Retrieves the JSON schema for a specific analyzer.
|
|
75
|
+
* Reads the corresponding '1.md' file, extracts the JSON block,
|
|
76
|
+
* and deduces schema types from the string values.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} analyzeMessagesBasePath - The absolute or relative path to the base directory containing the analyzer message files (e.g., 'messages/analyze').
|
|
79
|
+
* @param {string} analyzerId - The unique ID of the analyzer (format: 'analyzer_name::content_type::instructions_type').
|
|
80
|
+
* @returns {Promise<object|null>} A promise that resolves with the JSON schema object or null if the analyzer ID is invalid or the schema cannot be retrieved/parsed.
|
|
81
|
+
* @throws {Error} If the 1.md file is found but does not contain exactly one JSON code block.
|
|
82
|
+
*/
|
|
83
|
+
async function getAnalyzerSchema(analyzeMessagesBasePath, analyzerId) {
|
|
84
|
+
if (typeof analyzeMessagesBasePath !== 'string' || analyzeMessagesBasePath.trim() === '') {
|
|
85
|
+
console.error('Error: analyzeMessagesBasePath is required.');
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (typeof analyzerId !== 'string' || analyzerId.trim() === '') {
|
|
89
|
+
console.error('Error: analyzerId is required.');
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const parts = analyzerId.split('::');
|
|
94
|
+
if (parts.length !== 3) {
|
|
95
|
+
console.error(`Error: Invalid analyzerId format. Expected 'analyzer_name::content_type::instructions_type', but got '${analyzerId}'.`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const [analyzerName, contentType, instructionsType] = parts;
|
|
99
|
+
|
|
100
|
+
const instructionsFilePath = path.join(analyzeMessagesBasePath, analyzerName, contentType, instructionsType, '1.md');
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const fileContent = await fs.readFile(instructionsFilePath, 'utf8');
|
|
104
|
+
const { blocks } = CodeBlockUtils.extractCodeBlocks(fileContent, { silent: true });
|
|
105
|
+
const jsonBlocks = blocks.filter(block => block.type === 'code' && block.language === 'json');
|
|
106
|
+
|
|
107
|
+
if (jsonBlocks.length !== 1) {
|
|
108
|
+
throw new Error(`Expected exactly one JSON code block in ${instructionsFilePath}, but found ${jsonBlocks.length}.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const jsonBlockContent = jsonBlocks[0].content;
|
|
112
|
+
let rawJson = null;
|
|
113
|
+
try {
|
|
114
|
+
rawJson = JSON.parse(jsonBlockContent);
|
|
115
|
+
} catch (parseError) {
|
|
116
|
+
console.error(`Error parsing JSON content from ${instructionsFilePath}: ${parseError.message}`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const schema = {
|
|
121
|
+
type: 'object',
|
|
122
|
+
description: rawJson.description,
|
|
123
|
+
properties: {},
|
|
124
|
+
required: []
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const metadataProperties = rawJson?.extracted_metadata;
|
|
128
|
+
|
|
129
|
+
if (metadataProperties && typeof metadataProperties === 'object') {
|
|
130
|
+
for (const fieldName in metadataProperties) {
|
|
131
|
+
if (Object.hasOwnProperty.call(metadataProperties, fieldName)) {
|
|
132
|
+
const rawValue = metadataProperties[fieldName];
|
|
133
|
+
const fieldSchema = deduceSchemaType(rawValue, fieldName);
|
|
134
|
+
const description = rawValue.match(/^\[\w+: ([^\]]+)\]/)?.[1] || '';
|
|
135
|
+
|
|
136
|
+
schema.properties[fieldName] = {
|
|
137
|
+
...fieldSchema,
|
|
138
|
+
description,
|
|
139
|
+
title: fieldName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
console.warn(`Warning: Could not find 'extracted_metadata' object in JSON block from ${instructionsFilePath}. Schema will be empty.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return schema;
|
|
148
|
+
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error.code === 'ENOENT') {
|
|
151
|
+
console.warn(`Analyzer instructions file not found: ${instructionsFilePath}`);
|
|
152
|
+
return null;
|
|
153
|
+
} else {
|
|
154
|
+
console.error(`Error retrieving or processing schema for analyzer ${analyzerId}: ${error.message}`);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
getAnalyzerSchema,
|
|
162
|
+
deduceSchemaType
|
|
163
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
2
|
* Component: CodeBlockUtils Continuation Utilities
|
|
3
3
|
* Block-UUID: 6fdf5c7d-4def-42a1-bc88-0312f6002cfc
|
|
4
4
|
* Parent-UUID: 6322afcd-2e5e-425a-9a43-4c72c887f668
|
|
@@ -165,8 +165,8 @@ Example of the START of your response:
|
|
|
165
165
|
\`\`\`${language}
|
|
166
166
|
/**
|
|
167
167
|
* Component: ${componentName}
|
|
168
|
-
* Block-UUID: 01147ddf-320f-498a-a744-198d42a9d2ee
|
|
169
|
-
* Parent-UUID: e4c0d839-ea17-467d-a0cc-d269d1dbc404
|
|
168
|
+
* Block-UUID: 01147ddf-320f-498a-a744-198d42a9d2ee
|
|
169
|
+
* Parent-UUID: e4c0d839-ea17-467d-a0cc-d269d1dbc404
|
|
170
170
|
* Version: ${nextVersion}
|
|
171
171
|
* Description: ${description}
|
|
172
172
|
* Language: ${language}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Component: Config Utilities
|
|
3
|
+
* Block-UUID: 89016e46-9898-42f0-bef4-564e442cfaf3
|
|
4
|
+
* Parent-UUID: N/A
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
* Description: Provides utility functions for loading and accessing application configuration from data/chats.json.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-08-28T17:46:26.948Z
|
|
9
|
+
* Authors: Gemini 2.5 Flash (v1.0.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const fs = require('fs').promises;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Loads and parses the application configuration from the specified JSON file.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} filePath - The absolute path to the data/chats.json file.
|
|
19
|
+
* @returns {Promise<Object>} A promise that resolves with the parsed configuration object.
|
|
20
|
+
* @throws {Error} If the file cannot be read or parsed.
|
|
21
|
+
*/
|
|
22
|
+
async function loadConfig(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
25
|
+
return JSON.parse(fileContent);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT') {
|
|
28
|
+
throw new Error(`Configuration file not found at: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Failed to load or parse configuration from ${filePath}: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves the configuration details for a specific LLM provider.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} config - The loaded configuration object.
|
|
38
|
+
* @param {string} providerName - The name of the LLM provider (e.g., "Google", "Anthropic").
|
|
39
|
+
* @returns {Object|null} The provider's configuration object or null if not found.
|
|
40
|
+
*/
|
|
41
|
+
function getProviderConfig(config, providerName) {
|
|
42
|
+
if (!config || !config.providers || !Array.isArray(config.providers)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return config.providers.find(p => p.name === providerName) || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Finds the specific modelId and other details for a given user-friendly modelName and providerName.
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} config - The loaded configuration object.
|
|
52
|
+
* @param {string} modelName - The user-friendly name of the model (e.g., "Gemini 2.5 Flash").
|
|
53
|
+
* @param {string} providerName - The name of the provider associated with that model.
|
|
54
|
+
* @returns {Object|null} An object containing modelId, maxOutputTokens, and other provider-specific model details, or null if not found.
|
|
55
|
+
*/
|
|
56
|
+
function getModelProviderDetails(config, modelName, providerName) {
|
|
57
|
+
if (!config || !config.models || !Array.isArray(config.models)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const modelEntry = config.models.find(m => m.name === modelName);
|
|
62
|
+
if (!modelEntry || !modelEntry.providers || !Array.isArray(modelEntry.providers)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return modelEntry.providers.find(p => p.name === providerName) || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Retrieves the environment variable name (e.g., "GEMINI_API_KEY") for the API key of a specific provider.
|
|
71
|
+
*
|
|
72
|
+
* @param {Object} config - The loaded configuration object.
|
|
73
|
+
* @param {string} providerName - The name of the LLM provider.
|
|
74
|
+
* @returns {string|null} The name of the environment variable that holds the API key, or null if not found.
|
|
75
|
+
*/
|
|
76
|
+
function getApiKeyName(config, providerName) {
|
|
77
|
+
const providerConfig = getProviderConfig(config, providerName);
|
|
78
|
+
return providerConfig ? providerConfig.apiKeyName : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
loadConfig,
|
|
83
|
+
getProviderConfig,
|
|
84
|
+
getModelProviderDetails,
|
|
85
|
+
getApiKeyName
|
|
86
|
+
};
|