@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.
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/index.js.map +1 -0
- package/package.json +7 -2
- package/tools/calculator/calculator.js +111 -0
- package/tools/calculator/calculator.ts +1 -2
- 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/execute/execute.ts +6 -3
- 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_skill/matimo_create_skill.ts +1 -1
- 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_skill/matimo_get_skill.ts +8 -1
- 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_skill/matimo_validate_skill.ts +14 -3
- 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/search/search.ts +14 -4
- package/tools/shared/skill-validation.js +251 -0
- package/tools/web/definition.yaml +1 -1
- package/tools/web/web.js +90 -0
- package/tools/web/web.ts +4 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { validateToolDefinition, validateToolContent, classifyRisk, getTierForTool, getGlobalMatimoLogger, } from '@matimo/core';
|
|
5
|
+
const UNSAFE_NAME_PATTERN = /[/\\]|\.\.|[\x00-\x1f]/;
|
|
6
|
+
export default async function matimoCreateTool(params) {
|
|
7
|
+
const logger = getGlobalMatimoLogger();
|
|
8
|
+
const targetDir = params.target_dir || './matimo-tools';
|
|
9
|
+
// Step 1: Sanitize name
|
|
10
|
+
if (!params.name || params.name.trim().length === 0) {
|
|
11
|
+
return { success: false, message: 'Tool name is required' };
|
|
12
|
+
}
|
|
13
|
+
if (UNSAFE_NAME_PATTERN.test(params.name)) {
|
|
14
|
+
return { success: false, message: 'Tool name contains invalid characters (path traversal, backslash, or control characters)' };
|
|
15
|
+
}
|
|
16
|
+
if (params.name.startsWith('matimo_')) {
|
|
17
|
+
return { success: false, message: 'Tool name cannot start with reserved namespace "matimo_"' };
|
|
18
|
+
}
|
|
19
|
+
// Step 2: Parse YAML
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = yaml.load(params.yaml_content);
|
|
23
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
24
|
+
return { success: false, message: 'YAML must parse to an object' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return { success: false, message: `YAML parse error: ${err.message}` };
|
|
29
|
+
}
|
|
30
|
+
// Step 3: Force safety fields
|
|
31
|
+
parsed.name = params.name;
|
|
32
|
+
parsed.requires_approval = true;
|
|
33
|
+
parsed.status = 'draft';
|
|
34
|
+
// Step 4: Validate against schema
|
|
35
|
+
const yamlStr = yaml.dump(parsed);
|
|
36
|
+
try {
|
|
37
|
+
validateToolDefinition(yaml.load(yamlStr));
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return { success: false, message: `Schema validation failed: ${err.message}` };
|
|
41
|
+
}
|
|
42
|
+
// Step 5: Run content validator
|
|
43
|
+
const tool = validateToolDefinition(yaml.load(yamlStr));
|
|
44
|
+
const validation = validateToolContent(tool, { source: 'untrusted' });
|
|
45
|
+
const criticalOrHigh = validation.violations.filter((v) => v.severity === 'critical' || v.severity === 'high');
|
|
46
|
+
if (criticalOrHigh.length > 0) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
message: 'Tool failed policy validation',
|
|
50
|
+
errors: criticalOrHigh.map((v) => `[${v.severity}] ${v.rule}: ${v.message}`),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Step 6: Classify risk + tier
|
|
54
|
+
const riskLevel = classifyRisk(tool);
|
|
55
|
+
const tier = getTierForTool(tool);
|
|
56
|
+
const approvalState = tier === 'auto' ? 'auto-approved' : 'pending';
|
|
57
|
+
// Step 7: Write to disk
|
|
58
|
+
const toolDirPath = path.resolve(targetDir, params.name);
|
|
59
|
+
fs.mkdirSync(toolDirPath, { recursive: true });
|
|
60
|
+
let header = '';
|
|
61
|
+
if (params.proposed_by) {
|
|
62
|
+
header += `# Proposed by: ${params.proposed_by}\n`;
|
|
63
|
+
}
|
|
64
|
+
if (params.justification) {
|
|
65
|
+
header += `# Justification: ${params.justification}\n`;
|
|
66
|
+
}
|
|
67
|
+
if (header) {
|
|
68
|
+
header += '\n';
|
|
69
|
+
}
|
|
70
|
+
const filePath = path.join(toolDirPath, 'definition.yaml');
|
|
71
|
+
fs.writeFileSync(filePath, header + yamlStr, 'utf-8');
|
|
72
|
+
logger.info('matimo_create_tool: tool created', {
|
|
73
|
+
name: params.name,
|
|
74
|
+
path: filePath,
|
|
75
|
+
riskLevel,
|
|
76
|
+
approvalState,
|
|
77
|
+
});
|
|
78
|
+
const message = approvalState === 'auto-approved'
|
|
79
|
+
? 'Tool created and auto-approved (low-risk read-only). Ready for use.'
|
|
80
|
+
: 'Tool created as draft. Requires approval before execution. Use matimo_approve_tool to promote.';
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
path: filePath,
|
|
84
|
+
riskLevel,
|
|
85
|
+
status: 'draft',
|
|
86
|
+
approvalState,
|
|
87
|
+
message,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getGlobalMatimoLogger, getGlobalMatimoInstance, ToolLoader } from '@matimo/core';
|
|
4
|
+
import { parseSkillContent, listBundledResources } from '../shared/skill-validation.js';
|
|
5
|
+
/** Path traversal detection — defense-in-depth. */
|
|
6
|
+
const UNSAFE_NAME_PATTERN = /[/\\]|\.\.|[\x00-\x1f]/;
|
|
7
|
+
/**
|
|
8
|
+
* Helper: Find skill directory using auto-discovery (like matimo_list_skills)
|
|
9
|
+
*/
|
|
10
|
+
function findSkillDir(skillName, explicitSkillsDir) {
|
|
11
|
+
// Try explicit skills_dir first
|
|
12
|
+
if (explicitSkillsDir) {
|
|
13
|
+
const skillPath = path.join(explicitSkillsDir, skillName, 'SKILL.md');
|
|
14
|
+
if (fs.existsSync(skillPath)) {
|
|
15
|
+
return path.join(explicitSkillsDir, skillName);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Try MatimoInstance
|
|
19
|
+
try {
|
|
20
|
+
const matimo = getGlobalMatimoInstance();
|
|
21
|
+
if (matimo) {
|
|
22
|
+
const skills = matimo.listSkills();
|
|
23
|
+
const found = skills?.find((s) => s.name === skillName);
|
|
24
|
+
if (found) {
|
|
25
|
+
const skillPath = found._path;
|
|
26
|
+
if (skillPath && fs.existsSync(path.join(skillPath, 'SKILL.md'))) {
|
|
27
|
+
return skillPath;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Fall through
|
|
34
|
+
}
|
|
35
|
+
// Auto-discover from @matimo/* packages
|
|
36
|
+
try {
|
|
37
|
+
const toolLoader = new ToolLoader();
|
|
38
|
+
const discoveredPaths = toolLoader.autoDiscoverPackages();
|
|
39
|
+
for (const toolPath of discoveredPaths) {
|
|
40
|
+
const pkgDir = path.dirname(toolPath);
|
|
41
|
+
const skillPath = path.join(pkgDir, 'skills', skillName, 'SKILL.md');
|
|
42
|
+
if (fs.existsSync(skillPath)) {
|
|
43
|
+
return path.join(pkgDir, 'skills', skillName);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Fall through
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read a skill's content by name — Level 2 activation (SKILL.md) or
|
|
54
|
+
* Level 3 resource access (bundled file).
|
|
55
|
+
*
|
|
56
|
+
* When called without `file`, returns SKILL.md content + metadata + resource listing.
|
|
57
|
+
* When called with `file`, returns the contents of that bundled resource file.
|
|
58
|
+
*
|
|
59
|
+
* Skills are discovered in this order (priority):
|
|
60
|
+
* 1. Explicit skills_dir if provided
|
|
61
|
+
* 2. Global MatimoInstance (if initialized)
|
|
62
|
+
* 3. Auto-discovered @matimo/* packages
|
|
63
|
+
*
|
|
64
|
+
* @see https://agentskills.io/specification
|
|
65
|
+
*/
|
|
66
|
+
export default async function matimoGetSkill(params) {
|
|
67
|
+
const logger = getGlobalMatimoLogger();
|
|
68
|
+
if (!params.name || params.name.trim().length === 0) {
|
|
69
|
+
return { success: false, message: 'Skill name is required' };
|
|
70
|
+
}
|
|
71
|
+
if (UNSAFE_NAME_PATTERN.test(params.name)) {
|
|
72
|
+
return { success: false, message: 'Skill name contains invalid characters' };
|
|
73
|
+
}
|
|
74
|
+
// Find skill directory using auto-discovery
|
|
75
|
+
const skillDir = findSkillDir(params.name, params.skills_dir);
|
|
76
|
+
if (!skillDir) {
|
|
77
|
+
return { success: false, message: `Skill "${params.name}" not found` };
|
|
78
|
+
}
|
|
79
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
80
|
+
// Level 3: Read a specific bundled resource file
|
|
81
|
+
if (params.file) {
|
|
82
|
+
// For file paths, allow forward slashes but reject path traversal
|
|
83
|
+
if (/\.\.|\\/u.test(params.file) || /[\x00-\x1f]/.test(params.file)) {
|
|
84
|
+
return { success: false, message: 'File path contains invalid characters' };
|
|
85
|
+
}
|
|
86
|
+
const resourcePath = path.join(skillDir, params.file);
|
|
87
|
+
// Verify the resolved path stays within the skill directory
|
|
88
|
+
const resolvedPath = path.resolve(resourcePath);
|
|
89
|
+
const resolvedSkillDir = path.resolve(skillDir);
|
|
90
|
+
if (!resolvedPath.startsWith(resolvedSkillDir + path.sep) && resolvedPath !== resolvedSkillDir) {
|
|
91
|
+
return { success: false, message: 'File path escapes the skill directory' };
|
|
92
|
+
}
|
|
93
|
+
if (!fs.existsSync(resourcePath)) {
|
|
94
|
+
return { success: false, message: `Resource file "${params.file}" not found in skill "${params.name}"` };
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const fileContent = fs.readFileSync(resourcePath, 'utf-8');
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
name: params.name,
|
|
101
|
+
content: fileContent,
|
|
102
|
+
path: resourcePath,
|
|
103
|
+
message: `Resource file "${params.file}" retrieved successfully.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return { success: false, message: `Failed to read resource file: ${err.message}` };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Level 2: Read SKILL.md + metadata + resource listing
|
|
111
|
+
try {
|
|
112
|
+
const rawContent = fs.readFileSync(skillPath, 'utf-8');
|
|
113
|
+
const parseResult = parseSkillContent(rawContent);
|
|
114
|
+
const result = {
|
|
115
|
+
success: true,
|
|
116
|
+
name: params.name,
|
|
117
|
+
content: rawContent,
|
|
118
|
+
path: skillPath,
|
|
119
|
+
message: 'Skill retrieved successfully.',
|
|
120
|
+
};
|
|
121
|
+
if (parseResult.success && parseResult.parsed) {
|
|
122
|
+
const { frontmatter } = parseResult.parsed;
|
|
123
|
+
result.name = frontmatter.name || params.name;
|
|
124
|
+
result.description = frontmatter.description || '';
|
|
125
|
+
if (frontmatter.license)
|
|
126
|
+
result.license = frontmatter.license;
|
|
127
|
+
if (frontmatter.compatibility)
|
|
128
|
+
result.compatibility = frontmatter.compatibility;
|
|
129
|
+
if (frontmatter.metadata)
|
|
130
|
+
result.metadata = frontmatter.metadata;
|
|
131
|
+
}
|
|
132
|
+
// List bundled resources (Level 3 discovery)
|
|
133
|
+
result.resources = listBundledResources(skillDir);
|
|
134
|
+
logger.info('matimo_get_skill: skill retrieved', {
|
|
135
|
+
name: params.name,
|
|
136
|
+
path: skillPath,
|
|
137
|
+
});
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const errorMsg = err.message;
|
|
142
|
+
logger.error('matimo_get_skill: failed to read skill', {
|
|
143
|
+
name: params.name,
|
|
144
|
+
error: errorMsg,
|
|
145
|
+
});
|
|
146
|
+
return { success: false, message: `Failed to read skill: ${errorMsg}` };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getGlobalMatimoLogger, getGlobalMatimoInstance, ToolLoader, SkillSummary } from '@matimo/core';
|
|
4
|
-
import { parseSkillContent, listBundledResources
|
|
4
|
+
import { parseSkillContent, listBundledResources } from '../shared/skill-validation.js';
|
|
5
|
+
|
|
6
|
+
interface BundledResources {
|
|
7
|
+
scripts: string[];
|
|
8
|
+
references: string[];
|
|
9
|
+
assets: string[];
|
|
10
|
+
other: string[];
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
interface GetSkillParams {
|
|
7
14
|
name: string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { validateToolDefinition, getGlobalMatimoLogger } from '@matimo/core';
|
|
5
|
+
export default async function matimoGetTool(params) {
|
|
6
|
+
const logger = getGlobalMatimoLogger();
|
|
7
|
+
const toolDir = params.tool_dir ?? './matimo-tools';
|
|
8
|
+
const defPath = path.join(toolDir, params.name, 'definition.yaml');
|
|
9
|
+
if (!fs.existsSync(defPath)) {
|
|
10
|
+
logger.warn('matimo_get_tool: tool not found', { name: params.name, path: defPath });
|
|
11
|
+
return { found: false, message: `Tool "${params.name}" not found at ${defPath}` };
|
|
12
|
+
}
|
|
13
|
+
const yamlContent = fs.readFileSync(defPath, 'utf-8');
|
|
14
|
+
let definition;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = yaml.load(yamlContent);
|
|
17
|
+
const validated = validateToolDefinition(parsed);
|
|
18
|
+
// Omit internal _definitionPath from the returned object
|
|
19
|
+
const { _definitionPath: _, ...rest } = validated;
|
|
20
|
+
definition = rest;
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return {
|
|
24
|
+
found: true,
|
|
25
|
+
name: params.name,
|
|
26
|
+
yaml_content: yamlContent,
|
|
27
|
+
message: `Tool YAML is invalid: ${err.message}`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
logger.debug('matimo_get_tool: retrieved', { name: params.name });
|
|
31
|
+
return {
|
|
32
|
+
found: true,
|
|
33
|
+
name: params.name,
|
|
34
|
+
yaml_content: yamlContent,
|
|
35
|
+
definition,
|
|
36
|
+
message: `Tool "${params.name}" retrieved successfully`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { validateToolDefinition, classifyRisk, getTierForTool, ApprovalManifest, getGlobalMatimoLogger, } from '@matimo/core';
|
|
5
|
+
export default async function matimoGetToolStatus(params, context) {
|
|
6
|
+
const logger = getGlobalMatimoLogger();
|
|
7
|
+
const toolDir = params.tool_dir || './matimo-tools';
|
|
8
|
+
const defPath = path.join(toolDir, params.name, 'definition.yaml');
|
|
9
|
+
if (!fs.existsSync(defPath)) {
|
|
10
|
+
logger.warn('matimo_get_tool_status: tool not found', { name: params.name, path: defPath });
|
|
11
|
+
return { found: false, message: `Tool "${params.name}" not found at ${defPath}` };
|
|
12
|
+
}
|
|
13
|
+
const yamlContent = fs.readFileSync(defPath, 'utf-8');
|
|
14
|
+
let tool;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = yaml.load(yamlContent);
|
|
17
|
+
tool = validateToolDefinition(parsed);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return {
|
|
21
|
+
found: true,
|
|
22
|
+
name: params.name,
|
|
23
|
+
message: `Tool YAML is invalid: ${err.message}`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const riskLevel = classifyRisk(tool);
|
|
27
|
+
const tier = getTierForTool(tool);
|
|
28
|
+
// Determine approval state from manifest
|
|
29
|
+
const approvalDir = path.resolve(toolDir);
|
|
30
|
+
const manifest = new ApprovalManifest(approvalDir, context?.credentials?.MATIMO_APPROVAL_SECRET);
|
|
31
|
+
const hash = manifest.computeHash(yamlContent);
|
|
32
|
+
const approvalRecord = manifest.getApproval(params.name);
|
|
33
|
+
const isApproved = approvalRecord ? manifest.isApproved(params.name, hash) : false;
|
|
34
|
+
const pendingTools = manifest.getPendingTools();
|
|
35
|
+
let approvalState;
|
|
36
|
+
if (tool.status === 'deprecated') {
|
|
37
|
+
approvalState = 'rejected';
|
|
38
|
+
}
|
|
39
|
+
else if (isApproved) {
|
|
40
|
+
approvalState = 'approved';
|
|
41
|
+
}
|
|
42
|
+
else if (tier === 'auto') {
|
|
43
|
+
approvalState = 'auto-approved';
|
|
44
|
+
}
|
|
45
|
+
else if (pendingTools.includes(params.name)) {
|
|
46
|
+
approvalState = 'pending';
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Tool exists on disk but no pending record and not approved — treat as pending
|
|
50
|
+
approvalState = 'pending';
|
|
51
|
+
}
|
|
52
|
+
logger.debug('matimo_get_tool_status: status retrieved', {
|
|
53
|
+
name: params.name,
|
|
54
|
+
status: tool.status,
|
|
55
|
+
riskLevel,
|
|
56
|
+
approvalState,
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
found: true,
|
|
60
|
+
name: params.name,
|
|
61
|
+
status: tool.status ?? 'draft',
|
|
62
|
+
riskLevel,
|
|
63
|
+
approvalState,
|
|
64
|
+
approvedAt: approvalRecord?.approvedAt,
|
|
65
|
+
approvedBy: approvalRecord?.approvedBy,
|
|
66
|
+
message: `Tool "${params.name}" is ${approvalState} (${riskLevel} risk)`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { getGlobalMatimoLogger, getGlobalMatimoInstance, extractSkillMetadata, ToolLoader } from '@matimo/core';
|
|
4
|
+
/**
|
|
5
|
+
* Helper: Load SKILL.md files from a directory and extract metadata.
|
|
6
|
+
*/
|
|
7
|
+
function loadSkillsFromPath(skillsPath, source, logger) {
|
|
8
|
+
const skills = [];
|
|
9
|
+
if (!fs.existsSync(skillsPath))
|
|
10
|
+
return skills;
|
|
11
|
+
try {
|
|
12
|
+
const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (!entry.isDirectory())
|
|
15
|
+
continue;
|
|
16
|
+
const skillFilePath = path.join(skillsPath, entry.name, 'SKILL.md');
|
|
17
|
+
if (!fs.existsSync(skillFilePath))
|
|
18
|
+
continue;
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(skillFilePath, 'utf-8');
|
|
21
|
+
const result = extractSkillMetadata(content, source);
|
|
22
|
+
if (result.success && result.metadata) {
|
|
23
|
+
skills.push(result.metadata);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
logger.debug('matimo_list_skills: failed to extract metadata', {
|
|
28
|
+
skill: entry.name,
|
|
29
|
+
error: err.message,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
logger.debug('matimo_list_skills: failed to read directory', {
|
|
36
|
+
path: skillsPath,
|
|
37
|
+
error: err.message,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return skills;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* List all skills available in the current Matimo instance.
|
|
44
|
+
*
|
|
45
|
+
* Skills are discovered in this order (priority):
|
|
46
|
+
* 1. Global MatimoInstance (if initialized) — includes auto-discovered @matimo/* skills
|
|
47
|
+
* 2. Auto-discover from @matimo/* packages in node_modules (like tools do)
|
|
48
|
+
* 3. Explicit skills_dir if provided
|
|
49
|
+
*
|
|
50
|
+
* Returns METADATA ONLY: name, description, license, version, metadata, source.
|
|
51
|
+
* Full body content is available via matimo_get_skill when explicitly requested.
|
|
52
|
+
*
|
|
53
|
+
* Uses lightweight YAML-only extraction (no body/sections parsing) for efficiency.
|
|
54
|
+
* This avoids the overhead of parsing skill markdown sections and keeps responses small.
|
|
55
|
+
*/
|
|
56
|
+
export default async function matimoListSkills(params) {
|
|
57
|
+
const logger = getGlobalMatimoLogger();
|
|
58
|
+
const allSkills = new Map();
|
|
59
|
+
try {
|
|
60
|
+
// Try global MatimoInstance first
|
|
61
|
+
try {
|
|
62
|
+
const matimo = getGlobalMatimoInstance();
|
|
63
|
+
if (matimo) {
|
|
64
|
+
const matimoSkills = matimo.listSkills();
|
|
65
|
+
if (matimoSkills?.length > 0) {
|
|
66
|
+
logger.debug('matimo_list_skills: from MatimoInstance', { count: matimoSkills.length });
|
|
67
|
+
matimoSkills.forEach((s) => allSkills.set(s.name, s));
|
|
68
|
+
return { skills: Array.from(allSkills.values()), total: allSkills.size };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
logger.debug('matimo_list_skills: MatimoInstance unavailable', {
|
|
74
|
+
error: err.message,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Auto-discover from @matimo/* packages
|
|
78
|
+
try {
|
|
79
|
+
const toolLoader = new ToolLoader();
|
|
80
|
+
const discoveredPaths = toolLoader.autoDiscoverPackages();
|
|
81
|
+
for (const toolPath of discoveredPaths) {
|
|
82
|
+
const pkgDir = path.dirname(toolPath);
|
|
83
|
+
const skillsPath = path.join(pkgDir, 'skills');
|
|
84
|
+
const discovered = loadSkillsFromPath(skillsPath, 'builtin', logger);
|
|
85
|
+
discovered.forEach((s) => allSkills.set(s.name, s));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
logger.debug('matimo_list_skills: auto-discovery failed', {
|
|
90
|
+
error: err.message,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Load from explicit skills_dir if provided
|
|
94
|
+
if (params.skills_dir) {
|
|
95
|
+
const skillsDir = path.resolve(params.skills_dir);
|
|
96
|
+
const discovered = loadSkillsFromPath(skillsDir, 'user', logger);
|
|
97
|
+
discovered.forEach((s) => allSkills.set(s.name, s));
|
|
98
|
+
}
|
|
99
|
+
const results = Array.from(allSkills.values());
|
|
100
|
+
logger.debug('matimo_list_skills: complete', { total: results.length });
|
|
101
|
+
return { skills: results, total: results.length };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logger.error('matimo_list_skills: failed', {
|
|
105
|
+
error: err.message,
|
|
106
|
+
});
|
|
107
|
+
return { skills: [], total: 0 };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { validateToolDefinition, classifyRisk, getGlobalMatimoLogger, } from '@matimo/core';
|
|
5
|
+
export default async function matimoListUserTools(params) {
|
|
6
|
+
const logger = getGlobalMatimoLogger();
|
|
7
|
+
const toolDir = params.tool_dir || './matimo-tools';
|
|
8
|
+
const includeDrafts = params.include_drafts !== false;
|
|
9
|
+
const tools = [];
|
|
10
|
+
if (!fs.existsSync(toolDir)) {
|
|
11
|
+
return { tools: [], total: 0 };
|
|
12
|
+
}
|
|
13
|
+
const entries = fs.readdirSync(toolDir, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (!entry.isDirectory())
|
|
16
|
+
continue;
|
|
17
|
+
const defPath = path.join(toolDir, entry.name, 'definition.yaml');
|
|
18
|
+
if (!fs.existsSync(defPath))
|
|
19
|
+
continue;
|
|
20
|
+
try {
|
|
21
|
+
const content = fs.readFileSync(defPath, 'utf-8');
|
|
22
|
+
const parsed = yaml.load(content);
|
|
23
|
+
const tool = validateToolDefinition(parsed);
|
|
24
|
+
const status = tool.status || 'approved';
|
|
25
|
+
if (!includeDrafts && status === 'draft')
|
|
26
|
+
continue;
|
|
27
|
+
tools.push({
|
|
28
|
+
name: tool.name,
|
|
29
|
+
description: tool.description,
|
|
30
|
+
version: tool.version,
|
|
31
|
+
status,
|
|
32
|
+
riskLevel: classifyRisk(tool),
|
|
33
|
+
tags: tool.tags || [],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
logger.warn('matimo_list_user_tools: failed to parse tool', {
|
|
38
|
+
dir: entry.name,
|
|
39
|
+
error: err.message,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { tools, total: tools.length };
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getGlobalMatimoLogger } from '@matimo/core';
|
|
2
|
+
/**
|
|
3
|
+
* matimo_reload_tools — Hot-reload all tools from configured toolPaths.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This function is a placeholder. The actual reload logic is intercepted
|
|
6
|
+
* and handled directly by MatimoInstance.execute() because reloadTools() is a
|
|
7
|
+
* method on the instance itself (it clears the registry, re-reads YAML from
|
|
8
|
+
* disk, re-validates untrusted tools, etc.). The function executor cannot do
|
|
9
|
+
* this because it doesn't have a reference to the MatimoInstance.
|
|
10
|
+
*
|
|
11
|
+
* If this file is ever reached (e.g., in tests without the interception),
|
|
12
|
+
* it returns an error instructing the caller to use matimo.reloadTools() directly.
|
|
13
|
+
*/
|
|
14
|
+
export default async function matimoReloadTools() {
|
|
15
|
+
const logger = getGlobalMatimoLogger();
|
|
16
|
+
logger.warn('matimo_reload_tools: reached fallback implementation — this should be intercepted by MatimoInstance.execute()');
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
message: 'Reload must be handled by MatimoInstance. If you see this, the interception in execute() is not active.',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getGlobalMatimoInstance, getGlobalMatimoLogger, validateToolDefinition, classifyRisk } from '@matimo/core';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
export default async function matimoSearchTools(params) {
|
|
6
|
+
const logger = getGlobalMatimoLogger();
|
|
7
|
+
const query = params.query ?? '';
|
|
8
|
+
const limit = params.limit ?? 20;
|
|
9
|
+
const toSummary = (tool) => ({
|
|
10
|
+
name: tool.name,
|
|
11
|
+
description: tool.description,
|
|
12
|
+
version: tool.version,
|
|
13
|
+
tags: tool.tags ?? [],
|
|
14
|
+
riskLevel: classifyRisk(tool),
|
|
15
|
+
});
|
|
16
|
+
// Prefer registry search via the global instance (has all loaded tools)
|
|
17
|
+
try {
|
|
18
|
+
const instance = getGlobalMatimoInstance();
|
|
19
|
+
const found = instance.searchTools(query).slice(0, limit);
|
|
20
|
+
logger.debug('matimo_search_tools: registry search', { query, count: found.length });
|
|
21
|
+
return {
|
|
22
|
+
results: found.map(toSummary),
|
|
23
|
+
total: found.length,
|
|
24
|
+
query,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Global instance not set — fall through to disk-based scan below
|
|
29
|
+
logger.debug('matimo_search_tools: no global instance, falling back to disk scan');
|
|
30
|
+
}
|
|
31
|
+
// Fallback: scan the default tools directory
|
|
32
|
+
const toolDir = './matimo-tools';
|
|
33
|
+
if (!fs.existsSync(toolDir)) {
|
|
34
|
+
return { results: [], total: 0, query };
|
|
35
|
+
}
|
|
36
|
+
const lowerQuery = query.toLowerCase();
|
|
37
|
+
const results = [];
|
|
38
|
+
for (const entry of fs.readdirSync(toolDir, { withFileTypes: true })) {
|
|
39
|
+
if (!entry.isDirectory())
|
|
40
|
+
continue;
|
|
41
|
+
const defPath = path.join(toolDir, entry.name, 'definition.yaml');
|
|
42
|
+
if (!fs.existsSync(defPath))
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const tool = validateToolDefinition(yaml.load(fs.readFileSync(defPath, 'utf-8')));
|
|
46
|
+
const matches = tool.name.toLowerCase().includes(lowerQuery) ||
|
|
47
|
+
tool.description.toLowerCase().includes(lowerQuery) ||
|
|
48
|
+
(tool.tags ?? []).some((t) => t.toLowerCase().includes(lowerQuery));
|
|
49
|
+
if (matches)
|
|
50
|
+
results.push(toSummary(tool));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Skip invalid definitions silently
|
|
54
|
+
}
|
|
55
|
+
if (results.length >= limit)
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
return { results, total: results.length, query };
|
|
59
|
+
}
|