A TypeScript-first skill framework for Markdown Agent Skills — author skills in Markdown, compile once, consume them as typed imports. Type-safe references, self-contained runtime, zero IDE plugins.
Language: English | 简体中文
- Overview
- Quick start
- Authoring skills
- Compiling & configuration
- Consuming skills
- Integrations
- Links & license
review-skill helps developers manage agent instructions like application assets: write reusable skills in Markdown, compile them once, and consume them from TypeScript through typed imports. Autocomplete, hover metadata, token stats, and build-time reference checking all come from the compiler — no IDE plugin.
You write Markdown review-skill compiles Your agent reads
skills/ ↓ .skill/runtime/
SKILL.md → token-optimized runtime system prompt
review/rules.md → typed imports review.content
One source of truth (.skill/) feeds three layers:
| Layer | Command / package | What you get |
|---|---|---|
| Compiler | npx review-skill |
skills/*.md → token-optimized .skill/ runtime; link references validated at build time; --init bootstraps a project with zero manual config |
| Runtime | @review-skill/skill |
Typed skill("/path") imports, hover metadata, token stats, self-contained merged content, typed inject() templates |
| Integration | @review-skill/vite |
Skills as first-class @skill/* virtual modules; the plugin compiles on demand |
Other tools inject into AGENTS.md or generate standalone agents. review-skill is for TypeScript developers who want their skills tracked like code dependencies — the editor experience is native markdown links, at every layer.
npx review-skill --init
npm install--init scaffolds skills/, a config file, the @review-skill/skill TypeScript path alias, npm scripts, and adds review-skill to your dependencies.
Skills can reference other skills with plain links and interpolate variables:
---
variables:
- name: focus
---
# React Code Review
You are an expert React reviewer. Focus on {{focus}}.
See [state rules](../react/rules/state.md) for state management.npx review-skillCompiled 6 files in 91ms
3 skills · self-contained typed runtime
import { skill, inject } from "@review-skill/skill";
const rules = skill("/react/rules/state.md");
const review = inject(rules.content, { focus: "state management" });skills/
|-- SKILL.md
|-- react/
| |-- SKILL.md
| `-- rules/
| |-- effects.md
| `-- state.md
`-- security/
|-- SKILL.md
`-- owasp.md
Source Markdown stays readable — comments, formatting, tables, internal notes. Compilation strips the noise (see Compiling & configuration) so the runtime is leaner.
Reference other skills with plain markdown links, resolved relative to the containing file:
Follow the security constraints in [security](../security/SKILL.md) before drafting.VS Code handles the editor side natively — path completion as you type, Ctrl+click jump (including code files), Ctrl+hover preview, rendered links in the preview. No extension, no settings, no plugin.
The compiler treats links as its reference contract:
- Build-time validation — a link whose target doesn't resolve to a known skill/resource is reported as a warning (
Unknown skill reference /path). - Compile-time merging — the runtime output recursively absorbs linked content, so each skill is self-contained with no URL noise left for the agent. Cycles absorb to the link label.
- External URLs (
https://), anchors,mailto:, and images () are never treated as references.
Dynamic routes: a link whose path has a placeholder segment (e.g.
[endings](../story/【xxx】/good.md),{id}) is treated as a runtime route — not validated or merged, but kept in.contentso the consumer resolves the placeholder at runtime.
Note: reference-link syntax (
[x][id]+[id]: url) is not yet scanned — use the inline[x](../path)form.
Skills can be templates. Declare a variables contract in frontmatter, use {{placeholders}} in prose:
---
variables:
- name: sceneTitle
- name: isFinale
required: false
---
# Scene Plan
You are writing {{sceneTitle}}. Finale: {{isFinale}}.requireddefaults totrue; setrequired: falsefor optional variables.- The compiler enforces the contract at build time: an undeclared
{{var}}→ error, a required variable never used → error, an optional one never used → warning, a malformed placeholder ({{a-b}}) → error, any{{var}}without a contract → error. - Consume with typed
inject()(see Consuming skills) — the compiler generates a per-skill interface so missing required keys fail at compile time.
npx review-skill # compile once
npx review-skill --watch # rebuild on change
npx review-skill --init # scaffold a new projectCompiling needs no config file — defaults are skills/ → .skill/. The output:
| File | Contents |
|---|---|
.skill/runtime/** |
Compiled Markdown per skill/resource |
.skill/metadata.json |
Titles, descriptions, token stats, variable contracts |
.skill/skill.ts |
Typed skill() declarations + per-skill variable interfaces |
Source files carry two audiences: tooling metadata (frontmatter, comments, formatting) and the instructions the agent should read. Compilation always drops the frontmatter block (variable contracts, titles, descriptions), and strip removes the rest of the tooling layer — the runtime is instructions only, never the surrounding explanation.
--- is contextual: at the very top of a file it delimits the frontmatter block (always dropped); mid-document it's a horizontal rule (the "---" strip token).
skill.config.js / skill.config.mjs controls what compilation strips. strip is a character-based token array: list the exact markdown syntax literals to remove (each TS-autocompleted); omit anything you want kept. Not configuring strip at all strips nothing — the original file is returned as-is:
import { defineConfig } from "review-skill";
export default defineConfig({
skillsDir: "skills",
outputDir: ".skill",
// "absorb" (default) merges linked content into a self-contained output;
// "keep" preserves links so the consumer can route them at runtime.
merge: "absorb",
// delete entries you want KEPT; omit strip entirely to return the original:
strip: [
"<!-- HTML -->", // HTML comments
"**bold**", // bold
"*italic*", // italic
"~~strikethrough~~",
"", // images
"> quote", // blockquotes
"---", // horizontal rules
"- item", // bullet markers
"\n\n", // blank-line collapse
],
});Available tokens (see STRIP_TOKENS): "<!-- HTML -->" HTML comments · "**bold**" bold · "*italic*" italic · "~~strikethrough~~" strikethrough · "" images · "> quote" blockquotes · "---" horizontal rules · "- item" bullet markers · "\n\n" blank-line collapse. An empty array strip: [] strips nothing.
The legacy object form (strip: { formatting: false }) still works but is deprecated — the compiler warns with the exact equivalent token array. Migration guide: docs/strip.md.
merge controls link handling: "absorb" (default) inlines linked content at compile time so each skill is self-contained; "keep" preserves the links in the runtime output so the consumer resolves them dynamically (e.g. picks an ending by game state). Use "keep" when you want runtime link routing.
After compilation, every skill/resource path is available with autocomplete — no hand-written relative paths:
import { skill } from "@review-skill/skill";
const root = skill("/");
const rules = skill("/react/rules/state.md");
console.log(rules.meta.title); // "React State Rules"
console.log(rules.meta.runtime.tokens); // runtime token count
const markdown = rules.content;Hover a skill() call to see the skill's title, description, source file, current character/token count, estimated compiled size, and percentage saved.
Compilation already merges linked content into .content, so each skill's runtime is self-contained — no link URLs to chase. bundle() stays as an idempotent API for safety.
For a skill with a variables contract, the compiler generates a per-skill interface (/galgame/section-plan → GalgameSectionplanVars) in .skill/skill.ts. Missing required keys fail at compile time:
import { skill, inject, type GalgameSectionplanVars } from "@review-skill/skill";
const scene = inject<GalgameSectionplanVars>(skill("/galgame/section-plan").content, {
sceneTitle: "Act 2",
// isFinale is optional — omit it
});
// omitting sceneTitle → TS error: property 'sceneTitle' is missingCompiled resources are plain Markdown strings, so they work in any prompt builder — as system prompts, developer instructions, tool rules, review policies, or RAG chunks.
import { ChatOpenAI } from "@langchain/openai";
import { skill } from "@review-skill/skill";
const rules = skill("/react/rules/state.md");
const llm = new ChatOpenAI({ model: "gpt-4o" });
const result = await llm.invoke([
{ role: "system", content: rules.content },
{ role: "user", content: `Review this code:\n\`\`\`tsx\n${userCode}\n\`\`\`` },
]);import { Agent } from "@mastra/core";
import { skill } from "@review-skill/skill";
const agent = new Agent({
name: skill("/react").meta.title,
instructions: skill("/react/rules/state.md").content,
model: "openai/gpt-4o",
});import { generateText } from "ai";
import { skill } from "@review-skill/skill";
const { text } = await generateText({
model: "openai/gpt-4o",
system: skill("/react/rules/state.md").content,
prompt: `Review this code:\n${code}`,
});import OpenAI from "openai";
import { skill } from "@review-skill/skill";
const client = new OpenAI();
const response = await client.responses.create({
model: "gpt-4.1",
input: [
{ role: "developer", content: skill("/react/rules/state.md").content },
{ role: "user", content: `Review this code:\n${code}` },
],
});import { skill } from "@review-skill/skill";
const guide = skill("/security/owasp.md");
agent.setSystemPrompt(guide.content);Skills become first-class modules: @skill/meta and @skill/<path>, auto-compiled by the plugin.
// vite.config.ts
import { skillFramework } from "@review-skill/vite";
export default defineConfig({ plugins: [skillFramework()] });import plan from "@skill/galgame/section-plan"; // compiled runtime, links inlined
import meta from "@skill/meta"; // metadata.json as SkillMeta[]The plugin recompiles when the metadata is missing or older than any source file, and exposes @skill/* module types via @review-skill/vite/client.
MIT


