From a718299b73880a2cdf8cef1a2152a9ea3831308b Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Wed, 4 Feb 2026 19:20:33 +0100 Subject: [PATCH] feat: add well-known skills discovery endpoint --- playwriter/scripts/build-resources.ts | 78 +++++++++++++++++++ website/public/.well-known/skills/index.json | 11 +++ .../.well-known/skills/playwriter/SKILL.md | 30 +++++++ 3 files changed, 119 insertions(+) create mode 100644 website/public/.well-known/skills/index.json create mode 100644 website/public/.well-known/skills/playwriter/SKILL.md diff --git a/playwriter/scripts/build-resources.ts b/playwriter/scripts/build-resources.ts index 0eaa31d..edad936 100644 --- a/playwriter/scripts/build-resources.ts +++ b/playwriter/scripts/build-resources.ts @@ -7,10 +7,13 @@ * * Source of truth: * - playwriter/src/skill.md - manually edited, contains full docs including CLI usage + * - skills/playwriter/SKILL.md - stub with frontmatter for agent discovery * * Generated files: * - playwriter/dist/prompt.md - MCP prompt (skill.md minus CLI sections) * - website/public/SKILL.md - full copy for playwriter.dev/SKILL.md + * - website/public/.well-known/skills/index.json - Agent Skills Discovery endpoint + * - website/public/.well-known/skills/playwriter/SKILL.md - skill file with frontmatter */ import fs from 'node:fs' @@ -181,10 +184,85 @@ function buildPromptFromSkill() { console.log('Generated website/public/SKILL.md') } +/** + * Parses YAML frontmatter from a markdown file. + * Returns { frontmatter, body } where frontmatter is the parsed YAML object. + */ +function parseFrontmatter(content: string): { frontmatter: Record; body: string } { + const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/) + if (!match) { + return { frontmatter: {}, body: content } + } + + const yamlContent = match[1] + const body = match[2] + + // Simple YAML parsing for key: value pairs + const frontmatter: Record = {} + for (const line of yamlContent.split('\n')) { + const colonIndex = line.indexOf(':') + if (colonIndex > 0) { + const key = line.slice(0, colonIndex).trim() + const value = line.slice(colonIndex + 1).trim() + frontmatter[key] = value + } + } + + return { frontmatter, body } +} + +/** + * Builds the Well-Known Skills Discovery structure. + * + * Creates: + * - /.well-known/skills/index.json - discovery endpoint + * - /.well-known/skills/playwriter/SKILL.md - skill file + * + * See: https://agentskills.io/specification + */ +function buildWellKnownSkills() { + const repoRoot = path.join(playwriterDir, '..') + const skillSourcePath = path.join(repoRoot, 'skills', 'playwriter', 'SKILL.md') + const websitePublicRoot = path.join(repoRoot, 'website', 'public') + const wellKnownDir = path.join(websitePublicRoot, '.well-known', 'skills') + const playwriterSkillDir = path.join(wellKnownDir, 'playwriter') + + // Read and parse the skill file + const skillContent = fs.readFileSync(skillSourcePath, 'utf-8') + const { frontmatter } = parseFrontmatter(skillContent) + + // Ensure directories exist + ensureDir(wellKnownDir) + ensureDir(playwriterSkillDir) + + // Copy SKILL.md to well-known location + fs.writeFileSync(path.join(playwriterSkillDir, 'SKILL.md'), skillContent, 'utf-8') + console.log('Generated website/public/.well-known/skills/playwriter/SKILL.md') + + // Generate index.json + const indexJson = { + skills: [ + { + name: frontmatter.name || 'playwriter', + description: frontmatter.description || '', + files: ['SKILL.md'] + } + ] + } + + fs.writeFileSync( + path.join(wellKnownDir, 'index.json'), + JSON.stringify(indexJson, null, 2) + '\n', + 'utf-8' + ) + console.log('Generated website/public/.well-known/skills/index.json') +} + // Run all builds buildDebuggerApi() buildEditorApi() buildStylesApi() buildPromptFromSkill() +buildWellKnownSkills() console.log('Resource files generated successfully') diff --git a/website/public/.well-known/skills/index.json b/website/public/.well-known/skills/index.json new file mode 100644 index 0000000..21ea45a --- /dev/null +++ b/website/public/.well-known/skills/index.json @@ -0,0 +1,11 @@ +{ + "skills": [ + { + "name": "playwriter", + "description": "Control the user own Chrome browser via Playwriter extension with Playwright code snippets in a stateful local js sandbox via playwriter cli. Automate web interactions, take screenshots, inspect accessibility trees, debug & profile web applications. Run `playwriter skill` command to read the complete up to date skill", + "files": [ + "SKILL.md" + ] + } + ] +} diff --git a/website/public/.well-known/skills/playwriter/SKILL.md b/website/public/.well-known/skills/playwriter/SKILL.md new file mode 100644 index 0000000..0cfdbb7 --- /dev/null +++ b/website/public/.well-known/skills/playwriter/SKILL.md @@ -0,0 +1,30 @@ +--- +name: playwriter +description: Control the user own Chrome browser via Playwriter extension with Playwright code snippets in a stateful local js sandbox via playwriter cli. Automate web interactions, take screenshots, inspect accessibility trees, debug & profile web applications. Run `playwriter skill` command to read the complete up to date skill +--- + +## REQUIRED: Read Full Documentation First + +**Before using playwriter, you MUST run this command:** + +```bash +playwriter skill +``` + +This outputs the complete documentation including: +- Session management and timeout configuration +- Selector strategies (and which ones to AVOID) +- Rules to prevent timeouts and failures +- Best practices for slow pages and SPAs +- Context variables, utility functions, and more + +**Do NOT skip this step.** The quick examples below will fail without understanding timeouts, selector rules, and common pitfalls from the full docs. + +## Minimal Example (after reading full docs) + +```bash +playwriter session new +playwriter -s 1 -e "await page.goto('https://example.com')" +``` + +If `playwriter` is not found, use `npx playwriter@latest` or `bunx playwriter@latest`.