@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matimo/core",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Core SDK for Matimo: Framework-agnostic YAML-driven tool ecosystem for AI agents.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -65,6 +65,7 @@
65
65
  "@modelcontextprotocol/sdk": "^1.29.0",
66
66
  "axios": "^1.15.2",
67
67
  "dotenv": "^17.2.3",
68
+ "glob": "^10.5.0",
68
69
  "js-yaml": "^4.1.1",
69
70
  "winston": "^3.19.0",
70
71
  "yaml": "^2.8.2",
@@ -102,7 +103,7 @@
102
103
  "typescript": "^5.9.3"
103
104
  },
104
105
  "scripts": {
105
- "build": "tsc",
106
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.tools.json",
106
107
  "watch": "tsc --watch",
107
108
  "test": "jest",
108
109
  "test:watch": "jest --watch",
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Calculator Tool - Perform basic arithmetic operations
3
+ * Pattern: Function-based tool (same as execute)
4
+ */
5
+ import { MatimoError, ErrorCode, getGlobalMatimoLogger } from '@matimo/core/runtime';
6
+ /**
7
+ * Normalize operation name to handle variations
8
+ */
9
+ function normalizeOperation(op) {
10
+ const normalized = op.toLowerCase().trim();
11
+ // Map variations to canonical operation names
12
+ const operationMap = {
13
+ // Addition variants
14
+ add: 'add',
15
+ addition: 'add',
16
+ sum: 'add',
17
+ plus: 'add',
18
+ '+': 'add',
19
+ // Subtraction variants
20
+ subtract: 'subtract',
21
+ subtraction: 'subtract',
22
+ minus: 'subtract',
23
+ sub: 'subtract',
24
+ '-': 'subtract',
25
+ // Multiplication variants
26
+ multiply: 'multiply',
27
+ multiplication: 'multiply',
28
+ times: 'multiply',
29
+ product: 'multiply',
30
+ mul: 'multiply',
31
+ '*': 'multiply',
32
+ x: 'multiply',
33
+ // Division variants
34
+ divide: 'divide',
35
+ division: 'divide',
36
+ div: 'divide',
37
+ '/': 'divide',
38
+ };
39
+ return operationMap[normalized] || normalized;
40
+ }
41
+ /**
42
+ * Perform arithmetic calculation
43
+ */
44
+ export default async function calculator(params) {
45
+ const logger = getGlobalMatimoLogger();
46
+ const { operation, a, b } = params;
47
+ logger.debug('Calculator tool invoked', {
48
+ operation,
49
+ a,
50
+ b,
51
+ });
52
+ if (typeof a !== 'number' || typeof b !== 'number') {
53
+ logger.error('Invalid calculator parameters', {
54
+ a,
55
+ b,
56
+ expectedTypes: 'numbers',
57
+ });
58
+ throw new MatimoError('Parameters a and b must be numbers', ErrorCode.INVALID_PARAMETER, {
59
+ a,
60
+ b,
61
+ });
62
+ }
63
+ const normalizedOp = normalizeOperation(operation);
64
+ let result;
65
+ switch (normalizedOp) {
66
+ case 'add':
67
+ result = a + b;
68
+ break;
69
+ case 'subtract':
70
+ result = a - b;
71
+ break;
72
+ case 'multiply':
73
+ result = a * b;
74
+ break;
75
+ case 'divide':
76
+ if (b === 0) {
77
+ logger.error('Division by zero attempted', {
78
+ a,
79
+ b,
80
+ });
81
+ throw new MatimoError('Division by zero', ErrorCode.EXECUTION_FAILED, {
82
+ a,
83
+ b,
84
+ });
85
+ }
86
+ result = a / b;
87
+ break;
88
+ default:
89
+ logger.error('Unsupported calculator operation', {
90
+ operation: normalizedOp,
91
+ requested: operation,
92
+ });
93
+ throw new MatimoError('Invalid operation', ErrorCode.INVALID_PARAMETER, {
94
+ operation,
95
+ normalizedOperation: normalizedOp,
96
+ validOperations: ['add', 'addition', 'sum', 'plus', 'subtract', 'subtraction', 'minus', 'multiply', 'multiplication', 'times', 'divide', 'division', 'div'],
97
+ });
98
+ }
99
+ const returnValue = {
100
+ result,
101
+ operation: normalizedOp,
102
+ original_operation: operation,
103
+ operands: { a, b },
104
+ };
105
+ logger.info('Calculator operation completed', {
106
+ operation: normalizedOp,
107
+ operands: { a, b },
108
+ result,
109
+ });
110
+ return returnValue;
111
+ }
@@ -18,7 +18,7 @@ parameters:
18
18
 
19
19
  execution:
20
20
  type: function
21
- code: './calculator.ts'
21
+ code: './calculator.js'
22
22
 
23
23
  output_schema:
24
24
  type: object
@@ -42,7 +42,7 @@ parameters:
42
42
 
43
43
  execution:
44
44
  type: function
45
- code: './edit.ts'
45
+ code: './edit.js'
46
46
 
47
47
  output_schema:
48
48
  type: object
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Edit Tool - Edit files with insert/replace/delete/append operations
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
+ * Edit file with insert/replace/delete/append operations
11
+ */
12
+ export default async function editTool(params) {
13
+ const { filePath, operation, content = '', startLine, endLine, backup = true } = params;
14
+ const startTime = Date.now();
15
+ // Validate required parameters
16
+ if (!filePath) {
17
+ throw new MatimoError('Missing required parameter', ErrorCode.INVALID_PARAMETER, {
18
+ reason: 'filePath is required',
19
+ });
20
+ }
21
+ if (!operation) {
22
+ throw new MatimoError('Missing required parameter', ErrorCode.INVALID_PARAMETER, {
23
+ reason: 'operation is required',
24
+ });
25
+ }
26
+ if (startLine === undefined) {
27
+ throw new MatimoError('Missing required parameter', ErrorCode.INVALID_PARAMETER, {
28
+ reason: 'startLine is required',
29
+ });
30
+ }
31
+ // Resolve path
32
+ const resolvedPath = filePath.startsWith('~')
33
+ ? path.join(process.env.HOME || '/', filePath.slice(1))
34
+ : path.isAbsolute(filePath)
35
+ ? filePath
36
+ : path.resolve(process.cwd(), filePath);
37
+ // Validate file exists
38
+ if (!fs.existsSync(resolvedPath)) {
39
+ throw new MatimoError('File not found', ErrorCode.FILE_NOT_FOUND, {
40
+ filePath: resolvedPath,
41
+ });
42
+ }
43
+ // Approval is handled by MatimoInstance based on 'requires_approval' in YAML
44
+ // Read original content
45
+ const originalContent = fs.readFileSync(resolvedPath, 'utf8');
46
+ const lines = originalContent.split('\n');
47
+ const result = {
48
+ success: false,
49
+ filePath: resolvedPath,
50
+ operation,
51
+ linesAffected: 0,
52
+ backupCreated: false,
53
+ newLineCount: 0,
54
+ duration: 0,
55
+ };
56
+ const newLines = [...lines];
57
+ let backupPath;
58
+ let previousContent;
59
+ // Create backup if requested
60
+ if (backup) {
61
+ backupPath = `${resolvedPath}.backup`;
62
+ fs.writeFileSync(backupPath, originalContent, 'utf8');
63
+ result.backupCreated = true;
64
+ result.backupPath = backupPath;
65
+ }
66
+ // Validate line numbers (1-based)
67
+ if (startLine < 1) {
68
+ throw new MatimoError('Invalid line number', ErrorCode.INVALID_PARAMETER, {
69
+ startLine,
70
+ reason: 'startLine must be >= 1',
71
+ });
72
+ }
73
+ // Convert to 0-based indexing
74
+ const startIdx = startLine - 1;
75
+ const endIdx = endLine ? endLine - 1 : startIdx;
76
+ switch (operation) {
77
+ case 'insert': {
78
+ // Insert before startLine
79
+ if (startIdx > newLines.length) {
80
+ throw new MatimoError('Invalid line range', ErrorCode.INVALID_PARAMETER, {
81
+ startLine,
82
+ reason: `startLine ${startLine} is beyond file length ${newLines.length}`,
83
+ });
84
+ }
85
+ const contentLines = content.split('\n');
86
+ newLines.splice(startIdx, 0, ...contentLines);
87
+ result.linesAffected = contentLines.length;
88
+ break;
89
+ }
90
+ case 'replace': {
91
+ // Replace lines from startLine to endLine
92
+ if (startIdx >= newLines.length || endIdx >= newLines.length) {
93
+ throw new MatimoError('Invalid line range', ErrorCode.INVALID_PARAMETER, {
94
+ startLine,
95
+ endLine,
96
+ fileLineCount: newLines.length,
97
+ reason: `Line range ${startLine}-${endLine} out of bounds`,
98
+ });
99
+ }
100
+ previousContent = newLines.slice(startIdx, endIdx + 1).join('\n');
101
+ const contentLines = content.split('\n');
102
+ newLines.splice(startIdx, endIdx - startIdx + 1, ...contentLines);
103
+ result.linesAffected = endIdx - startIdx + 1;
104
+ result.previousContent = previousContent;
105
+ break;
106
+ }
107
+ case 'delete': {
108
+ // Delete lines from startLine to endLine
109
+ if (startIdx >= newLines.length || endIdx >= newLines.length) {
110
+ throw new MatimoError('Invalid line range', ErrorCode.INVALID_PARAMETER, {
111
+ startLine,
112
+ endLine,
113
+ fileLineCount: newLines.length,
114
+ reason: `Line range ${startLine}-${endLine} out of bounds`,
115
+ });
116
+ }
117
+ previousContent = newLines.slice(startIdx, endIdx + 1).join('\n');
118
+ newLines.splice(startIdx, endIdx - startIdx + 1);
119
+ result.linesAffected = endIdx - startIdx + 1;
120
+ result.previousContent = previousContent;
121
+ break;
122
+ }
123
+ case 'append': {
124
+ // Append to end of file
125
+ const contentLines = content.split('\n');
126
+ newLines.push(...contentLines);
127
+ result.linesAffected = contentLines.length;
128
+ break;
129
+ }
130
+ default: {
131
+ throw new MatimoError('Unknown operation', ErrorCode.INVALID_PARAMETER, {
132
+ operation,
133
+ supportedOperations: ['insert', 'replace', 'delete', 'append'],
134
+ });
135
+ }
136
+ }
137
+ // Write new content
138
+ const newContent = newLines.join('\n');
139
+ fs.writeFileSync(resolvedPath, newContent, 'utf8');
140
+ result.success = true;
141
+ result.newLineCount = newLines.length;
142
+ result.duration = Date.now() - startTime;
143
+ return result;
144
+ }
@@ -36,7 +36,7 @@ parameters:
36
36
 
37
37
  execution:
38
38
  type: function
39
- code: './execute.ts'
39
+ code: './execute.js'
40
40
 
41
41
  output_schema:
42
42
  type: object
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Execute Tool - Execute shell commands with full output capture
3
+ * LangChain-style: uses exec() directly from same process
4
+ * Cross-platform: Windows (cmd.exe), Unix/Linux/Mac (sh/bash)
5
+ */
6
+ import { exec } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { MatimoError, ErrorCode, getGlobalMatimoLogger, getGlobalApprovalHandler, } from '@matimo/core/runtime';
9
+ const execAsync = promisify(exec);
10
+ /**
11
+ * Basic injection detection - checks for common shell metacharacters
12
+ * that could be used for command injection attacks
13
+ */
14
+ function detectCommandInjection(command) {
15
+ // Common injection patterns: command chaining, redirection, substitution
16
+ // Note: $\w+ pattern is handled separately below
17
+ const dangerousPatterns = [
18
+ /;/, // Command separator
19
+ /\|/, // Pipe
20
+ /&/, // Background/AND
21
+ /`/, // Command substitution (backticks)
22
+ /\$\(/, // Command substitution ($(command))
23
+ /</, // Input redirection
24
+ />/, // Output redirection
25
+ /\$\{/, // Variable expansion ${VAR}
26
+ ];
27
+ // Allow some safe variable expansions like $HOME, $PATH, but flag suspicious ones
28
+ const safeVars = /^\$(HOME|PATH|USER|PWD|SHELL|LANG|TERM)$/i;
29
+ // Check for dangerous patterns first
30
+ for (const pattern of dangerousPatterns) {
31
+ if (pattern.test(command)) {
32
+ return true;
33
+ }
34
+ }
35
+ // Special handling for environment variables: allow safe ones, flag suspicious ones
36
+ const variablePattern = /\$\w+/g;
37
+ const variables = command.match(variablePattern);
38
+ if (variables) {
39
+ for (const variable of variables) {
40
+ if (!safeVars.test(variable)) {
41
+ return true;
42
+ }
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Execute a shell command and return structured output
49
+ * Pattern based on LangChain.js exec/execSync approach
50
+ */
51
+ export default async function executeCommand(params) {
52
+ const logger = getGlobalMatimoLogger();
53
+ const { command, cwd, timeout = 30000 } = params;
54
+ const startTime = Date.now();
55
+ logger.debug('Execute tool: Command received', {
56
+ command: command.substring(0, 100),
57
+ cwd,
58
+ timeout,
59
+ });
60
+ if (!command || command.trim().length === 0) {
61
+ logger.error('Execute tool: Empty command provided', {
62
+ reason: 'No command provided',
63
+ });
64
+ throw new MatimoError('Command required', ErrorCode.INVALID_PARAMETER, {
65
+ reason: 'No command provided',
66
+ });
67
+ }
68
+ // Check for potential command injection
69
+ if (detectCommandInjection(command)) {
70
+ logger.warn('Execute tool: Command injection detected', {
71
+ command: command.substring(0, 100),
72
+ reason: 'Contains potentially dangerous shell metacharacters',
73
+ });
74
+ throw new MatimoError('Command injection detected', ErrorCode.INVALID_PARAMETER, {
75
+ reason: 'Command contains potentially dangerous shell metacharacters',
76
+ command: command,
77
+ });
78
+ }
79
+ // Check if command appears to be destructive and request approval if needed
80
+ // ApprovalHandler checks against centralized destructive keywords from YAML
81
+ const approvalHandler = getGlobalApprovalHandler();
82
+ if (approvalHandler.requiresApproval(false, command)) {
83
+ logger.info('Execute tool: Destructive command detected - requesting approval', {
84
+ command: command.substring(0, 100),
85
+ });
86
+ // Request user approval before executing destructive command
87
+ if (!approvalHandler.isPreApproved('execute')) {
88
+ await approvalHandler.requestApproval({
89
+ toolName: 'execute',
90
+ description: `Execute shell command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`,
91
+ params: { command, cwd },
92
+ });
93
+ }
94
+ }
95
+ try {
96
+ // SECURITY WARNING: This tool executes arbitrary shell commands directly.
97
+ // The 'command' parameter is passed to exec() without sanitization, creating
98
+ // a command injection vulnerability if user input is not properly validated.
99
+ // Basic injection detection is performed above, but this is NOT foolproof.
100
+ // Only use with trusted input or implement additional validation layers.
101
+ // exec() auto-selects shell: cmd.exe on Windows, /bin/sh on Unix
102
+ const { stdout, stderr } = await execAsync(command, {
103
+ cwd,
104
+ timeout,
105
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs
106
+ });
107
+ const duration = Date.now() - startTime;
108
+ // Convert Buffer to string if needed
109
+ const stdoutStr = typeof stdout === 'string' ? stdout : (stdout ? String(stdout) : '');
110
+ const stderrStr = typeof stderr === 'string' ? stderr : (stderr ? String(stderr) : '');
111
+ logger.info('Execute tool: Command completed successfully', {
112
+ command: command.substring(0, 100),
113
+ duration,
114
+ stdoutLength: stdoutStr.length,
115
+ stderrLength: stderrStr.length,
116
+ });
117
+ return {
118
+ success: true,
119
+ exitCode: 0,
120
+ stdout: stdoutStr.trim(),
121
+ stderr: stderrStr.trim(),
122
+ command,
123
+ duration,
124
+ };
125
+ }
126
+ catch (error) {
127
+ const duration = Date.now() - startTime;
128
+ // Type guard for error object
129
+ const errorObj = error;
130
+ const isTimeout = errorObj.killed || errorObj.signal === 'SIGTERM';
131
+ // If it's already a MatimoError, re-throw it
132
+ if (error instanceof MatimoError) {
133
+ throw error;
134
+ }
135
+ // Convert Buffer to string if needed
136
+ const stdoutStr = typeof errorObj.stdout === 'string' ? errorObj.stdout : (errorObj.stdout ? String(errorObj.stdout) : '');
137
+ const stderrStr = typeof errorObj.stderr === 'string' ? errorObj.stderr : (errorObj.stderr ? String(errorObj.stderr) : '');
138
+ logger.warn('Execute tool: Command execution failed', {
139
+ command: command.substring(0, 100),
140
+ duration,
141
+ exitCode: isTimeout ? -1 : (errorObj.code || 1),
142
+ isTimeout,
143
+ errorMessage: errorObj.message ? errorObj.message.substring(0, 100) : 'Unknown error',
144
+ stderrLength: stderrStr.length,
145
+ });
146
+ // For command execution failures, return structured result (not throw)
147
+ // This allows the agent to see what went wrong
148
+ return {
149
+ success: false,
150
+ exitCode: isTimeout ? -1 : (errorObj.code || 1),
151
+ stdout: stdoutStr.trim(),
152
+ stderr: stderrStr.trim(),
153
+ command,
154
+ duration,
155
+ };
156
+ }
157
+ }
@@ -19,7 +19,7 @@ parameters:
19
19
 
20
20
  execution:
21
21
  type: function
22
- code: './matimo_approve_tool.ts'
22
+ code: './matimo_approve_tool.js'
23
23
 
24
24
  output_schema:
25
25
  type: object
@@ -0,0 +1,54 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { validateToolDefinition, validateToolContent, ApprovalManifest, getGlobalMatimoLogger, } from '@matimo/core';
5
+ export default async function matimoApproveTool(params, context) {
6
+ const logger = getGlobalMatimoLogger();
7
+ const toolDir = params.tool_dir || './matimo-tools';
8
+ // Step 1: Read tool definition
9
+ const defPath = path.join(toolDir, params.name, 'definition.yaml');
10
+ if (!fs.existsSync(defPath)) {
11
+ return { success: false, message: `Tool not found: ${defPath}` };
12
+ }
13
+ const yamlContent = fs.readFileSync(defPath, 'utf-8');
14
+ // Step 2: Parse and validate
15
+ let tool;
16
+ try {
17
+ const parsed = yaml.load(yamlContent);
18
+ tool = validateToolDefinition(parsed);
19
+ }
20
+ catch (err) {
21
+ return { success: false, message: `Validation failed: ${err.message}` };
22
+ }
23
+ // Step 3: Re-run content validator
24
+ const validation = validateToolContent(tool, { source: 'untrusted' });
25
+ const criticalOrHigh = validation.violations.filter((v) => v.severity === 'critical' || v.severity === 'high');
26
+ if (criticalOrHigh.length > 0) {
27
+ return {
28
+ success: false,
29
+ message: 'Tool has policy violations that must be resolved before approval',
30
+ };
31
+ }
32
+ // Step 4: Approve via manifest
33
+ const approvalDir = path.resolve(toolDir);
34
+ const manifest = new ApprovalManifest(approvalDir, context?.credentials?.MATIMO_APPROVAL_SECRET);
35
+ const hash = manifest.computeHash(yamlContent);
36
+ manifest.approve(params.name, hash);
37
+ const approval = manifest.getApproval(params.name);
38
+ // Step 5: Update status in YAML
39
+ const parsed = yaml.load(yamlContent);
40
+ parsed.status = 'approved';
41
+ const updatedYaml = yaml.dump(parsed);
42
+ fs.writeFileSync(defPath, updatedYaml, 'utf-8');
43
+ logger.info('matimo_approve_tool: tool approved', {
44
+ name: params.name,
45
+ hash,
46
+ });
47
+ return {
48
+ success: true,
49
+ name: params.name,
50
+ hash,
51
+ approvedAt: approval?.approvedAt,
52
+ message: 'Tool approved. Effective after reload or immediately if auto-reload is active.',
53
+ };
54
+ }
@@ -33,7 +33,7 @@ parameters:
33
33
 
34
34
  execution:
35
35
  type: function
36
- code: './matimo_create_skill.ts'
36
+ code: './matimo_create_skill.js'
37
37
 
38
38
  output_schema:
39
39
  type: object
@@ -0,0 +1,48 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getGlobalMatimoLogger } from '@matimo/core';
4
+ import { validateSkillName, parseSkillContent, validateFrontmatter, } from '../shared/skill-validation.js';
5
+ /**
6
+ * Create a new skill following the Agent Skills specification.
7
+ *
8
+ * Validates the name (lowercase, hyphens, max 64 chars), ensures frontmatter
9
+ * has required fields (name, description), and enforces that the frontmatter
10
+ * name matches the directory name.
11
+ *
12
+ * @see https://agentskills.io/specification
13
+ */
14
+ export default async function matimoCreateSkill(params) {
15
+ const logger = getGlobalMatimoLogger();
16
+ const targetDir = params.target_dir || './matimo-tools/skills';
17
+ // Step 1: Validate the skill name against Agent Skills spec
18
+ const nameResult = validateSkillName(params.name);
19
+ if (!nameResult.valid) {
20
+ return { success: false, message: nameResult.error };
21
+ }
22
+ // Step 2: Parse and validate frontmatter
23
+ const parseResult = parseSkillContent(params.content);
24
+ if (!parseResult.success) {
25
+ return { success: false, message: parseResult.error };
26
+ }
27
+ const { frontmatter } = parseResult.parsed;
28
+ // Step 3: Validate frontmatter fields + name must match directory
29
+ const fmResult = validateFrontmatter(frontmatter, params.name);
30
+ if (!fmResult.valid) {
31
+ const firstError = fmResult.issues.find(i => i.severity === 'error');
32
+ return { success: false, message: firstError.message };
33
+ }
34
+ // Step 4: Write to disk
35
+ const skillDirPath = path.resolve(targetDir, params.name);
36
+ fs.mkdirSync(skillDirPath, { recursive: true });
37
+ const filePath = path.join(skillDirPath, 'SKILL.md');
38
+ fs.writeFileSync(filePath, params.content, 'utf-8');
39
+ logger.info('matimo_create_skill: skill created', {
40
+ name: params.name,
41
+ path: filePath,
42
+ });
43
+ return {
44
+ success: true,
45
+ path: filePath,
46
+ message: `Skill "${params.name}" created successfully.`,
47
+ };
48
+ }
@@ -31,7 +31,7 @@ parameters:
31
31
 
32
32
  execution:
33
33
  type: function
34
- code: './matimo_create_tool.ts'
34
+ code: './matimo_create_tool.js'
35
35
 
36
36
  output_schema:
37
37
  type: object