@matimo/core 0.1.3 → 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.
- package/package.json +3 -2
- package/tools/calculator/calculator.js +111 -0
- package/tools/calculator/definition.yaml +1 -1
- package/tools/edit/definition.yaml +1 -1
- package/tools/edit/edit.js +144 -0
- package/tools/execute/definition.yaml +1 -1
- package/tools/execute/execute.js +157 -0
- package/tools/matimo_approve_tool/definition.yaml +1 -1
- package/tools/matimo_approve_tool/matimo_approve_tool.js +54 -0
- package/tools/matimo_create_skill/definition.yaml +1 -1
- package/tools/matimo_create_skill/matimo_create_skill.js +48 -0
- package/tools/matimo_create_tool/definition.yaml +1 -1
- package/tools/matimo_create_tool/matimo_create_tool.js +89 -0
- package/tools/matimo_get_skill/definition.yaml +1 -1
- package/tools/matimo_get_skill/matimo_get_skill.js +148 -0
- package/tools/matimo_get_tool/definition.yaml +1 -1
- package/tools/matimo_get_tool/matimo_get_tool.js +38 -0
- package/tools/matimo_get_tool_status/definition.yaml +1 -1
- package/tools/matimo_get_tool_status/matimo_get_tool_status.js +68 -0
- package/tools/matimo_list_skills/definition.yaml +1 -1
- package/tools/matimo_list_skills/matimo_list_skills.js +109 -0
- package/tools/matimo_list_user_tools/definition.yaml +1 -1
- package/tools/matimo_list_user_tools/matimo_list_user_tools.js +44 -0
- package/tools/matimo_reload_tools/definition.yaml +1 -1
- package/tools/matimo_reload_tools/matimo_reload_tools.js +21 -0
- package/tools/matimo_search_tools/definition.yaml +1 -1
- package/tools/matimo_search_tools/matimo_search_tools.js +59 -0
- package/tools/matimo_validate_skill/definition.yaml +1 -1
- package/tools/matimo_validate_skill/matimo_validate_skill.js +94 -0
- package/tools/matimo_validate_tool/definition.yaml +1 -1
- package/tools/matimo_validate_tool/matimo_validate_tool.js +134 -0
- package/tools/read/definition.yaml +1 -1
- package/tools/read/read.js +82 -0
- package/tools/search/definition.yaml +1 -1
- package/tools/search/search.js +140 -0
- package/tools/shared/skill-validation.js +219 -208
- package/tools/web/definition.yaml +1 -1
- package/tools/web/web.js +90 -0
- package/tools/web/web.ts +2 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|