@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.
Files changed (39) hide show
  1. package/package.json +3 -2
  2. package/tools/calculator/calculator.js +111 -0
  3. package/tools/calculator/definition.yaml +1 -1
  4. package/tools/edit/definition.yaml +1 -1
  5. package/tools/edit/edit.js +144 -0
  6. package/tools/execute/definition.yaml +1 -1
  7. package/tools/execute/execute.js +157 -0
  8. package/tools/matimo_approve_tool/definition.yaml +1 -1
  9. package/tools/matimo_approve_tool/matimo_approve_tool.js +54 -0
  10. package/tools/matimo_create_skill/definition.yaml +1 -1
  11. package/tools/matimo_create_skill/matimo_create_skill.js +48 -0
  12. package/tools/matimo_create_tool/definition.yaml +1 -1
  13. package/tools/matimo_create_tool/matimo_create_tool.js +89 -0
  14. package/tools/matimo_get_skill/definition.yaml +1 -1
  15. package/tools/matimo_get_skill/matimo_get_skill.js +148 -0
  16. package/tools/matimo_get_tool/definition.yaml +1 -1
  17. package/tools/matimo_get_tool/matimo_get_tool.js +38 -0
  18. package/tools/matimo_get_tool_status/definition.yaml +1 -1
  19. package/tools/matimo_get_tool_status/matimo_get_tool_status.js +68 -0
  20. package/tools/matimo_list_skills/definition.yaml +1 -1
  21. package/tools/matimo_list_skills/matimo_list_skills.js +109 -0
  22. package/tools/matimo_list_user_tools/definition.yaml +1 -1
  23. package/tools/matimo_list_user_tools/matimo_list_user_tools.js +44 -0
  24. package/tools/matimo_reload_tools/definition.yaml +1 -1
  25. package/tools/matimo_reload_tools/matimo_reload_tools.js +21 -0
  26. package/tools/matimo_search_tools/definition.yaml +1 -1
  27. package/tools/matimo_search_tools/matimo_search_tools.js +59 -0
  28. package/tools/matimo_validate_skill/definition.yaml +1 -1
  29. package/tools/matimo_validate_skill/matimo_validate_skill.js +94 -0
  30. package/tools/matimo_validate_tool/definition.yaml +1 -1
  31. package/tools/matimo_validate_tool/matimo_validate_tool.js +134 -0
  32. package/tools/read/definition.yaml +1 -1
  33. package/tools/read/read.js +82 -0
  34. package/tools/search/definition.yaml +1 -1
  35. package/tools/search/search.js +140 -0
  36. package/tools/shared/skill-validation.js +219 -208
  37. package/tools/web/definition.yaml +1 -1
  38. package/tools/web/web.js +90 -0
  39. package/tools/web/web.ts +2 -1
@@ -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
+ }
@@ -29,7 +29,7 @@ parameters:
29
29
 
30
30
  execution:
31
31
  type: function
32
- code: './matimo_get_skill.ts'
32
+ code: './matimo_get_skill.js'
33
33
 
34
34
  output_schema:
35
35
  type: object
@@ -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
+ }
@@ -19,7 +19,7 @@ parameters:
19
19
 
20
20
  execution:
21
21
  type: function
22
- code: './matimo_get_tool.ts'
22
+ code: './matimo_get_tool.js'
23
23
 
24
24
  output_schema:
25
25
  type: object
@@ -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
+ }
@@ -19,7 +19,7 @@ parameters:
19
19
 
20
20
  execution:
21
21
  type: function
22
- code: './matimo_get_tool_status.ts'
22
+ code: './matimo_get_tool_status.js'
23
23
 
24
24
  output_schema:
25
25
  type: object
@@ -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
+ }
@@ -37,7 +37,7 @@ parameters:
37
37
 
38
38
  execution:
39
39
  type: function
40
- code: './matimo_list_skills.ts'
40
+ code: './matimo_list_skills.js'
41
41
 
42
42
  output_schema:
43
43
  type: object
@@ -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
+ }
@@ -19,7 +19,7 @@ parameters:
19
19
 
20
20
  execution:
21
21
  type: function
22
- code: './matimo_list_user_tools.ts'
22
+ code: './matimo_list_user_tools.js'
23
23
 
24
24
  output_schema:
25
25
  type: object
@@ -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
+ }
@@ -16,7 +16,7 @@ parameters: {}
16
16
 
17
17
  execution:
18
18
  type: function
19
- code: './matimo_reload_tools.ts'
19
+ code: './matimo_reload_tools.js'
20
20
 
21
21
  output_schema:
22
22
  type: object
@@ -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
+ }
@@ -19,7 +19,7 @@ parameters:
19
19
 
20
20
  execution:
21
21
  type: function
22
- code: './matimo_search_tools.ts'
22
+ code: './matimo_search_tools.js'
23
23
 
24
24
  output_schema:
25
25
  type: object
@@ -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
+ }
@@ -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