@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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared validation utilities for Agent Skills — aligned with the official
|
|
3
|
+
* Agent Skills specification (https://agentskills.io/specification).
|
|
4
|
+
*
|
|
5
|
+
* Covers: name rules, description rules, frontmatter parsing/validation,
|
|
6
|
+
* and bundled resource discovery.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
// ── Name Validation ──────────────────────────────────────────────────────────
|
|
11
|
+
/** Spec: lowercase letters, numbers, hyphens only. */
|
|
12
|
+
const VALID_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
13
|
+
/** Consecutive hyphens are not allowed. */
|
|
14
|
+
const CONSECUTIVE_HYPHENS = /--/;
|
|
15
|
+
/** Max length for skill name. */
|
|
16
|
+
const MAX_NAME_LENGTH = 64;
|
|
17
|
+
/** Max length for description. */
|
|
18
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
19
|
+
/** Max length for compatibility field. */
|
|
20
|
+
const MAX_COMPATIBILITY_LENGTH = 500;
|
|
21
|
+
/** Path traversal detection — kept for defense-in-depth. */
|
|
22
|
+
const UNSAFE_NAME_PATTERN = /[/\\]|\.\.|[\x00-\x1f]/;
|
|
23
|
+
/**
|
|
24
|
+
* Validate a skill name against the Agent Skills spec.
|
|
25
|
+
*
|
|
26
|
+
* Rules:
|
|
27
|
+
* - 1–64 characters
|
|
28
|
+
* - Lowercase letters, numbers, hyphens only
|
|
29
|
+
* - Must not start or end with a hyphen
|
|
30
|
+
* - Must not contain consecutive hyphens (--)
|
|
31
|
+
* - Must not contain path traversal characters
|
|
32
|
+
*/
|
|
33
|
+
export function validateSkillName(name) {
|
|
34
|
+
if (!name || name.trim().length === 0) {
|
|
35
|
+
return { valid: false, error: 'Skill name is required' };
|
|
36
|
+
}
|
|
37
|
+
if (UNSAFE_NAME_PATTERN.test(name)) {
|
|
38
|
+
return { valid: false, error: 'Skill name contains invalid characters' };
|
|
39
|
+
}
|
|
40
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
41
|
+
return { valid: false, error: `Skill name must be at most ${MAX_NAME_LENGTH} characters (got ${name.length})` };
|
|
42
|
+
}
|
|
43
|
+
if (!VALID_NAME_PATTERN.test(name)) {
|
|
44
|
+
return {
|
|
45
|
+
valid: false,
|
|
46
|
+
error: 'Skill name must contain only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (CONSECUTIVE_HYPHENS.test(name)) {
|
|
50
|
+
return { valid: false, error: 'Skill name must not contain consecutive hyphens (--)' };
|
|
51
|
+
}
|
|
52
|
+
return { valid: true };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse YAML frontmatter from SKILL.md content.
|
|
56
|
+
*
|
|
57
|
+
* Handles the spec's required fields (name, description) and optional fields
|
|
58
|
+
* (license, compatibility, metadata, allowed-tools).
|
|
59
|
+
*/
|
|
60
|
+
export function parseSkillContent(content) {
|
|
61
|
+
if (!content || !content.startsWith('---')) {
|
|
62
|
+
return { success: false, error: 'Skill content must start with YAML frontmatter (---)' };
|
|
63
|
+
}
|
|
64
|
+
const endIndex = content.indexOf('---', 3);
|
|
65
|
+
if (endIndex === -1) {
|
|
66
|
+
return { success: false, error: 'Skill content must have closing YAML frontmatter (---)' };
|
|
67
|
+
}
|
|
68
|
+
const frontmatterBlock = content.substring(3, endIndex).trim();
|
|
69
|
+
const body = content.substring(endIndex + 3).trim();
|
|
70
|
+
// Parse frontmatter lines
|
|
71
|
+
const fields = {};
|
|
72
|
+
let currentMetadata = null;
|
|
73
|
+
let currentArray = null;
|
|
74
|
+
let currentArrayKey = null;
|
|
75
|
+
for (const line of frontmatterBlock.split('\n')) {
|
|
76
|
+
// Detect array elements (prefixed with "- ")
|
|
77
|
+
if (currentArray !== null && /^\s*- /.test(line)) {
|
|
78
|
+
const item = line.trim().substring(2).trim();
|
|
79
|
+
if (item) {
|
|
80
|
+
currentArray.push(item.replace(/^["']|["']$/g, ''));
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Detect metadata sub-keys (indented with spaces under "metadata:")
|
|
85
|
+
if (currentMetadata !== null && /^\s+\S/.test(line)) {
|
|
86
|
+
const colonIndex = line.indexOf(':');
|
|
87
|
+
if (colonIndex !== -1) {
|
|
88
|
+
const key = line.substring(0, colonIndex).trim();
|
|
89
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
90
|
+
if (key && value) {
|
|
91
|
+
currentMetadata[key] = value.replace(/^["']|["']$/g, '');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Top-level keys
|
|
97
|
+
currentMetadata = null;
|
|
98
|
+
currentArray = null;
|
|
99
|
+
currentArrayKey = null;
|
|
100
|
+
const colonIndex = line.indexOf(':');
|
|
101
|
+
if (colonIndex === -1)
|
|
102
|
+
continue;
|
|
103
|
+
const key = line.substring(0, colonIndex).trim();
|
|
104
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
105
|
+
if (key === 'metadata' && !value) {
|
|
106
|
+
// Start collecting metadata sub-keys
|
|
107
|
+
currentMetadata = {};
|
|
108
|
+
fields['__metadata__'] = 'MAP';
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Check for array start (value is empty, array items follow on next lines)
|
|
112
|
+
if (key === 'allowed-tools' && !value) {
|
|
113
|
+
currentArray = [];
|
|
114
|
+
currentArrayKey = 'allowed-tools';
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (key && value) {
|
|
118
|
+
// Strip surrounding quotes (YAML style)
|
|
119
|
+
fields[key] = value.replace(/^["']|["']$/g, '');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Store any pending array
|
|
123
|
+
if (currentArray !== null && currentArrayKey) {
|
|
124
|
+
fields[currentArrayKey] = currentArray;
|
|
125
|
+
}
|
|
126
|
+
// Build frontmatter object
|
|
127
|
+
const frontmatter = {
|
|
128
|
+
name: fields.name || '',
|
|
129
|
+
description: fields.description || '',
|
|
130
|
+
};
|
|
131
|
+
if (fields.license)
|
|
132
|
+
frontmatter.license = fields.license;
|
|
133
|
+
if (fields.compatibility)
|
|
134
|
+
frontmatter.compatibility = fields.compatibility;
|
|
135
|
+
if (fields['allowed-tools']) {
|
|
136
|
+
frontmatter['allowed-tools'] = Array.isArray(fields['allowed-tools'])
|
|
137
|
+
? fields['allowed-tools']
|
|
138
|
+
: fields['allowed-tools'];
|
|
139
|
+
}
|
|
140
|
+
if (currentMetadata && Object.keys(currentMetadata).length > 0) {
|
|
141
|
+
frontmatter.metadata = currentMetadata;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
parsed: { frontmatter, body, raw: content },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Validate parsed frontmatter against the Agent Skills spec.
|
|
150
|
+
*
|
|
151
|
+
* Checks: name rules, description rules, optional field constraints,
|
|
152
|
+
* and name/directory consistency (if directoryName provided).
|
|
153
|
+
*/
|
|
154
|
+
export function validateFrontmatter(frontmatter, directoryName) {
|
|
155
|
+
const issues = [];
|
|
156
|
+
// Required: name
|
|
157
|
+
if (!frontmatter.name) {
|
|
158
|
+
issues.push({ field: 'name', message: 'YAML frontmatter must include a "name" field', severity: 'error' });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const nameResult = validateSkillName(frontmatter.name);
|
|
162
|
+
if (!nameResult.valid) {
|
|
163
|
+
issues.push({ field: 'name', message: nameResult.error, severity: 'error' });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Required: description
|
|
167
|
+
if (!frontmatter.description) {
|
|
168
|
+
issues.push({ field: 'description', message: 'YAML frontmatter must include a "description" field', severity: 'error' });
|
|
169
|
+
}
|
|
170
|
+
else if (frontmatter.description.length > MAX_DESCRIPTION_LENGTH) {
|
|
171
|
+
issues.push({
|
|
172
|
+
field: 'description',
|
|
173
|
+
message: `Description must be at most ${MAX_DESCRIPTION_LENGTH} characters (got ${frontmatter.description.length})`,
|
|
174
|
+
severity: 'error',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// Optional: compatibility
|
|
178
|
+
if (frontmatter.compatibility && frontmatter.compatibility.length > MAX_COMPATIBILITY_LENGTH) {
|
|
179
|
+
issues.push({
|
|
180
|
+
field: 'compatibility',
|
|
181
|
+
message: `Compatibility must be at most ${MAX_COMPATIBILITY_LENGTH} characters (got ${frontmatter.compatibility.length})`,
|
|
182
|
+
severity: 'error',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// name must match directory name (if provided)
|
|
186
|
+
if (directoryName && frontmatter.name && frontmatter.name !== directoryName) {
|
|
187
|
+
issues.push({
|
|
188
|
+
field: 'name',
|
|
189
|
+
message: `Skill name "${frontmatter.name}" must match its directory name "${directoryName}"`,
|
|
190
|
+
severity: 'error',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
valid: issues.filter(i => i.severity === 'error').length === 0,
|
|
195
|
+
issues,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* List bundled resources in a skill directory.
|
|
200
|
+
*
|
|
201
|
+
* The spec recognizes three standard subdirectories:
|
|
202
|
+
* - scripts/ — executable code
|
|
203
|
+
* - references/ — additional documentation
|
|
204
|
+
* - assets/ — templates, resources
|
|
205
|
+
*
|
|
206
|
+
* Any other files (besides SKILL.md) are listed under "other".
|
|
207
|
+
*/
|
|
208
|
+
export function listBundledResources(skillDir) {
|
|
209
|
+
const resources = {
|
|
210
|
+
scripts: [],
|
|
211
|
+
references: [],
|
|
212
|
+
assets: [],
|
|
213
|
+
other: [],
|
|
214
|
+
};
|
|
215
|
+
if (!fs.existsSync(skillDir))
|
|
216
|
+
return resources;
|
|
217
|
+
const KNOWN_DIRS = {
|
|
218
|
+
scripts: 'scripts',
|
|
219
|
+
references: 'references',
|
|
220
|
+
assets: 'assets',
|
|
221
|
+
};
|
|
222
|
+
const entries = fs.readdirSync(skillDir, { withFileTypes: true });
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (entry.name === 'SKILL.md')
|
|
225
|
+
continue;
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
const category = KNOWN_DIRS[entry.name];
|
|
228
|
+
if (category) {
|
|
229
|
+
// List files inside the known subdirectory
|
|
230
|
+
const subDir = path.join(skillDir, entry.name);
|
|
231
|
+
const subEntries = fs.readdirSync(subDir);
|
|
232
|
+
for (const sub of subEntries) {
|
|
233
|
+
resources[category].push(`${entry.name}/${sub}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Unknown directory — list contents under "other"
|
|
238
|
+
const subDir = path.join(skillDir, entry.name);
|
|
239
|
+
const subEntries = fs.readdirSync(subDir);
|
|
240
|
+
for (const sub of subEntries) {
|
|
241
|
+
resources.other.push(`${entry.name}/${sub}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Top-level file (not SKILL.md)
|
|
247
|
+
resources.other.push(entry.name);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return resources;
|
|
251
|
+
}
|
package/tools/web/web.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Web Tool - Fetch web content from URLs with flexible HTTP methods
|
|
4
|
+
* Function-type tool: Exports default async function
|
|
5
|
+
*/
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
8
|
+
/**
|
|
9
|
+
* Fetch URL and return response with optional JSON parsing
|
|
10
|
+
*/
|
|
11
|
+
export default async function webTool(params) {
|
|
12
|
+
const { url, method = 'GET', headers, body, timeout = 30000, followRedirects = true, parseJson = true, maxSize = 10485760, } = params;
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
// Validate required parameter
|
|
15
|
+
if (!url) {
|
|
16
|
+
throw new MatimoError('Missing required parameter', ErrorCode.INVALID_PARAMETER, {
|
|
17
|
+
reason: 'url is required',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Validate URL
|
|
21
|
+
try {
|
|
22
|
+
new URL(url);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new MatimoError('Invalid URL', ErrorCode.INVALID_PARAMETER, {
|
|
26
|
+
url,
|
|
27
|
+
reason: 'URL must be valid http or https',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const requestConfig = {
|
|
31
|
+
method: method.toUpperCase() || 'GET',
|
|
32
|
+
url,
|
|
33
|
+
timeout,
|
|
34
|
+
maxContentLength: maxSize,
|
|
35
|
+
maxBodyLength: maxSize,
|
|
36
|
+
headers: {
|
|
37
|
+
'User-Agent': 'Matimo/1.0 (AI Agent Tool SDK)',
|
|
38
|
+
Accept: 'application/json, text/plain, text/html, */*',
|
|
39
|
+
...(headers || {}),
|
|
40
|
+
},
|
|
41
|
+
validateStatus: () => true, // Don't throw on any status code
|
|
42
|
+
};
|
|
43
|
+
if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
|
|
44
|
+
requestConfig.data = body;
|
|
45
|
+
}
|
|
46
|
+
if (!followRedirects) {
|
|
47
|
+
requestConfig.maxRedirects = 0;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const response = await axios.request(requestConfig);
|
|
51
|
+
let content = response.data;
|
|
52
|
+
// Try to parse JSON if requested and content-type suggests JSON
|
|
53
|
+
const contentType = response.headers['content-type'];
|
|
54
|
+
if (parseJson && typeof contentType === 'string' && contentType.includes('application/json')) {
|
|
55
|
+
try {
|
|
56
|
+
if (typeof response.data === 'string') {
|
|
57
|
+
content = JSON.parse(response.data);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
content = response.data;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// If JSON parsing fails, keep original content
|
|
65
|
+
content = response.data;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const result = {
|
|
69
|
+
success: response.status >= 200 && response.status < 300,
|
|
70
|
+
url,
|
|
71
|
+
statusCode: response.status,
|
|
72
|
+
statusText: response.statusText || '',
|
|
73
|
+
contentType: response.headers['content-type'] || 'unknown',
|
|
74
|
+
content,
|
|
75
|
+
headers: response.headers,
|
|
76
|
+
size: JSON.stringify(response.data).length,
|
|
77
|
+
duration: Date.now() - startTime,
|
|
78
|
+
};
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const axiosError = error;
|
|
83
|
+
throw new MatimoError('HTTP request failed', ErrorCode.NETWORK_ERROR, {
|
|
84
|
+
url,
|
|
85
|
+
statusCode: axiosError.response?.status,
|
|
86
|
+
statusText: axiosError.response?.statusText,
|
|
87
|
+
originalError: axiosError.message,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
package/tools/web/web.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
|
|
8
|
-
import { MatimoError, ErrorCode } from '
|
|
8
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
9
9
|
|
|
10
10
|
interface WebParams {
|
|
11
11
|
url: string;
|
|
@@ -74,7 +74,7 @@ export default async function webTool(params: WebParams): Promise<WebResult> {
|
|
|
74
74
|
headers: {
|
|
75
75
|
'User-Agent': 'Matimo/1.0 (AI Agent Tool SDK)',
|
|
76
76
|
Accept: 'application/json, text/plain, text/html, */*',
|
|
77
|
-
...headers,
|
|
77
|
+
...(headers || {}),
|
|
78
78
|
},
|
|
79
79
|
validateStatus: () => true, // Don't throw on any status code
|
|
80
80
|
};
|
|
@@ -92,7 +92,8 @@ export default async function webTool(params: WebParams): Promise<WebResult> {
|
|
|
92
92
|
let content: unknown = response.data;
|
|
93
93
|
|
|
94
94
|
// Try to parse JSON if requested and content-type suggests JSON
|
|
95
|
-
|
|
95
|
+
const contentType = response.headers['content-type'];
|
|
96
|
+
if (parseJson && typeof contentType === 'string' && contentType.includes('application/json')) {
|
|
96
97
|
try {
|
|
97
98
|
if (typeof response.data === 'string') {
|
|
98
99
|
content = JSON.parse(response.data);
|