feat: add SkillTools — list_skills, get_skill, get_page_context (208 total tools, 5 skills)
This commit is contained in:
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
namespace AskTamMCP\Tools;
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skill Tools — read .md skill files from the plugin's skills/ directory.
|
||||||
|
*
|
||||||
|
* Skills are markdown files that contain design, copywriting, and build instructions
|
||||||
|
* for Claude to follow when constructing landing pages, Elementor layouts, etc.
|
||||||
|
*
|
||||||
|
* Tools:
|
||||||
|
* list_skills — list all available skills with metadata
|
||||||
|
* get_skill — read a single skill file by name
|
||||||
|
* get_page_context — read all auto-load skills in one call
|
||||||
|
*
|
||||||
|
* To add a skill: drop a .md file in skills/ with YAML front matter. No code changes needed.
|
||||||
|
*/
|
||||||
|
class SkillTools {
|
||||||
|
|
||||||
|
private static string $skills_dir = '';
|
||||||
|
|
||||||
|
private static function dir(): string {
|
||||||
|
if (!self::$skills_dir) {
|
||||||
|
self::$skills_dir = plugin_dir_path(dirname(__DIR__)) . 'skills/';
|
||||||
|
}
|
||||||
|
return self::$skills_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function listSkills(array $args): array {
|
||||||
|
$dir = self::dir();
|
||||||
|
$tag = sanitize_key($args['tag'] ?? '');
|
||||||
|
$files = glob($dir . '*.md') ?: [];
|
||||||
|
|
||||||
|
$skills = [];
|
||||||
|
foreach ($files as $filepath) {
|
||||||
|
$meta = self::parseFrontMatter($filepath);
|
||||||
|
if (!$meta) continue;
|
||||||
|
if ($tag) {
|
||||||
|
$tags = $meta['tags'] ?? [];
|
||||||
|
if (!in_array($tag, (array)$tags)) continue;
|
||||||
|
}
|
||||||
|
$skills[] = [
|
||||||
|
'name' => $meta['name'] ?? basename($filepath, '.md'),
|
||||||
|
'description' => $meta['description'] ?? '',
|
||||||
|
'version' => $meta['version'] ?? '1.0.0',
|
||||||
|
'auto_load' => (bool)($meta['auto-load'] ?? false),
|
||||||
|
'tags' => (array)($meta['tags'] ?? []),
|
||||||
|
'filename' => basename($filepath),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
usort($skills, fn($a, $b) => $b['auto_load'] <=> $a['auto_load']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => count($skills),
|
||||||
|
'skills' => $skills,
|
||||||
|
'skills_dir' => str_replace(ABSPATH, '/', $dir),
|
||||||
|
'usage' => 'Call get_skill(name) to read any skill. Call get_page_context() to load all auto-load skills at once before building a landing page.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSkill(array $args): array {
|
||||||
|
$name = sanitize_file_name($args['name'] ?? '');
|
||||||
|
if (empty($name)) throw new \RuntimeException('name is required.');
|
||||||
|
$name = preg_replace('/\.md$/i', '', $name);
|
||||||
|
$filepath = self::dir() . $name . '.md';
|
||||||
|
|
||||||
|
$real = realpath($filepath);
|
||||||
|
$dir = realpath(self::dir());
|
||||||
|
if (!$real || !str_starts_with($real, $dir)) {
|
||||||
|
throw new \RuntimeException("Skill '{$name}' not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($real);
|
||||||
|
$meta = self::parseFrontMatterFromContent($raw);
|
||||||
|
$body = self::stripFrontMatter($raw);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $meta['name'] ?? $name,
|
||||||
|
'description' => $meta['description'] ?? '',
|
||||||
|
'version' => $meta['version'] ?? '1.0.0',
|
||||||
|
'auto_load' => (bool)($meta['auto-load'] ?? false),
|
||||||
|
'tags' => (array)($meta['tags'] ?? []),
|
||||||
|
'content' => $body,
|
||||||
|
'char_count' => strlen($body),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPageContext(array $args): array {
|
||||||
|
$dir = self::dir();
|
||||||
|
$files = glob($dir . '*.md') ?: [];
|
||||||
|
$loaded = [];
|
||||||
|
$skipped = [];
|
||||||
|
|
||||||
|
foreach ($files as $filepath) {
|
||||||
|
$raw = file_get_contents($filepath);
|
||||||
|
$meta = self::parseFrontMatterFromContent($raw);
|
||||||
|
if (!(bool)($meta['auto-load'] ?? false)) {
|
||||||
|
$skipped[] = $meta['name'] ?? basename($filepath, '.md');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$loaded[] = ['name' => $meta['name'] ?? basename($filepath, '.md'), 'content' => self::stripFrontMatter($raw)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$combined = '';
|
||||||
|
foreach ($loaded as $skill) {
|
||||||
|
$combined .= "\n\n" . str_repeat('=', 60) . "\n";
|
||||||
|
$combined .= "SKILL: {$skill['name']}\n";
|
||||||
|
$combined .= str_repeat('=', 60) . "\n\n";
|
||||||
|
$combined .= $skill['content'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'loaded_skills' => array_column($loaded, 'name'),
|
||||||
|
'skipped_skills' => $skipped,
|
||||||
|
'total_loaded' => count($loaded),
|
||||||
|
'context' => trim($combined),
|
||||||
|
'char_count' => strlen($combined),
|
||||||
|
'usage_note' => 'This context contains your landing page build instructions. Follow them for every page you build. For motion/CSS patterns call get_skill("motion-and-css") separately.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function parseFrontMatter(string $filepath): ?array {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
if (!$content) return null;
|
||||||
|
return self::parseFrontMatterFromContent($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function parseFrontMatterFromContent(string $content): array {
|
||||||
|
if (!preg_match('/^---\s*\n(.*?)\n---/s', $content, $m)) return [];
|
||||||
|
$meta = [];
|
||||||
|
$lines = explode("\n", trim($m[1]));
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (!preg_match('/^([a-zA-Z0-9_\-]+)\s*:\s*(.*)$/', $line, $lm)) continue;
|
||||||
|
$key = trim($lm[1]);
|
||||||
|
$val = trim($lm[2]);
|
||||||
|
$val = preg_replace('/^["\'](.*)["\']$/', '$1', $val);
|
||||||
|
if (strtolower($val) === 'true') $val = true;
|
||||||
|
elseif (strtolower($val) === 'false') $val = false;
|
||||||
|
elseif (is_numeric($val) && !str_starts_with($val, '0') && !str_contains($val, '.')) $val = (int)$val;
|
||||||
|
if (is_string($val) && str_starts_with($val, '[') && str_ends_with($val, ']')) {
|
||||||
|
$val = array_map(fn($v) => trim(trim($v), '"\''), explode(',', substr($val, 1, -1)));
|
||||||
|
}
|
||||||
|
$meta[$key] = $val;
|
||||||
|
}
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function stripFrontMatter(string $content): string {
|
||||||
|
return trim(preg_replace('/^---\s*\n.*?\n---\s*\n/s', '', $content));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user