Skip to content

feat(skills): add .well-known/skills module#31

Merged
amondnet merged 2 commits into
mainfrom
worktree-agent-a22afa164f06825bf
May 28, 2026
Merged

feat(skills): add .well-known/skills module#31
amondnet merged 2 commits into
mainfrom
worktree-agent-a22afa164f06825bf

Conversation

@amondnet
Copy link
Copy Markdown
Contributor

@amondnet amondnet commented May 28, 2026

Summary

Ports the agent skills discovery module from upstream docus (survey item #7 in docs/docus-upstream-changes.md). Adds a Nitro module that scans <rootDir>/skills/ for Anthropic Agent Skills and exposes them under .well-known/skills/.

Continues the porting work started in #27.

Upstream commits

  • 2f7861bd feat(skills): add agent skills discovery via .well-known
  • 764329f5 feat(skills): make directory configurable via module options

What's new

packages/layer/modules/skills/index.ts

A defineNuxtModule with options { dir?: string } (default 'skills', configurable via skills.dir). At setup time the module:

  1. Resolves <rootDir>/<dir> and bails out if it doesn't exist.
  2. Scans every subdirectory for a SKILL.md file with YAML frontmatter { name, description }.
  3. Validates names against the Anthropic Agent Skills spec — /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, max 64 chars, no --, must match the directory name. Invalid entries are skipped with a warning.
  4. Registers the skills directory as a Nitro serverAsset (assets:skills) so files are accessible at runtime via storage even when the source tree is unavailable (serverless, etc.).
  5. Registers every skill file path in nitro.prerender.routes for static builds.
  6. Adds two server handlers via addServerHandler.

runtime/server/routes/.well-known/skills/index.get.ts

Scans the asset storage for top-level SKILL.md files, re-parses frontmatter, and returns:

{
  "skills": {
    "catalog": [
      { "name": "...", "description": "...", "path": "/.well-known/skills/.../SKILL.md" }
    ]
  }
}

Sets Cache-Control: public, max-age=3600.

runtime/server/routes/.well-known/skills/[...path].get.ts

Serves any file under a skill folder with content-type detection (.mdtext/markdown, .jsonapplication/json, .yaml/.ymlapplication/yaml, code files → text/plain, etc.). Blocks path traversal (.., .) and dotfile access. 404s when the file doesn't exist. Cache 1h.

Both routes use getItemRaw to avoid destr auto-parsing JSON payloads.

Configuration changes

  • packages/layer/nuxt.config.ts — registers the new module after ./modules/shadcn.
  • packages/layer/package.json — adds yaml as a direct dependency (matches docus' approach and keeps the module independent of @nuxt/content).

Verification

Local e2e against apps/docs:

mkdir -p apps/docs/skills/test-skill
cat > apps/docs/skills/test-skill/SKILL.md <<'SKILL'
---
name: test-skill
description: hello
---
Sample skill body.
SKILL
bun dev  # in one shell
curl -s http://localhost:3000/.well-known/skills/index.json | jq .
curl -s http://localhost:3000/.well-known/skills/test-skill/SKILL.md

Both endpoints return the expected payloads, including correct content-type headers and 404 for unknown paths. Tested .json files round-trip correctly (initial draft used getItem which would have JSON-parsed them; switched to getItemRaw).

  • bun lint — clean.
  • cd packages/layer && bun typecheck — no new errors (pre-existing baseline errors unchanged).

The sample skill was removed before committing.

References


Summary by cubic

Adds a Nuxt module that discovers Anthropic Agent Skills in skills/ and exposes them under /.well-known/skills for discovery and static builds. The index is served from a prebuilt catalog for faster responses.

  • New Features

    • Scans <rootDir>/<dir> (default skills) for SKILL.md with YAML frontmatter; requires name and description (200-char cap), validates names per the Anthropic spec, and skips invalid entries with warnings.
    • Builds a catalog at startup and serves /.well-known/skills/index.json from runtime config with { name, description, path } (cached 1h).
    • /.well-known/skills/<name>/<file> serves any skill file with content-type detection, traversal/dotfile protection, and binary-safe responses (cached 1h).
    • Registers skills as Nitro serverAssets and prerenders all skill file routes for static builds.
    • Configurable via skills.dir in nuxt.config.
  • Dependencies

    • Added yaml.

Written for commit 207f398. Summary will update on new commits.

Port the agent skills discovery module from upstream docus, exposing
Anthropic Agent Skills under `.well-known/skills/`:

- `packages/layer/modules/skills/index.ts` — scans `<rootDir>/<dir>/`
  for skill folders, validates against the Anthropic naming spec
  (`^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`, max 64 chars), and registers
  the directory as Nitro `serverAssets` so files are served from
  runtime storage even on serverless deployments. Configurable via the
  `skills.dir` module option (default `skills`).
- `runtime/server/routes/.well-known/skills/index.get.ts` — scans the
  asset storage for `SKILL.md` files, parses YAML frontmatter, and
  returns `{ skills: { catalog: [{ name, description, path }] } }`.
- `runtime/server/routes/.well-known/skills/[...path].get.ts` — serves
  any file under a skill folder with content-type detection. Blocks
  path traversal and dotfile access.

Every skill file path is registered in `nitro.prerender.routes` so
static builds materialise them up-front.

Ported from upstream docus:
- 2f7861bd feat(skills): add agent skills discovery via .well-known
- 764329f5 feat(skills): make directory configurable via module options

See docs/docus-upstream-changes.md item #7.
@amondnet amondnet force-pushed the worktree-agent-a22afa164f06825bf branch from cac65cf to 34ce435 Compare May 28, 2026 15:11
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 28, 2026

Deploying docs-please with  Cloudflare Pages  Cloudflare Pages

Latest commit: 207f398
Status:⚡️  Build in progress...

View logs

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new skills module to expose Anthropic Agent Skills via .well-known/skills by scanning directories, parsing YAML frontmatter, and registering routes. The review feedback highlights two critical issues: first, the server handler file [...path].get.ts registered in the module is missing from the changes, which will cause runtime errors; second, the index.get.ts handler redundantly re-scans and re-parses files on every request instead of utilizing the pre-built catalog from the runtime configuration.

Comment thread packages/layer/modules/skills/index.ts
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/layer/modules/skills/runtime/server/routes/.well-known/skills/[...path].get.ts">

<violation number="1" location="packages/layer/modules/skills/runtime/server/routes/.well-known/skills/[...path].get.ts:39">
P2: Guard the decode. A malformed `%` sequence here can throw a `URIError` and turn a bad request into a 500 instead of a clean 400/404.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Build as Nuxt Build
    participant Module as Skills Module
    participant FS as File System (skills/)
    participant AssetStore as Nitro Asset Store (assets:skills)
    participant Runtime as Nuxt Runtime
    participant Client as HTTP Client

    Note over Build,Client: Build-Time: Skill Discovery & Registration

    Build->>Module: setup() with options { dir: 'skills' }
    Module->>FS: Scan subdirectories for SKILL.md
    FS-->>Module: List of skill directories

    loop For each skill subdirectory
        Module->>FS: Read SKILL.md
        FS-->>Module: Raw content with YAML frontmatter
        Module->>Module: Parse frontmatter (name, description)
        alt Invalid name (spec violation, max 64 chars, dir mismatch)
            Module->>Module: Log warning, skip skill
        else Valid skill
            Module->>FS: Recursively list skill files
            FS-->>Module: File list (filtered, dotfiles excluded)
            Module->>Module: Build catalog entry { name, description, files }
        end
    end

    alt No skills directory or empty catalog
        Module->>Build: Return early, no routes registered
    else Skills found
        Module->>Build: Set runtimeConfig.skills.catalog
        Module->>Build: Register serverAsset 'skills' -> skillsDir
        Module->>Build: Add prerender routes (index.json + all skill files)

        Module->>Build: addServerHandler('/.well-known/skills/index.json')
        Module->>Build: addServerHandler('/.well-known/skills/**')
        Build-->>Module: Handlers registered
    end

    Note over Build,Client: Runtime: Request Handling

    Client->>Runtime: GET /.well-known/skills/index.json
    Runtime->>AssetStore: getKeys() via assets:skills
    AssetStore-->>Runtime: All storage keys (skill:SKILL.md pairs)
    Runtime->>Runtime: Filter top-level SKILL.md keys
    loop For each top-level skill
        Runtime->>AssetStore: getItemRaw('skill:SKILL.md')
        AssetStore-->>Runtime: Raw file content
        Runtime->>Runtime: Parse YAML frontmatter, validate name
        alt Invalid (missing description, bad name)
            Runtime->>Runtime: Log warning, skip entry
        else Valid
            Runtime->>Runtime: Add to catalog { name, description, path }
        end
    end
    Runtime-->>Client: 200 JSON { skills: { catalog: [...] } }
    Note over Runtime: Cache-Control: public, max-age=3600

    Client->>Runtime: GET /.well-known/skills/<name>/<file>
    Runtime->>Runtime: Extract path from URL
    alt Path traversal detected (.., ., dotfiles) or empty/index.json
        Runtime-->>Client: 400 Bad Request
    else Valid path
        Runtime->>AssetStore: getItemRaw('<name>:<file>')
        alt File not found (null/undefined)
            AssetStore-->>Runtime: null
            Runtime-->>Client: 404 Not Found
        else File found
            AssetStore-->>Runtime: Raw bytes or string
            Runtime->>Runtime: Convert to string, detect content-type
            Runtime-->>Client: 200 with file content + content-type header
            Note over Runtime: Cache-Control: public, max-age=3600
        end
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/layer/modules/skills/index.ts Outdated
Comment thread packages/layer/modules/skills/index.ts Outdated
@amondnet amondnet self-assigned this May 28, 2026
- index route now reads the pre-built catalog from runtimeConfig
  instead of re-scanning storage and re-parsing YAML on every request
  (per gemini-code-assist review).
- Require `name` in SKILL.md frontmatter rather than silently falling
  back to the folder name (per cubic-dev-ai review).
- Enforce the 200-character description cap from the Agent Skills
  spec before adding a skill to the catalog (per cubic-dev-ai review).
- File handler now returns raw bytes for non-string payloads so binary
  skill files (e.g. images, archives) are served untouched instead of
  being mangled by UTF-8 decoding (per cubic-dev-ai review).
@sonarqubecloud
Copy link
Copy Markdown

@amondnet amondnet merged commit 903dcfc into main May 28, 2026
2 of 3 checks passed
@amondnet amondnet deleted the worktree-agent-a22afa164f06825bf branch May 28, 2026 15:56
amondnet added a commit that referenced this pull request May 28, 2026
Resolve modules-array conflict in packages/layer/nuxt.config.ts by
keeping both new module registrations from #29/#30/#31 and this PR's
markdown-rewrite module.
amondnet added a commit that referenced this pull request May 28, 2026
* feat(layer): add Vercel markdown-rewrite module

Adds a Nuxt module that injects Vercel build-output rewrite rules so
that AI agents (anything sending `Accept: text/markdown` or
`User-Agent: curl/*`) get served raw markdown instead of the SPA shell.

Behaviour:
- No-op on every non-Vercel preset (matches `vercel`, `vercel-edge`,
  `vercel-static`, etc. via `preset.startsWith('vercel')`).
- On Vercel: read `<output.publicDir>/../config.json`, validate that
  `llms.txt` was emitted, and unshift route pairs onto `routes` so
  they fire before the SPA fallback.
- Rules:
  - `/`         -> `/llms.txt`
  - `/<locale>` -> `/llms.txt`   (per `runtimeConfig.public.i18n.locales`)
  - `/<page>`   -> `/raw<page>.md` for every `/raw/...md` link found
    in `llms.txt`
- Vercel's `has` array is AND-ed, so OR semantics between the
  `Accept` and `User-Agent` matchers require emitting two rules per
  `src` -> `dest` pair.
- Locale codes are regex-escaped before being joined into the
  alternation so exotic codes can't break the pattern.

Ports upstream docus commits `6fd8686b` and `9ceafe6f` -- see
`docs/docus-upstream-changes.md` item #9.

Verification:
- `bun lint`        -> clean
- `bun typecheck`   -> no new errors in `markdown-rewrite.ts`
- `NITRO_PRESET=vercel bun --filter @pleaseai/docs-site build`
  injects two routes (homepage / Accept + User-Agent) into
  `.vercel/output/config.json`.
- Default (cloudflare) build is unchanged; module bails silently.

* fix(layer): apply review suggestions

Apply review feedback from cubic-dev-ai and gemini-code-assist on
the markdown-rewrite module:

- Read locale config from `nuxt.options.i18n` instead of
  `runtimeConfig.public.i18n` (matching the pattern used by the
  `nitro:config` hook in nuxt.config.ts). The i18n module does
  not always populate `runtimeConfig.public.i18n`, so the previous
  source could silently miss locale routes. (cubic)
- Decode the URL pathname with `decodeURIComponent` so that paths
  with URL-encoded characters (e.g. `%20`) match Vercel's router,
  which compares against decoded request pathnames. (gemini)
- Allow an optional trailing slash on docs page route patterns
  (`/?$` instead of `$`) so requests like
  `/en/getting-started/installation/` are matched consistently
  with the per-locale homepage routes. (cubic, gemini)

* Merge branch 'main' into worktree-agent-a1a3a89e87faf9e6d

Resolve modules-array conflict in packages/layer/nuxt.config.ts by
keeping both new module registrations from #29/#30/#31 and this PR's
markdown-rewrite module.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant