@matimo/core 0.1.2 → 0.1.4

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 (49) hide show
  1. package/dist/runtime/index.d.ts +10 -0
  2. package/dist/runtime/index.d.ts.map +1 -0
  3. package/dist/runtime/index.js +10 -0
  4. package/dist/runtime/index.js.map +1 -0
  5. package/package.json +7 -2
  6. package/tools/calculator/calculator.js +111 -0
  7. package/tools/calculator/calculator.ts +1 -2
  8. package/tools/calculator/definition.yaml +1 -1
  9. package/tools/edit/definition.yaml +1 -1
  10. package/tools/edit/edit.js +144 -0
  11. package/tools/execute/definition.yaml +1 -1
  12. package/tools/execute/execute.js +157 -0
  13. package/tools/execute/execute.ts +6 -3
  14. package/tools/matimo_approve_tool/definition.yaml +1 -1
  15. package/tools/matimo_approve_tool/matimo_approve_tool.js +54 -0
  16. package/tools/matimo_create_skill/definition.yaml +1 -1
  17. package/tools/matimo_create_skill/matimo_create_skill.js +48 -0
  18. package/tools/matimo_create_skill/matimo_create_skill.ts +1 -1
  19. package/tools/matimo_create_tool/definition.yaml +1 -1
  20. package/tools/matimo_create_tool/matimo_create_tool.js +89 -0
  21. package/tools/matimo_get_skill/definition.yaml +1 -1
  22. package/tools/matimo_get_skill/matimo_get_skill.js +148 -0
  23. package/tools/matimo_get_skill/matimo_get_skill.ts +8 -1
  24. package/tools/matimo_get_tool/definition.yaml +1 -1
  25. package/tools/matimo_get_tool/matimo_get_tool.js +38 -0
  26. package/tools/matimo_get_tool_status/definition.yaml +1 -1
  27. package/tools/matimo_get_tool_status/matimo_get_tool_status.js +68 -0
  28. package/tools/matimo_list_skills/definition.yaml +1 -1
  29. package/tools/matimo_list_skills/matimo_list_skills.js +109 -0
  30. package/tools/matimo_list_user_tools/definition.yaml +1 -1
  31. package/tools/matimo_list_user_tools/matimo_list_user_tools.js +44 -0
  32. package/tools/matimo_reload_tools/definition.yaml +1 -1
  33. package/tools/matimo_reload_tools/matimo_reload_tools.js +21 -0
  34. package/tools/matimo_search_tools/definition.yaml +1 -1
  35. package/tools/matimo_search_tools/matimo_search_tools.js +59 -0
  36. package/tools/matimo_validate_skill/definition.yaml +1 -1
  37. package/tools/matimo_validate_skill/matimo_validate_skill.js +94 -0
  38. package/tools/matimo_validate_skill/matimo_validate_skill.ts +14 -3
  39. package/tools/matimo_validate_tool/definition.yaml +1 -1
  40. package/tools/matimo_validate_tool/matimo_validate_tool.js +134 -0
  41. package/tools/read/definition.yaml +1 -1
  42. package/tools/read/read.js +82 -0
  43. package/tools/search/definition.yaml +1 -1
  44. package/tools/search/search.js +140 -0
  45. package/tools/search/search.ts +14 -4
  46. package/tools/shared/skill-validation.js +251 -0
  47. package/tools/web/definition.yaml +1 -1
  48. package/tools/web/web.js +90 -0
  49. package/tools/web/web.ts +4 -3
@@ -23,7 +23,7 @@ parameters:
23
23
 
24
24
  execution:
25
25
  type: function
26
- code: './matimo_validate_skill.ts'
26
+ code: './matimo_validate_skill.js'
27
27
 
28
28
  output_schema:
29
29
  type: object
@@ -0,0 +1,94 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getGlobalMatimoLogger } from '@matimo/core';
4
+ import { parseSkillContent, validateFrontmatter, listBundledResources, } from '../shared/skill-validation.js';
5
+ /**
6
+ * Validate an existing skill against the Agent Skills specification.
7
+ *
8
+ * Checks:
9
+ * - SKILL.md exists
10
+ * - YAML frontmatter is present and valid
11
+ * - Name follows spec rules (lowercase, hyphens, max 64 chars)
12
+ * - Name matches directory name
13
+ * - Description is present and within limits
14
+ * - Optional fields follow constraints
15
+ * - Lists bundled resources for review
16
+ *
17
+ * @see https://agentskills.io/specification
18
+ */
19
+ export default async function matimoValidateSkill(params) {
20
+ const logger = getGlobalMatimoLogger();
21
+ const skillsDir = params.skills_dir || './matimo-tools/skills';
22
+ const failResult = (message, issues = []) => ({
23
+ valid: false,
24
+ name: params.name || '',
25
+ issues,
26
+ structure: { has_skill_md: false, resources: { scripts: [], references: [], assets: [], other: [] } },
27
+ message,
28
+ });
29
+ if (!params.name || params.name.trim().length === 0) {
30
+ return failResult('Skill name is required');
31
+ }
32
+ const skillDir = path.join(skillsDir, params.name);
33
+ const skillPath = path.join(skillDir, 'SKILL.md');
34
+ // Check directory exists
35
+ if (!fs.existsSync(skillDir)) {
36
+ return failResult(`Skill directory not found: ${skillDir}`);
37
+ }
38
+ // Check SKILL.md exists
39
+ if (!fs.existsSync(skillPath)) {
40
+ return failResult(`SKILL.md not found in ${skillDir}`, [
41
+ { field: 'SKILL.md', message: 'Required SKILL.md file is missing', severity: 'error' },
42
+ ]);
43
+ }
44
+ // Parse content
45
+ const content = fs.readFileSync(skillPath, 'utf-8');
46
+ const parseResult = parseSkillContent(content);
47
+ if (!parseResult.success) {
48
+ return failResult(parseResult.error, [
49
+ { field: 'frontmatter', message: parseResult.error, severity: 'error' },
50
+ ]);
51
+ }
52
+ const { frontmatter, body } = parseResult.parsed;
53
+ // Validate frontmatter (with directory name matching)
54
+ const fmResult = validateFrontmatter(frontmatter, params.name);
55
+ // Add warnings for best practices
56
+ const allIssues = [...fmResult.issues];
57
+ // Warn if body is empty
58
+ if (!body || body.trim().length === 0) {
59
+ allIssues.push({
60
+ field: 'body',
61
+ message: 'SKILL.md has no instructions body — add content after the frontmatter',
62
+ severity: 'warning',
63
+ });
64
+ }
65
+ // Warn if body is too long (spec recommends < 5000 tokens ≈ < 500 lines)
66
+ const lineCount = body.split('\n').length;
67
+ if (lineCount > 500) {
68
+ allIssues.push({
69
+ field: 'body',
70
+ message: `SKILL.md body has ${lineCount} lines — spec recommends < 500 lines. Consider splitting into referenced files.`,
71
+ severity: 'warning',
72
+ });
73
+ }
74
+ // List bundled resources
75
+ const resources = listBundledResources(skillDir);
76
+ const valid = allIssues.filter(i => i.severity === 'error').length === 0;
77
+ logger.info('matimo_validate_skill: validation complete', {
78
+ name: params.name,
79
+ valid,
80
+ issueCount: allIssues.length,
81
+ });
82
+ return {
83
+ valid,
84
+ name: params.name,
85
+ issues: allIssues,
86
+ structure: {
87
+ has_skill_md: true,
88
+ resources,
89
+ },
90
+ message: valid
91
+ ? `Skill "${params.name}" is valid per the Agent Skills specification.`
92
+ : `Skill "${params.name}" has ${allIssues.filter(i => i.severity === 'error').length} error(s).`,
93
+ };
94
+ }
@@ -5,9 +5,20 @@ import {
5
5
  parseSkillContent,
6
6
  validateFrontmatter,
7
7
  listBundledResources,
8
- type ValidationIssue,
9
- type BundledResources,
10
- } from '../shared/skill-validation';
8
+ } from '../shared/skill-validation.js';
9
+
10
+ interface ValidationIssue {
11
+ field: string;
12
+ message: string;
13
+ severity: 'error' | 'warning';
14
+ }
15
+
16
+ interface BundledResources {
17
+ scripts: string[];
18
+ references: string[];
19
+ assets: string[];
20
+ other: string[];
21
+ }
11
22
 
12
23
  interface ValidateSkillParams {
13
24
  /** Name of the skill directory to validate. */
@@ -15,7 +15,7 @@ parameters:
15
15
 
16
16
  execution:
17
17
  type: function
18
- code: './matimo_validate_tool.ts'
18
+ code: './matimo_validate_tool.js'
19
19
 
20
20
  output_schema:
21
21
  type: object
@@ -0,0 +1,134 @@
1
+ import yaml from 'js-yaml';
2
+ import { validateToolDefinition, validateToolContent, classifyRisk, getGlobalMatimoLogger, } from '@matimo/core';
3
+ const EXECUTION_TYPE_OPTIONS = ['command', 'http', 'function'];
4
+ const PARAMETER_TYPE_OPTIONS = ['string', 'number', 'boolean', 'array', 'object'];
5
+ const HTTP_METHOD_OPTIONS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
6
+ const AUTH_TYPE_OPTIONS = ['api_key', 'basic', 'bearer', 'oauth2', 'custom'];
7
+ const STATUS_OPTIONS = ['draft', 'approved', 'deprecated'];
8
+ /**
9
+ * Map known field paths to valid option sets so agents get actionable errors.
10
+ */
11
+ const VALID_OPTIONS_BY_PATH = {
12
+ 'execution.type': EXECUTION_TYPE_OPTIONS,
13
+ 'execution.method': HTTP_METHOD_OPTIONS,
14
+ 'authentication.type': AUTH_TYPE_OPTIONS,
15
+ status: STATUS_OPTIONS,
16
+ };
17
+ const PARAMETER_TYPE_PATTERN = /^parameters\.[^.]+\.type$/;
18
+ const PARAMETER_ENCODING_BACKOFF = /^error_handling\.backoff_type$/;
19
+ function getValidOptions(fieldPath) {
20
+ if (VALID_OPTIONS_BY_PATH[fieldPath])
21
+ return VALID_OPTIONS_BY_PATH[fieldPath];
22
+ if (PARAMETER_TYPE_PATTERN.test(fieldPath))
23
+ return PARAMETER_TYPE_OPTIONS;
24
+ if (PARAMETER_ENCODING_BACKOFF.test(fieldPath))
25
+ return ['linear', 'exponential'];
26
+ return undefined;
27
+ }
28
+ /**
29
+ * Convert a raw Zod issue into a human-readable SchemaError.
30
+ * Handles the most common patterns: missing fields, invalid enums,
31
+ * invalid discriminated union (execution.type).
32
+ */
33
+ function formatZodIssue(issue) {
34
+ const fieldPath = issue.path.length > 0 ? issue.path.join('.') : 'root';
35
+ const validOptions = getValidOptions(fieldPath);
36
+ let message;
37
+ switch (issue.code) {
38
+ case 'invalid_type':
39
+ if (issue.received === 'undefined') {
40
+ message = `Missing required field: \`${fieldPath}\``;
41
+ if (validOptions) {
42
+ message += ` — must be one of: ${validOptions.map((v) => `'${v}'`).join(', ')}`;
43
+ }
44
+ }
45
+ else {
46
+ message = `Invalid type for \`${fieldPath}\`: expected ${issue.expected ?? 'unknown'}, got ${issue.received ?? 'unknown'}`;
47
+ }
48
+ break;
49
+ case 'invalid_literal':
50
+ case 'invalid_enum_value':
51
+ message = `Invalid value for \`${fieldPath}\``;
52
+ if (validOptions) {
53
+ message += ` — must be one of: ${validOptions.map((v) => `'${v}'`).join(', ')}`;
54
+ }
55
+ else {
56
+ message += ` (${issue.message})`;
57
+ }
58
+ break;
59
+ case 'invalid_union':
60
+ // Discriminated union failure — most commonly execution.type
61
+ message = `Invalid value for \`${fieldPath}\``;
62
+ if (fieldPath === 'execution' || fieldPath === 'root') {
63
+ message = `Missing or invalid \`execution.type\` — must be one of: ${EXECUTION_TYPE_OPTIONS.map((v) => `'${v}'`).join(', ')}`;
64
+ return { field: 'execution.type', message, validOptions: EXECUTION_TYPE_OPTIONS };
65
+ }
66
+ if (validOptions) {
67
+ message += ` — must be one of: ${validOptions.map((v) => `'${v}'`).join(', ')}`;
68
+ }
69
+ else {
70
+ message += ` (${issue.message})`;
71
+ }
72
+ break;
73
+ default:
74
+ message = `\`${fieldPath}\`: ${issue.message}`;
75
+ }
76
+ return { field: fieldPath, message, ...(validOptions ? { validOptions } : {}) };
77
+ }
78
+ export default async function matimoValidateTool(params) {
79
+ const logger = getGlobalMatimoLogger();
80
+ const result = {
81
+ valid: true,
82
+ schemaErrors: [],
83
+ policyViolations: [],
84
+ riskLevel: 'low',
85
+ };
86
+ // Step 1: Parse YAML
87
+ let parsed;
88
+ try {
89
+ parsed = yaml.load(params.yaml_content);
90
+ }
91
+ catch (err) {
92
+ result.valid = false;
93
+ result.schemaErrors.push({
94
+ field: 'root',
95
+ message: `YAML parse error: ${err.message}`,
96
+ });
97
+ logger.warn('matimo_validate_tool: YAML parse failed', { error: err.message });
98
+ return result;
99
+ }
100
+ // Step 2: Validate against ToolDefinition schema
101
+ let tool;
102
+ try {
103
+ tool = validateToolDefinition(parsed);
104
+ }
105
+ catch (err) {
106
+ result.valid = false;
107
+ // MatimoError carries raw Zod issues in details.issues — use them for structured output
108
+ const matimoErr = err;
109
+ const rawIssues = matimoErr.details?.issues;
110
+ if (Array.isArray(rawIssues) && rawIssues.length > 0) {
111
+ result.schemaErrors = rawIssues.map(formatZodIssue);
112
+ }
113
+ else {
114
+ result.schemaErrors.push({ field: 'root', message: err.message });
115
+ }
116
+ logger.warn('matimo_validate_tool: schema validation failed', {
117
+ errorCount: result.schemaErrors.length,
118
+ });
119
+ return result;
120
+ }
121
+ // Step 3: Run content validator (as untrusted source)
122
+ const validation = validateToolContent(tool, { source: 'untrusted' });
123
+ if (!validation.valid) {
124
+ result.valid = false;
125
+ result.policyViolations = validation.violations.map((v) => ({
126
+ rule: v.rule,
127
+ severity: v.severity,
128
+ message: v.message,
129
+ }));
130
+ }
131
+ // Step 4: Classify risk
132
+ result.riskLevel = classifyRisk(tool);
133
+ return result;
134
+ }
@@ -37,7 +37,7 @@ parameters:
37
37
 
38
38
  execution:
39
39
  type: function
40
- code: './read.ts'
40
+ code: './read.js'
41
41
 
42
42
  output_schema:
43
43
  type: object
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Read Tool - Read file contents with line range support
4
+ * Function-type tool: Exports default async function
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { MatimoError, ErrorCode } from '@matimo/core';
9
+ /**
10
+ * Read file contents with optional line range
11
+ */
12
+ export default async function readTool(params) {
13
+ const { filePath, startLine, endLine, encoding = 'utf8', maxLines = 10000 } = params;
14
+ // Validate required parameter
15
+ if (!filePath) {
16
+ throw new MatimoError('Missing required parameter', ErrorCode.INVALID_PARAMETER, {
17
+ reason: 'filePath is required',
18
+ });
19
+ }
20
+ // Resolve path
21
+ const resolvedPath = filePath.startsWith('~')
22
+ ? path.join(process.env.HOME || '/', filePath.slice(1))
23
+ : path.isAbsolute(filePath)
24
+ ? filePath
25
+ : path.resolve(process.cwd(), filePath);
26
+ // Check file exists
27
+ if (!fs.existsSync(resolvedPath)) {
28
+ throw new MatimoError('File not found', ErrorCode.FILE_NOT_FOUND, {
29
+ filePath: resolvedPath,
30
+ });
31
+ }
32
+ // Get file stats
33
+ const stats = fs.statSync(resolvedPath);
34
+ if (!stats.isFile()) {
35
+ throw new MatimoError('Not a file', ErrorCode.EXECUTION_FAILED, {
36
+ filePath: resolvedPath,
37
+ reason: 'Path exists but is not a file',
38
+ });
39
+ }
40
+ if (stats.size > 50 * 1024 * 1024) {
41
+ throw new MatimoError('File too large', ErrorCode.EXECUTION_FAILED, {
42
+ filePath: resolvedPath,
43
+ size: stats.size,
44
+ maxSize: 50 * 1024 * 1024,
45
+ });
46
+ }
47
+ // Read file
48
+ const content = fs.readFileSync(resolvedPath, encoding);
49
+ const lines = content.split('\n');
50
+ // Check line count
51
+ if (lines.length > maxLines) {
52
+ throw new MatimoError('File exceeds maxLines limit', ErrorCode.EXECUTION_FAILED, {
53
+ filePath: resolvedPath,
54
+ lineCount: lines.length,
55
+ maxLines,
56
+ });
57
+ }
58
+ // Handle line range request
59
+ let readContent = content;
60
+ let readLines = lines.length;
61
+ const linesRequested = {};
62
+ if (startLine !== undefined || endLine !== undefined) {
63
+ const start = Math.max(0, (startLine || 1) - 1); // Convert to 0-based
64
+ const end = Math.min(lines.length, (endLine || lines.length)); // Convert to 0-based exclusive
65
+ readContent = lines.slice(start, end).join('\n');
66
+ readLines = end - start;
67
+ linesRequested.start = startLine;
68
+ linesRequested.end = endLine;
69
+ }
70
+ const result = {
71
+ success: true,
72
+ filePath: resolvedPath,
73
+ content: readContent,
74
+ encoding,
75
+ readLines,
76
+ lineCount: lines.length,
77
+ linesRequested: Object.keys(linesRequested).length > 0 ? linesRequested : undefined,
78
+ size: stats.size,
79
+ mtime: stats.mtime.toISOString(),
80
+ };
81
+ return result;
82
+ }
@@ -54,7 +54,7 @@ parameters:
54
54
 
55
55
  execution:
56
56
  type: function
57
- code: './search.ts'
57
+ code: './search.js'
58
58
 
59
59
  output_schema:
60
60
  type: object
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Search Tool - Search files with native Node.js fs and regex
4
+ * Function-type tool: Exports default async function
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { glob } from 'glob';
9
+ import { MatimoError, ErrorCode, getGlobalMatimoLogger } from '@matimo/core';
10
+ /**
11
+ * Search files for pattern matches
12
+ */
13
+ export default async function searchTool(params) {
14
+ const logger = getGlobalMatimoLogger();
15
+ const { query, directory = '.', filePattern = '**/*', isRegex = false, caseSensitive = false, maxResults = 50, contextLines = 2, } = params;
16
+ const excludePatterns = params.excludePatterns;
17
+ const startTime = Date.now();
18
+ logger.debug('Search tool invoked', {
19
+ query: query.substring(0, 100),
20
+ directory,
21
+ isRegex,
22
+ caseSensitive,
23
+ maxResults,
24
+ });
25
+ // Validate required parameter
26
+ if (!query) {
27
+ logger.error('Search tool: Missing required parameter', {
28
+ reason: 'query is required',
29
+ });
30
+ throw new MatimoError('Missing required parameter', ErrorCode.INVALID_PARAMETER, {
31
+ reason: 'query is required',
32
+ });
33
+ }
34
+ // Resolve directory
35
+ const resolvedDir = directory.startsWith('~')
36
+ ? path.join(process.env.HOME || '/', directory.slice(1))
37
+ : path.isAbsolute(directory)
38
+ ? directory
39
+ : path.resolve(process.cwd(), directory);
40
+ if (!fs.existsSync(resolvedDir)) {
41
+ logger.error('Search tool: Directory not found', {
42
+ directory: resolvedDir,
43
+ });
44
+ throw new MatimoError('Directory not found', ErrorCode.FILE_NOT_FOUND, {
45
+ directory: resolvedDir,
46
+ });
47
+ }
48
+ // Build search pattern
49
+ let searchRegex;
50
+ try {
51
+ if (isRegex) {
52
+ searchRegex = new RegExp(query, caseSensitive ? 'g' : 'gi');
53
+ }
54
+ else {
55
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
56
+ searchRegex = new RegExp(escapedQuery, caseSensitive ? 'g' : 'gi');
57
+ }
58
+ }
59
+ catch {
60
+ throw new MatimoError('Invalid regex pattern', ErrorCode.INVALID_PARAMETER, {
61
+ pattern: query,
62
+ isRegex,
63
+ });
64
+ }
65
+ // Enforce max results cap
66
+ const safeMaxResults = Math.min(maxResults, 1000);
67
+ // Find files matching pattern
68
+ const globPattern = path.join(resolvedDir, filePattern);
69
+ let files = [];
70
+ try {
71
+ const globOptions = {
72
+ nodir: true,
73
+ absolute: true,
74
+ };
75
+ if (excludePatterns && excludePatterns.length > 0) {
76
+ globOptions.ignore = excludePatterns;
77
+ }
78
+ files = await glob(globPattern, globOptions);
79
+ }
80
+ catch {
81
+ throw new MatimoError('Invalid glob pattern', ErrorCode.INVALID_PARAMETER, {
82
+ pattern: filePattern,
83
+ baseDirectory: resolvedDir,
84
+ });
85
+ }
86
+ const matches = [];
87
+ let filesSearched = 0;
88
+ // Search each file
89
+ for (const filePath of files) {
90
+ if (matches.length >= safeMaxResults)
91
+ break;
92
+ try {
93
+ // Skip binary files
94
+ const stats = fs.statSync(filePath);
95
+ if (stats.size > 5 * 1024 * 1024)
96
+ continue; // Skip files > 5MB
97
+ filesSearched++;
98
+ const content = fs.readFileSync(filePath, 'utf8');
99
+ const lines = content.split('\n');
100
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
101
+ if (matches.length >= safeMaxResults)
102
+ break;
103
+ const line = lines[lineNum];
104
+ const lineContent = line.trim();
105
+ const match = searchRegex.exec(line);
106
+ if (match) {
107
+ // Get context lines
108
+ const contextStart = Math.max(0, lineNum - contextLines);
109
+ const contextEnd = Math.min(lines.length, lineNum + contextLines + 1);
110
+ const context = lines.slice(contextStart, contextEnd);
111
+ matches.push({
112
+ filePath,
113
+ lineNumber: lineNum + 1,
114
+ lineContent,
115
+ matchIndex: match.index,
116
+ context,
117
+ });
118
+ // Reset regex for next exec
119
+ searchRegex.lastIndex = 0;
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // Skip files that can't be read
125
+ continue;
126
+ }
127
+ }
128
+ const result = {
129
+ success: true,
130
+ query,
131
+ directory: resolvedDir,
132
+ pattern: filePattern,
133
+ matches,
134
+ totalMatches: matches.length,
135
+ filesSearched,
136
+ duration: Date.now() - startTime,
137
+ truncated: matches.length >= safeMaxResults && files.length > filesSearched,
138
+ };
139
+ return result;
140
+ }
@@ -53,9 +53,10 @@ export default async function searchTool(params: SearchParams): Promise<SearchRe
53
53
  caseSensitive = false,
54
54
  maxResults = 50,
55
55
  contextLines = 2,
56
- excludePatterns = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
57
56
  } = params;
58
57
 
58
+ const excludePatterns = params.excludePatterns;
59
+
59
60
  const startTime = Date.now();
60
61
 
61
62
  logger.debug('Search tool invoked', {
@@ -116,11 +117,20 @@ export default async function searchTool(params: SearchParams): Promise<SearchRe
116
117
  let files: string[] = [];
117
118
 
118
119
  try {
119
- files = await glob(globPattern, {
120
+ const globOptions: {
121
+ nodir: true;
122
+ absolute: true;
123
+ ignore?: string[];
124
+ } = {
120
125
  nodir: true,
121
126
  absolute: true,
122
- ignore: excludePatterns,
123
- });
127
+ };
128
+
129
+ if (excludePatterns && excludePatterns.length > 0) {
130
+ globOptions.ignore = excludePatterns;
131
+ }
132
+
133
+ files = await glob(globPattern, globOptions);
124
134
  } catch {
125
135
  throw new MatimoError('Invalid glob pattern', ErrorCode.INVALID_PARAMETER, {
126
136
  pattern: filePattern,