@probelabs/probe 0.6.0-rc148 → 0.6.0-rc150

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.
@@ -5,6 +5,65 @@
5
5
 
6
6
  import { createMessagePreview } from '../tools/common.js';
7
7
  import { validate, fixText, extractMermaidBlocks } from '@probelabs/maid';
8
+ import Ajv from 'ajv';
9
+
10
+ /**
11
+ * Recursively apply additionalProperties: false to all object schemas
12
+ * This ensures strict validation at all nesting levels
13
+ * @param {Object} schema - JSON schema object
14
+ * @returns {Object} - Modified schema with additionalProperties enforced
15
+ */
16
+ function enforceNoAdditionalProperties(schema) {
17
+ if (!schema || typeof schema !== 'object') {
18
+ return schema;
19
+ }
20
+
21
+ // Create a deep clone to avoid modifying the original
22
+ const cloned = JSON.parse(JSON.stringify(schema));
23
+
24
+ function applyRecursively(obj) {
25
+ if (!obj || typeof obj !== 'object') {
26
+ return;
27
+ }
28
+
29
+ // If this is an object type schema and doesn't have additionalProperties set
30
+ if (obj.type === 'object' && obj.additionalProperties === undefined) {
31
+ obj.additionalProperties = false;
32
+ }
33
+
34
+ // Recursively process properties
35
+ if (obj.properties && typeof obj.properties === 'object') {
36
+ Object.values(obj.properties).forEach(applyRecursively);
37
+ }
38
+
39
+ // Process items for arrays
40
+ if (obj.items) {
41
+ if (Array.isArray(obj.items)) {
42
+ obj.items.forEach(applyRecursively);
43
+ } else {
44
+ applyRecursively(obj.items);
45
+ }
46
+ }
47
+
48
+ // Process nested schemas in oneOf, anyOf, allOf
49
+ ['oneOf', 'anyOf', 'allOf'].forEach(key => {
50
+ if (Array.isArray(obj[key])) {
51
+ obj[key].forEach(applyRecursively);
52
+ }
53
+ });
54
+
55
+ // Process definitions/defs
56
+ if (obj.definitions && typeof obj.definitions === 'object') {
57
+ Object.values(obj.definitions).forEach(applyRecursively);
58
+ }
59
+ if (obj.$defs && typeof obj.$defs === 'object') {
60
+ Object.values(obj.$defs).forEach(applyRecursively);
61
+ }
62
+ }
63
+
64
+ applyRecursively(cloned);
65
+ return cloned;
66
+ }
8
67
 
9
68
  /**
10
69
  * HTML entity decoder map for common entities that might appear in mermaid diagrams
@@ -141,12 +200,15 @@ export function cleanSchemaResponse(response) {
141
200
  * @returns {Object} - {isValid: boolean, parsed?: Object, error?: string}
142
201
  */
143
202
  export function validateJsonResponse(response, options = {}) {
144
- const { debug = false } = options;
203
+ const { debug = false, schema = null, strictSchema = true } = options;
145
204
 
146
205
  if (debug) {
147
206
  console.log(`[DEBUG] JSON validation: Starting validation for response (${response.length} chars)`);
148
207
  const preview = createMessagePreview(response);
149
208
  console.log(`[DEBUG] JSON validation: Preview: ${preview}`);
209
+ if (schema) {
210
+ console.log(`[DEBUG] JSON validation: Schema validation enabled`);
211
+ }
150
212
  }
151
213
 
152
214
  try {
@@ -159,6 +221,140 @@ export function validateJsonResponse(response, options = {}) {
159
221
  console.log(`[DEBUG] JSON validation: Object type: ${typeof parsed}, keys: ${Object.keys(parsed || {}).length}`);
160
222
  }
161
223
 
224
+ // If schema provided, validate against it
225
+ if (schema) {
226
+ const schemaStart = Date.now();
227
+ let schemaObj;
228
+
229
+ try {
230
+ // Parse schema if it's a string
231
+ schemaObj = typeof schema === 'string' ? JSON.parse(schema) : schema;
232
+
233
+ // Apply strict mode: enforce additionalProperties: false on all nested objects
234
+ // unless explicitly disabled via strictSchema: false option
235
+ if (strictSchema) {
236
+ schemaObj = enforceNoAdditionalProperties(schemaObj);
237
+ if (debug) {
238
+ console.log(`[DEBUG] JSON validation: Applied strict mode - additionalProperties: false enforced on all levels`);
239
+ }
240
+ }
241
+ } catch (schemaParseError) {
242
+ if (debug) {
243
+ console.log(`[DEBUG] JSON validation: Failed to parse schema: ${schemaParseError.message}`);
244
+ }
245
+ return {
246
+ isValid: false,
247
+ error: 'Invalid schema provided',
248
+ schemaError: schemaParseError.message
249
+ };
250
+ }
251
+
252
+ // Create AJV validator with strict mode
253
+ const ajv = new Ajv({
254
+ strict: false, // Don't fail on unknown schema keywords
255
+ allErrors: true, // Return all errors, not just the first
256
+ verbose: true // Include schema and data in errors
257
+ });
258
+
259
+ let validate;
260
+ try {
261
+ validate = ajv.compile(schemaObj);
262
+ } catch (compileError) {
263
+ if (debug) {
264
+ console.log(`[DEBUG] JSON validation: Schema compilation failed: ${compileError.message}`);
265
+ }
266
+ return {
267
+ isValid: false,
268
+ error: 'Schema compilation failed',
269
+ schemaError: compileError.message
270
+ };
271
+ }
272
+
273
+ const valid = validate(parsed);
274
+ const schemaTime = Date.now() - schemaStart;
275
+
276
+ if (debug) {
277
+ console.log(`[DEBUG] JSON validation: Schema validation completed in ${schemaTime}ms, valid: ${valid}`);
278
+ }
279
+
280
+ if (!valid) {
281
+ // Format schema errors for better readability and actionability
282
+ const formattedErrors = validate.errors.map(err => {
283
+ // Convert JSON Pointer path to dot notation for readability
284
+ // /user/profile/age -> user.profile.age
285
+ const path = err.instancePath
286
+ ? err.instancePath.substring(1).replace(/\//g, '.')
287
+ : '<root>';
288
+
289
+ let message = '';
290
+ let suggestion = '';
291
+
292
+ // Create crisp, actionable error messages based on error type
293
+ if (err.keyword === 'additionalProperties') {
294
+ const extraField = err.params.additionalProperty;
295
+ message = `Extra field '${extraField}' is not allowed`;
296
+ suggestion = `Remove '${extraField}' or add it to the schema`;
297
+ } else if (err.keyword === 'required') {
298
+ const missingField = err.params.missingProperty;
299
+ message = `Missing required field '${missingField}'`;
300
+ suggestion = `Add '${missingField}' to this object`;
301
+ } else if (err.keyword === 'type') {
302
+ const expected = err.params.type;
303
+ const actual = typeof err.data;
304
+ const value = JSON.stringify(err.data);
305
+ message = `Wrong type: expected ${expected}, got ${actual} (value: ${value})`;
306
+ suggestion = `Change value to ${expected} type`;
307
+ } else if (err.keyword === 'enum') {
308
+ const allowed = err.params.allowedValues;
309
+ const value = JSON.stringify(err.data);
310
+ message = `Invalid value ${value}. Allowed: ${allowed.map(v => JSON.stringify(v)).join(', ')}`;
311
+ suggestion = `Use one of the allowed values`;
312
+ } else if (err.keyword === 'minimum' || err.keyword === 'maximum') {
313
+ const limit = err.params.limit;
314
+ const comparison = err.params.comparison;
315
+ message = `Value ${err.data} ${err.message} (${comparison} ${limit})`;
316
+ suggestion = `Adjust value to meet constraint`;
317
+ } else if (err.keyword === 'minLength' || err.keyword === 'maxLength') {
318
+ const limit = err.params.limit;
319
+ message = `String length ${err.message} (current: ${err.data.length}, ${err.keyword}: ${limit})`;
320
+ suggestion = `Adjust string length`;
321
+ } else if (err.keyword === 'pattern') {
322
+ message = `Value doesn't match required pattern: ${err.params.pattern}`;
323
+ suggestion = `Update value to match the pattern`;
324
+ } else {
325
+ // Fallback for other error types
326
+ message = err.message;
327
+ suggestion = '';
328
+ }
329
+
330
+ // Format: "path: message | suggestion"
331
+ const location = path ? `at '${path}'` : 'at root';
332
+ return suggestion
333
+ ? `${location}: ${message} → ${suggestion}`
334
+ : `${location}: ${message}`;
335
+ });
336
+
337
+ const errorSummary = formattedErrors.join('\n ');
338
+
339
+ if (debug) {
340
+ console.log(`[DEBUG] JSON validation: Schema validation errors:\n ${errorSummary}`);
341
+ }
342
+
343
+ return {
344
+ isValid: false,
345
+ error: 'Schema validation failed',
346
+ schemaErrors: validate.errors,
347
+ formattedErrors: formattedErrors,
348
+ errorSummary: `Schema validation failed:\n ${errorSummary}`,
349
+ parsed: parsed // Still return parsed data for debugging
350
+ };
351
+ }
352
+
353
+ if (debug) {
354
+ console.log(`[DEBUG] JSON validation: Schema validation passed`);
355
+ }
356
+ }
357
+
162
358
  return { isValid: true, parsed };
163
359
  } catch (error) {
164
360
  // Extract error position from error message if available
@@ -886,9 +1082,21 @@ When presented with broken JSON, analyze it thoroughly and provide the corrected
886
1082
  errorContext = validationResult.enhancedError;
887
1083
  }
888
1084
 
1085
+ // Add schema validation errors if present
1086
+ let schemaErrorDetails = '';
1087
+ if (validationResult.errorSummary) {
1088
+ schemaErrorDetails = `\n\nSchema Validation Errors:\n${validationResult.errorSummary}`;
1089
+ } else if (validationResult.schemaErrors && validationResult.schemaErrors.length > 0) {
1090
+ const errors = validationResult.schemaErrors.map(err => {
1091
+ const path = err.instancePath || '(root)';
1092
+ return ` ${path}: ${err.message}`;
1093
+ }).join('\n');
1094
+ schemaErrorDetails = `\n\nSchema Validation Errors:\n${errors}`;
1095
+ }
1096
+
889
1097
  const prompt = `Fix the following invalid JSON.
890
1098
 
891
- Error: ${errorContext}
1099
+ Error: ${errorContext}${schemaErrorDetails}
892
1100
 
893
1101
  Invalid JSON:
894
1102
  ${invalidJson}
@@ -896,7 +1104,7 @@ ${invalidJson}
896
1104
  Expected schema structure:
897
1105
  ${schema}
898
1106
 
899
- Provide only the corrected JSON without any markdown formatting or explanations.`;
1107
+ ${schemaErrorDetails ? 'CRITICAL: Pay special attention to the schema validation errors above. The JSON may be syntactically valid but does not conform to the required schema. Make sure to:\n- Include all required fields\n- Use correct data types\n- Remove any additional properties not defined in the schema (if additionalProperties is false)\n- Ensure all values match their schema constraints\n\n' : ''}Provide only the corrected JSON without any markdown formatting or explanations.`;
900
1108
 
901
1109
  try {
902
1110
  if (this.options.debug) {
package/build/extract.js CHANGED
@@ -48,7 +48,7 @@ export async function extract(options) {
48
48
  const hasContent = options.content !== undefined && options.content !== null;
49
49
 
50
50
  if (!hasFiles && !hasInputFile && !hasContent) {
51
- throw new Error('Either files array, inputFile, or content must be provided');
51
+ throw new Error('Extract requires one of: "files" (array of file paths), "inputFile" (path to input file), or "content" (string/buffer for stdin)');
52
52
  }
53
53
 
54
54
  // Get the binary path
@@ -149,18 +149,18 @@ class ProbeServer {
149
149
  },
150
150
  {
151
151
  name: 'extract_code',
152
- description: "Extract code from files. Formats: file.js (whole file), file.js:42 (from line), file.js#functionName (symbol).",
152
+ description: "Extract code blocks from files using tree-sitter AST parsing. Each file path can include optional line numbers or symbol names to extract specific code blocks.",
153
153
  inputSchema: {
154
154
  type: 'object',
155
155
  properties: {
156
156
  path: {
157
157
  type: 'string',
158
- description: 'Absolute path to the project directory',
158
+ description: 'Absolute path to the project root directory (used as working directory for relative file paths)',
159
159
  },
160
160
  files: {
161
161
  type: 'array',
162
162
  items: { type: 'string' },
163
- description: 'Array of file paths with optional line/symbol: ["file.rs:10", "file.rs#func_name"]',
163
+ description: 'Array of file paths to extract from. Formats: "file.js" (entire file), "file.js:42" (code block at line 42), "file.js:10-20" (lines 10-20), "file.js#funcName" (specific symbol). Line numbers and symbols are part of the path string, not separate parameters. Paths can be absolute or relative to the project directory.',
164
164
  }
165
165
  },
166
166
  required: ['path', 'files'],
@@ -199,18 +199,18 @@ class ProbeServer {
199
199
  },
200
200
  {
201
201
  name: 'extract_code',
202
- description: "Extract code from files. Formats: file.js (whole file), file.js:42 (from line), file.js#functionName (symbol).",
202
+ description: "Extract code blocks from files using tree-sitter AST parsing. Each file path can include optional line numbers or symbol names to extract specific code blocks.",
203
203
  inputSchema: {
204
204
  type: 'object',
205
205
  properties: {
206
206
  path: {
207
207
  type: 'string',
208
- description: 'Absolute path to the project directory',
208
+ description: 'Absolute path to the project root directory (used as working directory for relative file paths)',
209
209
  },
210
210
  files: {
211
211
  type: 'array',
212
212
  items: { type: 'string' },
213
- description: 'Array of file paths with optional line/symbol: ["file.rs:10", "file.rs#func_name"]',
213
+ description: 'Array of file paths to extract from. Formats: "file.js" (entire file), "file.js:42" (code block at line 42), "file.js:10-20" (lines 10-20), "file.js#funcName" (specific symbol). Line numbers and symbols are part of the path string, not separate parameters. Paths can be absolute or relative to the project directory.',
214
214
  }
215
215
  },
216
216
  required: ['path', 'files'],
@@ -7,13 +7,8 @@ import { z } from 'zod';
7
7
 
8
8
  // Common schemas for tool parameters (used for internal execution after XML parsing)
9
9
  export const searchSchema = z.object({
10
- query: z.string().describe('Search query with Elasticsearch syntax. Use + for important terms.'),
11
- path: z.string().optional().default('.').describe('Path to search in. For dependencies use "go:github.com/owner/repo", "js:package_name", or "rust:cargo_name" etc.'),
12
- allow_tests: z.boolean().optional().default(false).describe('Allow test files in search results'),
13
- exact: z.boolean().optional().default(false).describe('Perform exact search without tokenization (case-insensitive)'),
14
- maxResults: z.number().optional().describe('Maximum number of results to return'),
15
- maxTokens: z.number().optional().default(10000).describe('Maximum number of tokens to return'),
16
- language: z.string().optional().describe('Limit search to files of a specific programming language')
10
+ query: z.string().describe('Search query with Elasticsearch syntax. Use quotes for exact matches, AND/OR for boolean logic, - for negation.'),
11
+ path: z.string().optional().default('.').describe('Path to search in. For dependencies use "go:github.com/owner/repo", "js:package_name", or "rust:cargo_name" etc.')
17
12
  });
18
13
 
19
14
  export const querySchema = z.object({
@@ -24,13 +19,8 @@ export const querySchema = z.object({
24
19
  });
25
20
 
26
21
  export const extractSchema = z.object({
27
- targets: z.string().optional().describe('File paths or symbols to extract from. Can include line numbers, symbol names, or multiple space-separated targets'),
28
- input_content: z.string().optional().describe('Text content to extract file paths from'),
29
- line: z.number().optional().describe('Start line number to extract a specific code block'),
30
- end_line: z.number().optional().describe('End line number for extracting a range of lines'),
31
- allow_tests: z.boolean().optional().default(false).describe('Allow test files and test code blocks'),
32
- context_lines: z.number().optional().default(10).describe('Number of context lines to include'),
33
- format: z.string().optional().default('plain').describe('Output format (plain, markdown, json, xml, color, outline-xml, outline-diff)')
22
+ targets: z.string().optional().describe('File paths or symbols to extract from. Formats: "file.js" (whole file), "file.js:42" (line 42), "file.js:10-20" (lines 10-20), "file.js#funcName" (symbol). Multiple targets separated by spaces.'),
23
+ input_content: z.string().optional().describe('Text content to extract file paths from (alternative to targets)')
34
24
  });
35
25
 
36
26
  export const delegateSchema = z.object({
@@ -120,13 +110,8 @@ You need to focus on main keywords when constructing the query, and always use e
120
110
  - Once data is returned, it's cached and won't return on next runs (this is expected behavior)
121
111
 
122
112
  Parameters:
123
- - query: (required) Search query with Elasticsearch syntax. You can use + for important terms, and - for negation.
124
- - path: (required) Path to search in. All dependencies located in /dep folder, under language sub folders, like this: "/dep/go/github.com/owner/repo", "/dep/js/package_name", or "/dep/rust/cargo_name" etc. YOU SHOULD ALWAYS provide FULL PATH when searching dependencies, including depency name.
125
- - allow_tests: (optional, default: false) Allow test files in search results (true/false).
126
- - exact: (optional, default: false) Perform exact pricise search. Use it when you already know function or struct name, or some other code block, and want exact match.
127
- - maxResults: (optional) Maximum number of results to return (number).
128
- - maxTokens: (optional, default: 10000) Maximum number of tokens to return (number).
129
- - language: (optional) Limit search to files of a specific programming language (e.g., 'rust', 'js', 'python', 'go' etc.).
113
+ - query: (required) Search query with Elasticsearch syntax. Use quotes for exact matches ("functionName"), AND/OR for boolean logic, - for negation, + for important terms.
114
+ - path: (optional, default: '.') Path to search in. All dependencies located in /dep folder, under language sub folders, like this: "/dep/go/github.com/owner/repo", "/dep/js/package_name", or "/dep/rust/cargo_name" etc.
130
115
 
131
116
  **Workflow:** Always start with search, then use extract for detailed context when needed.
132
117
 
@@ -148,30 +133,24 @@ User: How to calculate the total amount in the payments module?
148
133
  <search>
149
134
  <query>calculate AND payment</query>
150
135
  <path>src/utils</path>
151
- <allow_tests>false</allow_tests>
152
136
  </search>
153
137
 
154
138
  User: How do the user authentication and authorization work?
155
139
  <search>
156
- <query>+user and (authentification OR authroization OR authz)</query>
140
+ <query>+user AND (authentication OR authorization OR authz)</query>
157
141
  <path>.</path>
158
- <allow_tests>true</allow_tests>
159
- <language>go</language>
160
142
  </search>
161
143
 
162
144
  User: Find all react imports in the project.
163
145
  <search>
164
- <query>import { react }</query>
146
+ <query>"import" AND "react"</query>
165
147
  <path>.</path>
166
- <exact>true</exact>
167
- <language>js</language>
168
148
  </search>
169
149
 
170
- User: Find how decompoud library works?
150
+ User: Find how decompound library works?
171
151
  <search>
172
- <query>import { react }</query>
152
+ <query>decompound</query>
173
153
  <path>/dep/rust/decompound</path>
174
- <language>rust</language>
175
154
  </search>
176
155
 
177
156
  </examples>
@@ -208,11 +187,9 @@ Full file extraction should be the LAST RESORT! Always prefer search.
208
187
  **Session Awareness:** Reuse context from previous tool calls. Don't re-extract the same symbols you already have.
209
188
 
210
189
  Parameters:
211
- - targets: (required) File paths or symbols to extract from. Can include line numbers, symbol names, or multiple space-separated targets (e.g., 'src/main.rs:10-20', 'src/utils.js#myFunction').
212
- For multiple extractions: 'session.rs#AuthService.login auth.rs:2-100 config.rs#DatabaseConfig'
213
- - line: (optional) Start line number to extract a specific code block. Use with end_line for ranges.
214
- - end_line: (optional) End line number for extracting a range of lines.
215
- - allow_tests: (optional, default: false) Allow test files and test code blocks (true/false).
190
+ - targets: (required) File paths or symbols to extract from. Formats: "file.js" (whole file), "file.js:42" (code block at line 42), "file.js:10-20" (lines 10-20), "file.js#funcName" (specific symbol). Multiple targets separated by spaces.
191
+ - input_content: (optional) Text content to extract file paths from (alternative to targets for processing diffs/logs).
192
+
216
193
  Usage Example:
217
194
 
218
195
  <examples>
@@ -239,9 +216,7 @@ User: Lets read the whole file
239
216
 
240
217
  User: Read the first 10 lines of the file
241
218
  <extract>
242
- <targets>src/search/ranking.rs</targets>
243
- <line>1</line>
244
- <end_line>10</end_line>
219
+ <targets>src/search/ranking.rs:1-10</targets>
245
220
  </extract>
246
221
 
247
222
  User: Read file inside the dependency
@@ -349,7 +324,7 @@ export const bashDescription = 'Execute bash commands for system exploration and
349
324
  // Valid tool names that should be parsed as tool calls
350
325
  const DEFAULT_VALID_TOOLS = [
351
326
  'search',
352
- 'query',
327
+ 'query',
353
328
  'extract',
354
329
  'delegate',
355
330
  'listFiles',
@@ -358,6 +333,43 @@ const DEFAULT_VALID_TOOLS = [
358
333
  'attempt_completion'
359
334
  ];
360
335
 
336
+ /**
337
+ * Get valid parameter names for a specific tool from its schema
338
+ * @param {string} toolName - Name of the tool
339
+ * @returns {string[]} - Array of valid parameter names for this tool
340
+ */
341
+ function getValidParamsForTool(toolName) {
342
+ // Map tool names to their schemas
343
+ const schemaMap = {
344
+ search: searchSchema,
345
+ query: querySchema,
346
+ extract: extractSchema,
347
+ delegate: delegateSchema,
348
+ bash: bashSchema,
349
+ attempt_completion: attemptCompletionSchema
350
+ };
351
+
352
+ const schema = schemaMap[toolName];
353
+ if (!schema) {
354
+ // For tools without schema (listFiles, searchFiles, implement), return common params
355
+ // These are the shared params that appear across multiple tools
356
+ return ['path', 'directory', 'pattern', 'recursive', 'includeHidden', 'task', 'files', 'autoCommits', 'result'];
357
+ }
358
+
359
+ // For attempt_completion, it has custom validation, just return 'result'
360
+ if (toolName === 'attempt_completion') {
361
+ return ['result'];
362
+ }
363
+
364
+ // Extract keys from Zod schema
365
+ if (schema && schema._def && schema._def.shape) {
366
+ return Object.keys(schema._def.shape());
367
+ }
368
+
369
+ // Fallback: return empty array if we can't extract schema keys
370
+ return [];
371
+ }
372
+
361
373
  // Simple XML parser helper - safer string-based approach
362
374
  export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
363
375
  // Look for each valid tool name specifically using string search
@@ -387,15 +399,12 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
387
399
 
388
400
  const params = {};
389
401
 
402
+ // Get valid parameters for this specific tool from its schema
403
+ const validParams = getValidParamsForTool(toolName);
404
+
390
405
  // Parse parameters using string-based approach for better safety
391
- // Common parameter names to look for (can be extended as needed)
392
- // Note: includes both camelCase and underscore_case variants to handle inconsistencies
393
- const commonParams = ['query', 'file_path', 'line', 'end_line', 'path', 'recursive', 'includeHidden',
394
- 'max_results', 'maxResults', 'result', 'command', 'description', 'task', 'param', 'pattern',
395
- 'allow_tests', 'exact', 'maxTokens', 'language', 'input_content',
396
- 'context_lines', 'format', 'directory', 'autoCommits', 'files', 'targets'];
397
-
398
- for (const paramName of commonParams) {
406
+ // Only look for parameters that are valid for this specific tool
407
+ for (const paramName of validParams) {
399
408
  const paramOpenTag = `<${paramName}>`;
400
409
  const paramCloseTag = `</${paramName}>`;
401
410
 
@@ -410,7 +419,7 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
410
419
  if (paramCloseIndex === -1) {
411
420
  // Find the next opening tag after this parameter
412
421
  let nextTagIndex = innerContent.length;
413
- for (const nextParam of commonParams) {
422
+ for (const nextParam of validParams) {
414
423
  const nextOpenTag = `<${nextParam}>`;
415
424
  const nextIndex = innerContent.indexOf(nextOpenTag, paramOpenIndex + paramOpenTag.length);
416
425
  if (nextIndex !== -1 && nextIndex < nextTagIndex) {