@gitsense/gsc-utils 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/dist/gitsense-chat-utils.cjs.js +10977 -0
  3. package/dist/gitsense-chat-utils.esm.js +10975 -0
  4. package/dist/gsc-utils.cjs.js +11043 -0
  5. package/dist/gsc-utils.esm.js +11041 -0
  6. package/package.json +37 -0
  7. package/src/AnalysisBlockUtils.js +151 -0
  8. package/src/ChatUtils.js +126 -0
  9. package/src/CodeBlockUtils/blockExtractor.js +277 -0
  10. package/src/CodeBlockUtils/blockProcessor.js +559 -0
  11. package/src/CodeBlockUtils/blockProcessor.js.rej +8 -0
  12. package/src/CodeBlockUtils/constants.js +62 -0
  13. package/src/CodeBlockUtils/continuationUtils.js +191 -0
  14. package/src/CodeBlockUtils/headerParser.js +175 -0
  15. package/src/CodeBlockUtils/headerUtils.js +236 -0
  16. package/src/CodeBlockUtils/index.js +83 -0
  17. package/src/CodeBlockUtils/lineNumberFormatter.js +117 -0
  18. package/src/CodeBlockUtils/markerRemover.js +89 -0
  19. package/src/CodeBlockUtils/patchIntegration.js +38 -0
  20. package/src/CodeBlockUtils/relationshipUtils.js +159 -0
  21. package/src/CodeBlockUtils/updateCodeBlock.js +372 -0
  22. package/src/CodeBlockUtils/uuidUtils.js +48 -0
  23. package/src/ContextUtils.js +180 -0
  24. package/src/GSToolBlockUtils.js +108 -0
  25. package/src/GitSenseChatUtils.js +386 -0
  26. package/src/JsonUtils.js +101 -0
  27. package/src/LLMUtils.js +31 -0
  28. package/src/MessageUtils.js +460 -0
  29. package/src/PatchUtils/constants.js +72 -0
  30. package/src/PatchUtils/diagnosticReporter.js +213 -0
  31. package/src/PatchUtils/enhancedPatchProcessor.js +390 -0
  32. package/src/PatchUtils/fuzzyMatcher.js +252 -0
  33. package/src/PatchUtils/hunkCorrector.js +204 -0
  34. package/src/PatchUtils/hunkValidator.js +305 -0
  35. package/src/PatchUtils/index.js +135 -0
  36. package/src/PatchUtils/patchExtractor.js +175 -0
  37. package/src/PatchUtils/patchHeaderFormatter.js +143 -0
  38. package/src/PatchUtils/patchParser.js +289 -0
  39. package/src/PatchUtils/patchProcessor.js +389 -0
  40. package/src/PatchUtils/patchVerifier/constants.js +23 -0
  41. package/src/PatchUtils/patchVerifier/detectAndFixOverlappingHunks.js +281 -0
  42. package/src/PatchUtils/patchVerifier/detectAndFixRedundantChanges.js +404 -0
  43. package/src/PatchUtils/patchVerifier/formatAndAddLineNumbers.js +165 -0
  44. package/src/PatchUtils/patchVerifier/index.js +25 -0
  45. package/src/PatchUtils/patchVerifier/verifyAndCorrectHunkHeaders.js +202 -0
  46. package/src/PatchUtils/patchVerifier/verifyAndCorrectLineNumbers.js +254 -0
  47. package/src/SharedUtils/timestampUtils.js +41 -0
  48. package/src/SharedUtils/versionUtils.js +58 -0
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@gitsense/gsc-utils",
3
+ "version": "0.1.0",
4
+ "description": "Utilities for GitSense Chat (GSC)",
5
+ "main": "dist/gsc-utils.cjs.js",
6
+ "module": "dist/gsc-utils.esm.js",
7
+ "browser": "dist/gsc-utils.cjs.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "src",
12
+ "LICENSE",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "rollup -c",
17
+ "dev": "rollup -c -w",
18
+ "test": "jest",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "git",
23
+ "chat",
24
+ "code-blocks",
25
+ "patches",
26
+ "markdown",
27
+ "parsing"
28
+ ],
29
+ "author": "GitSense Team",
30
+ "devDependencies": {
31
+ "@rollup/plugin-commonjs": "^25.0.7",
32
+ "@rollup/plugin-node-resolve": "^15.2.3",
33
+ "@rollup/plugin-terser": "^0.4.4",
34
+ "jest": "^29.7.0",
35
+ "rollup": "^4.9.6"
36
+ }
37
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Component: AnalysisBlockUtils
3
+ * Block-UUID: df4a5c56-bcee-4e83-989f-056b0d20005b
4
+ * Parent-UUID: 2a1b8c5d-e0f3-4a1c-8d7e-5e6f0a9b1c2e
5
+ * Version: 1.2.0
6
+ * Description: Provides utilities for identifying, parsing, and validating structured analysis blocks, including specific types like Overview (Short/Long/Tiny). These blocks typically follow a specific Markdown format generated by LLMs.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-04-20T04:26:47.000Z
9
+ * Authors: Gemini 2.5 Pro (v1.0.0), Gemini 2.5 Pro (v1.1.0), Gemini 2.5 Flash Thinking (v1.2.0)
10
+ */
11
+
12
+
13
+ /**
14
+ * Checks for the `# Gitsense Chat Analysis` line
15
+ * @param {string} content - The raw content string to check.
16
+ * @returns {boolean} True if the content starts with the overview headers, false otherwise.
17
+ */
18
+ function isAnalysisBlock(content) {
19
+ if (typeof content !== 'string') {
20
+ return false;
21
+ }
22
+
23
+ const trimmedContent = content.trimStart();
24
+ return trimmedContent.startsWith('# GitSense Chat Analysis\n');
25
+ }
26
+
27
+ /**
28
+ * Determines the type of overview block based on its starting header.
29
+ * @param {string} content - The raw content string.
30
+ * @returns analysis block type or null if not recognized.
31
+ */
32
+ function getAnalysisBlockType(content) {
33
+ if (typeof content !== 'string') {
34
+ return null;
35
+ }
36
+ const lines = content.split('\n');
37
+
38
+ // The first header 2 line contains the analyze type
39
+ const typeLine = lines.find(line => line.startsWith('## ')) || '';
40
+
41
+ if (typeLine)
42
+ return typeLine.trim().split('## ').pop().toLowerCase();
43
+
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Parses the metadata from a structured analysis block content.
49
+ * Assumes metadata is presented as a Markdown list: '* **Field:** Value'.
50
+ * Handles single-line values primarily. Multi-line values might require adjustments.
51
+ * @param {string} content - The raw content of the analysis block.
52
+ * @returns {Object | null} An object containing the extracted metadata fields (e.g., { 'Chat ID': '...', 'Repository': '...' }) or null if parsing fails or no fields are found.
53
+ */
54
+ function parseOverviewMetadata(content) {
55
+ if (!isAnalysisBlock(content)) {
56
+ return null; // Not an overview block
57
+ }
58
+
59
+ const metadata = {};
60
+ const lines = content.split('\n');
61
+
62
+ // Regex to capture "Field Name" and "Value" from lines like '* **Field Name: ** Value'
63
+ const fieldRegex = /^\*\s+\*\*([\w\s.-]+):\*\*\s+(.*)$/;
64
+ let foundList = false;
65
+ let currentKey = null; // For potential multi-line handling in the future
66
+
67
+ for (const line of lines) {
68
+ const trimmedLine = line.trim();
69
+
70
+ // Skip empty lines before the list starts
71
+ if (!trimmedLine && !foundList) {
72
+ continue;
73
+ }
74
+ // If we found the list and encounter an empty line, assume the list ended.
75
+ if (!trimmedLine && foundList) {
76
+ break;
77
+ }
78
+
79
+ const match = trimmedLine.match(fieldRegex);
80
+
81
+ if (match) {
82
+ foundList = true;
83
+ const key = match[1].trim(); // e.g., "Chat ID", "Summarized At"
84
+ const value = match[2].trim();
85
+ metadata[key] = key.includes('ID') ? parseInt(value) : value;
86
+ currentKey = key; // Track the last successfully parsed key
87
+ } else if (foundList) {
88
+ // FIXME: This is currently not supported. Should we remove it?
89
+ //
90
+ // Handle potential multi-line values (simple append approach)
91
+ // Only append if the line doesn't look like another list item or header
92
+ if (currentKey && !trimmedLine.startsWith('*') && !trimmedLine.startsWith('#')) {
93
+ // Check if the field is one known to potentially be multi-line
94
+ const multiLineFields = ['Summary', 'Key Functionality', 'Keywords'];
95
+ if (multiLineFields.includes(currentKey)) {
96
+ metadata[currentKey] += '\n' + trimmedLine; // Append the raw trimmed line
97
+ } else {
98
+ // If not a known multi-line field, assume the list ended here.
99
+ break;
100
+ }
101
+ } else {
102
+ // If it looks like another list item or something else, stop parsing this block's list.
103
+ break;
104
+ }
105
+ }
106
+ // Ignore lines before the list starts that don't match the pattern
107
+ }
108
+
109
+ // Basic check if any metadata was actually parsed
110
+ if (Object.keys(metadata).length === 0) {
111
+ // console.warn("AnalysisBlockUtils: Could not parse any metadata fields from the block.");
112
+ return null;
113
+ }
114
+
115
+ return metadata;
116
+ }
117
+
118
+ /**
119
+ * Validates the parsed analysis metadata object.
120
+ * Checks for the presence and basic type of required fields.
121
+ * @param {Object | null} metadata - The metadata object returned by parseOverviewMetadata.
122
+ * @returns {{isValid: boolean, errors: Array<string>}} Validation result.
123
+ */
124
+ function validateAnalysisMetadata(metadata) {
125
+ const errors = [];
126
+ const requiredFields = [ 'Chat ID' ];
127
+
128
+ if (!metadata || typeof metadata !== 'object') {
129
+ return { isValid: false, errors: ['Metadata object is null or invalid.'] };
130
+ }
131
+
132
+ // Check for presence and non-empty string values
133
+ for (const field of requiredFields) {
134
+ if (!metadata[field]) {
135
+ errors.push(`Missing or empty required field: "${field}".`);
136
+ }
137
+ }
138
+
139
+ return {
140
+ isValid: errors.length === 0,
141
+ errors: errors
142
+ };
143
+ }
144
+
145
+
146
+ module.exports = {
147
+ isAnalysisBlock,
148
+ getAnalysisBlockType,
149
+ parseOverviewMetadata,
150
+ validateAnalysisMetadata
151
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Component: ChatUtils
3
+ * Block-UUID: fdbd7f40-a1ee-4c07-bbfe-bbac53efd2c8
4
+ * Parent-UUID: c981830e-3472-4d2b-b6d4-37b8e661ef4c
5
+ * Version: 1.3.0
6
+ * Description: Provides utility functions for analyzing and classifying chat sessions based on message content and structure.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-07-11T19:54:25.519Z
9
+ * Authors: Gemini 2.5 Pro (v1.0.0), Gemini 2.5 Pro (v1.1.0), Gemini 2.5 Flash Thinking (v1.2.0), Gemini 2.5 Flash Thinking (v1.2.1), Gemini 2.5 Flash Thinking (v1.3.0)
10
+ */
11
+
12
+
13
+ const { getMessagesBeforeId } = require('./MessageUtils');
14
+
15
+ /**
16
+ * Internal helper function to determine if a chat session matches a specific pattern.
17
+ * Checks message count, structure, and specific starting strings for the first two assistant messages.
18
+ * @param {object} chat - The chat object, expected to have `chat.messages[0]` containing the message array.
19
+ * @param {string} model - The model.
20
+ * @param {string} firstAssistantMsgStart - The string the first assistant message must start with.
21
+ * @returns {boolean} True if the chat matches the pattern, false otherwise.
22
+ * @private
23
+ */
24
+ function checkChatTypePattern(chat, model, firstAssistantMsgStart) {
25
+ if (!chat || !Array.isArray(chat.messages) || !chat.messages.length) {
26
+ console.error("ChatUtils: Invalid chat or message structure provided.");
27
+ return false;
28
+ }
29
+
30
+ const messages = getChatMessages(chat, model);
31
+
32
+ /*
33
+ * Common checks for chat type classification:
34
+ * 1) Must be 4 or more messages.
35
+ * 2) First assistant message must start with a specific string (passed as parameter).
36
+ * 3) Second assistant message must be instructions or context.
37
+ * - Instructions start with '# User Instructions\n'
38
+ * - Context starts with '## FILE CONTENT - ' or '## OVERVIEW - '
39
+ */
40
+
41
+ // Filter assistant messages
42
+ const assistantMessages = messages.filter(m => m.role === 'assistant');
43
+
44
+ // First assistant message check
45
+ if (!assistantMessages[0].message || !assistantMessages[0].message.startsWith(firstAssistantMsgStart)) {
46
+ return false;
47
+ }
48
+
49
+ return true;
50
+ }
51
+
52
+
53
+ /**
54
+ * Determines if the current chat session appears to be a "New Analyzer" session.
55
+ * @param {object} chat - The chat object.
56
+ * @returns {boolean} True if the chat is of type 'new-analyzer'
57
+ */
58
+ function isNewAnalyzerChat(chat, model) {
59
+ return chat.type === 'new-analyzer';
60
+ }
61
+
62
+ /**
63
+ * Determines if the current chat session appears to be an "Analyze" session.
64
+ * @param {object} chat - The chat object.
65
+ * @param {string} model - Optional model.
66
+ * @returns {boolean} True if the chat matches the Overview Builder pattern, false otherwise.
67
+ */
68
+ function isAnalyzeChat(chat, model) {
69
+ return chat.type === 'analyze';
70
+ }
71
+
72
+ /**
73
+ * Determines if the current chat session appears to be an "Ask" session.
74
+ * @param {object} chat - The chat object.
75
+ * @param {string} model - Optional model.
76
+ * @returns {boolean} True if the chat matches the Ask pattern, false otherwise.
77
+ */
78
+ function isAskChat(chat, model) {
79
+ return checkChatTypePattern(chat, model || chat.main_model, '# Ask\n');
80
+ }
81
+
82
+ /**
83
+ * Determines if the current chat session appears to be a "Plan Chat" session.
84
+ * @param {object} chat - The chat object.
85
+ * @param {string} model - Optional model.
86
+ * @returns {boolean} True if the chat matches the Plan Chat pattern, false otherwise.
87
+ */
88
+ function isPlanChat(chat, model) {
89
+ // Note: Original code checked for '# Plan'. If '# Context Builder Assistant' was intended, update the string below.
90
+ return checkChatTypePattern(chat, model || chat.main_model, '# Plan\n');
91
+ }
92
+
93
+ /**
94
+ * Determines if the current chat session appears to be a "Code Chat" session.
95
+ * @param {object} chat - The chat object.
96
+ * @param {string} model - Optional model.
97
+ * @returns {boolean} True if the chat matches the Code Chat pattern, false otherwise.
98
+ */
99
+ function isCodeChat(chat, model) {
100
+ // Note: Original code checked for '# Coding Assistant'. If '# Code - Coding Assistant' was intended, update the string below.
101
+ return checkChatTypePattern(chat, model || chat.main_model, '# Code\n');
102
+ }
103
+
104
+ /**
105
+ * Retrieves all messages from a chat session.
106
+ * @param {object} chat - The chat object, expected to have `chat.messages[0]` containing the message array.
107
+ * @param {string} model - Optional model.
108
+ * @returns {Array<object>} An array of message objects, or an empty array if chat or messages are invalid.
109
+ */
110
+ function getChatMessages(chat, model) {
111
+ if (!chat || !Array.isArray(chat.messages) || !chat.messages.length) {
112
+ console.error("ChatUtils: Invalid chat or message structure provided for getChatMessages.");
113
+ return [];
114
+ }
115
+ // Setting before id to null will retrieve all messages
116
+ return getMessagesBeforeId(model || chat.main_model, chat.messages[0], null);
117
+ }
118
+
119
+ module.exports = {
120
+ isAskChat,
121
+ isNewAnalyzerChat,
122
+ isAnalyzeChat,
123
+ isPlanChat,
124
+ isCodeChat,
125
+ getChatMessages
126
+ };
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Component: CodeBlockUtils Block Extractor
3
+ * Block-UUID: efacd230-2ced-4910-a417-efc536ca4c68
4
+ * Parent-UUID: 4b1a9f2c-8d3e-4c5a-9b1f-0e2d7a3c8b4d
5
+ * Version: 1.2.0
6
+ * Description: Provides functions to find and match markdown code fences (```) to identify block boundaries, and extract blocks with UUIDs.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-04-15T15:55:49.293Z
9
+ * Authors: Gemini 2.5 Pro (v1.0.0), Gemini 2.5 Pro (v1.1.0), Gemini 2.5 Flash Thinking (v1.2.0)
10
+ */
11
+
12
+
13
+ /**
14
+ * Finds all opening and closing code fence positions using a line-by-line approach
15
+ * @param {string} text - The input text
16
+ * @returns {Object} Object containing arrays of opening and closing fence positions: { openingPositions: Array, closingPositions: Array }
17
+ */
18
+ function findAllCodeFences(text) {
19
+ const openingPositions = [];
20
+ const closingPositions = [];
21
+
22
+ // Split text into lines for processing
23
+ const lines = text.split('\n');
24
+ let inCodeBlock = false;
25
+ let currentOpeningFence = null;
26
+ let linePosition = 0;
27
+
28
+ // Process each line
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const line = lines[i];
31
+ const trimmedLine = line.trim();
32
+
33
+ // Check for opening fence (only if not already in a code block)
34
+ if (!inCodeBlock) {
35
+ // Match opening fence with or without language specifier
36
+ const openingMatch = trimmedLine.match(/^```([a-zA-Z-]*)$/);
37
+ if (openingMatch) {
38
+ inCodeBlock = true;
39
+ const fencePosition = linePosition + line.indexOf('```');
40
+ currentOpeningFence = {
41
+ position: fencePosition,
42
+ language: openingMatch[1].trim().toLowerCase() || "unknown",
43
+ length: openingMatch[0].length
44
+ };
45
+ openingPositions.push(currentOpeningFence);
46
+ }
47
+ }
48
+ // Check for closing fence (only if in a code block)
49
+ else if (inCodeBlock) {
50
+ // Match closing fence (exactly ```)
51
+ if (trimmedLine === '```') {
52
+ inCodeBlock = false;
53
+ const fencePosition = linePosition + line.indexOf('```');
54
+ closingPositions.push({
55
+ position: fencePosition,
56
+ length: 3
57
+ });
58
+ currentOpeningFence = null;
59
+ }
60
+ }
61
+
62
+ // Update line position for next iteration
63
+ linePosition += line.length + 1; // +1 for the newline character
64
+ }
65
+
66
+ // If we reach the end and still have an open code block, it's incomplete
67
+ // We don't add a closing position for it
68
+
69
+ return {
70
+ openingPositions,
71
+ closingPositions
72
+ };
73
+ }
74
+
75
+
76
+ /**
77
+ * Matches opening and closing fences to identify complete and incomplete blocks
78
+ * @param {string} text - The input text
79
+ * @param {Array} openingPositions - Array of opening fence objects { position, language, length }
80
+ * @param {Array} closingPositions - Array of closing fence objects { position, length }
81
+ * @returns {Object} Object containing: { completeBlocks: Array, incompleteBlocks: Array, warnings: Array }
82
+ */
83
+ function matchFencesAndExtractBlocks(text, openingPositions, closingPositions) {
84
+ const completeBlocks = [];
85
+ const incompleteBlocks = [];
86
+ const warnings = [];
87
+
88
+ // Create a copy of closing positions that we can modify
89
+ const availableClosingPositions = [...closingPositions];
90
+
91
+ // Sort opening and closing positions by their position in the text
92
+ const sortedOpenings = [...openingPositions].sort((a, b) => a.position - b.position);
93
+ availableClosingPositions.sort((a, b) => a.position - b.position);
94
+
95
+ // Process all opening fences in order
96
+ let openingIndex = 0;
97
+ let closingIndex = 0; // Tracks the next available closing fence to consider
98
+
99
+ while (openingIndex < sortedOpenings.length) {
100
+ const currentOpening = sortedOpenings[openingIndex];
101
+
102
+ // Find the next closing fence that comes *after* this opening fence ends
103
+ let matchingClosingIndex = -1;
104
+ for (let i = closingIndex; i < availableClosingPositions.length; i++) {
105
+ if (availableClosingPositions[i].position > currentOpening.position + currentOpening.length -1) { // Ensure closing is strictly after opening
106
+ matchingClosingIndex = i;
107
+ break;
108
+ }
109
+ }
110
+
111
+ if (matchingClosingIndex !== -1) {
112
+ // Found a potential matching closing fence
113
+ const matchingClosing = availableClosingPositions[matchingClosingIndex];
114
+
115
+ // Check if there's another opening fence before this closing fence
116
+ let nestedOpeningFound = false;
117
+ if (openingIndex + 1 < sortedOpenings.length) {
118
+ if (sortedOpenings[openingIndex + 1].position < matchingClosing.position) {
119
+ // This indicates a likely nested structure or potentially malformed input.
120
+ // For simplicity in this extractor, we'll treat the current opening as incomplete
121
+ // if a new opening starts before its potential closer.
122
+ // A more sophisticated parser might handle nesting.
123
+ // nestedOpeningFound = true; // Uncomment if handling nesting differently
124
+ }
125
+ }
126
+
127
+ // If not nested (or handling nesting differently), proceed
128
+ if (!nestedOpeningFound) {
129
+ // Check if there's any content between the fences (excluding only whitespace)
130
+ const blockContent = text.substring(
131
+ currentOpening.position + currentOpening.length,
132
+ matchingClosing.position
133
+ );
134
+
135
+ // Only consider it a valid block if there's non-whitespace content or it's explicitly empty
136
+ if (blockContent.trim().length > 0 || blockContent.length === 0) { // Allow empty blocks like ```\n```
137
+ completeBlocks.push({
138
+ opening: currentOpening,
139
+ closing: matchingClosing,
140
+ incomplete: false
141
+ });
142
+
143
+ // Consume this closing fence and advance the closing index
144
+ closingIndex = matchingClosingIndex + 1;
145
+ } else {
146
+ // Block contains only whitespace - treat as potentially malformed or skip
147
+ warnings.push({
148
+ position: currentOpening.position,
149
+ type: 'whitespace_only_block',
150
+ message: `Code block starting at position ${currentOpening.position} contains only whitespace.`
151
+ });
152
+ // Don't consume the closing fence yet, let the next opening try to pair with it
153
+ }
154
+ } else {
155
+ // Handle nested/malformed case - treat current opening as incomplete for now
156
+ incompleteBlocks.push({
157
+ opening: currentOpening,
158
+ incomplete: true
159
+ });
160
+ warnings.push({
161
+ position: currentOpening.position,
162
+ type: 'potentially_nested_or_malformed',
163
+ message: `Potentially nested or malformed block structure starting at position ${currentOpening.position}. Treating as incomplete.`
164
+ });
165
+ }
166
+
167
+ } else {
168
+ // No matching closing fence found for this opening fence - this is an incomplete block
169
+ // Check if there's actual content after the opening fence
170
+ const remainingContent = text.substring(currentOpening.position + currentOpening.length);
171
+ if (remainingContent.trim().length > 0) {
172
+ incompleteBlocks.push({
173
+ opening: currentOpening,
174
+ incomplete: true
175
+ });
176
+ warnings.push({
177
+ position: currentOpening.position,
178
+ type: 'incomplete_block',
179
+ message: `Incomplete code block found starting at position ${currentOpening.position}. Missing closing fence.`
180
+ });
181
+ } else {
182
+ // Opening fence at the very end of the file with nothing after it. Ignore.
183
+ warnings.push({
184
+ position: currentOpening.position,
185
+ type: 'trailing_opening_fence',
186
+ message: `Opening code fence found at the end of the text with no content after it at position ${currentOpening.position}.`
187
+ });
188
+ }
189
+ }
190
+
191
+ // Move to the next opening fence
192
+ openingIndex++;
193
+ }
194
+
195
+ // Check for any remaining closing fences that weren't matched (might indicate ``` outside blocks)
196
+ if (closingIndex < availableClosingPositions.length) {
197
+ for (let i = closingIndex; i < availableClosingPositions.length; i++) {
198
+ warnings.push({
199
+ position: availableClosingPositions[i].position,
200
+ type: 'unmatched_closing_fence',
201
+ message: `Found an unmatched closing code fence at position ${availableClosingPositions[i].position}.`
202
+ });
203
+ }
204
+ }
205
+
206
+
207
+ return { completeBlocks, incompleteBlocks, warnings };
208
+ }
209
+
210
+ /**
211
+ * Extracts all code blocks with their Block-UUIDs from a message
212
+ * @param {string} messageText - The message text
213
+ * @returns {Array} Array of objects with code, language, blockUUID, startIndex, endIndex
214
+ */
215
+ function extractCodeBlocksWithUUIDs(messageText) {
216
+ if (!messageText || typeof messageText !== 'string') {
217
+ return [];
218
+ }
219
+
220
+ const codeBlocks = [];
221
+ // Improved regex to handle various code block formats
222
+ const codeBlockRegex = /```(\w+)?\s*\n([\s\S]*?)```/g;
223
+
224
+ let match;
225
+ while ((match = codeBlockRegex.exec(messageText)) !== null) {
226
+ const language = match[1] ? match[1].trim() : 'text';
227
+ const code = match[2]; // Keep original spacing/newlines within block
228
+
229
+ // Extract Block-UUID from code if present - more flexible pattern
230
+ let blockUUID = null;
231
+ // Match UUID in potential header section (look near the top)
232
+ const lines = code.split('\n');
233
+ const maxHeaderLines = 20; // Limit search to avoid scanning huge blocks
234
+ for (let i = 0; i < Math.min(lines.length, maxHeaderLines); i++) {
235
+ const uuidMatch = code.match(/Block-UUID:\s*([a-f0-9-]+)/i);
236
+ if (uuidMatch && uuidMatch[1]) {
237
+ blockUUID = uuidMatch[1];
238
+ break; // Found it
239
+ }
240
+ }
241
+
242
+
243
+ codeBlocks.push({
244
+ language,
245
+ code, // Raw code content
246
+ blockUUID,
247
+ startIndex: match.index,
248
+ endIndex: match.index + match[0].length
249
+ });
250
+ }
251
+
252
+ return codeBlocks;
253
+ }
254
+
255
+ /**
256
+ * Finds a code block by UUID in message text
257
+ * (Moved from original PatchUtils)
258
+ * @param {string} messageText - The message text
259
+ * @param {string} blockUUID - The Block-UUID to find
260
+ * @returns {Object|null} Code block object or null if not found
261
+ */
262
+ function findCodeBlockByUUID(messageText, blockUUID) {
263
+ if (!messageText || !blockUUID) {
264
+ return null;
265
+ }
266
+ // Use the extractor function now part of this module
267
+ const codeBlocks = extractCodeBlocksWithUUIDs(messageText);
268
+ return codeBlocks.find(block => block.blockUUID === blockUUID) || null;
269
+ }
270
+
271
+ module.exports = {
272
+ findAllCodeFences,
273
+ matchFencesAndExtractBlocks,
274
+ extractCodeBlocksWithUUIDs,
275
+ findCodeBlockByUUID
276
+ };
277
+