Skip to content

Repository files navigation

review-skill

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.

review-skill overview

Language: English | 简体中文



Overview

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

Framework layers

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.

Quick start

1. Bootstrap — no manual config

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.

2. Write a skill

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.

3. Compile

npx review-skill
Compiled 6 files in 91ms
  3 skills · self-contained typed runtime

4. Consume

import { skill, inject } from "@review-skill/skill";

const rules = skill("/react/rules/state.md");
const review = inject(rules.content, { focus: "state management" });

Authoring skills

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.

References — native markdown links

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 (![alt](url)) 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 .content so 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.

Templates — {{variables}} + inject()

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}}.
  • required defaults to true; set required: false for 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.

Compiling & configuration

CLI

npx review-skill            # compile once
npx review-skill --watch    # rebuild on change
npx review-skill --init     # scaffold a new project

Compiling 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

strip — token optimization

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~~",
    "![alt](url)",    // 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 · "![alt](url)" 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.

Consuming skills

skill() — typed imports

skill path autocomplete

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 metadata

skill TypeScript hover tooltip with metadata and token stats

Hover a skill() call to see the skill's title, description, source file, current character/token count, estimated compiled size, and percentage saved.

Self-contained content

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.

inject() — typed templates

For a skill with a variables contract, the compiler generates a per-skill interface (/galgame/section-planGalgameSectionplanVars) 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 missing

Integrations

Agent frameworks

Compiled resources are plain Markdown strings, so they work in any prompt builder — as system prompts, developer instructions, tool rules, review policies, or RAG chunks.

LangChain

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\`\`\`` },
]);

Mastra

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",
});

Vercel AI SDK

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}`,
});

OpenAI SDK

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}` },
  ],
});

Custom agent

import { skill } from "@review-skill/skill";

const guide = skill("/security/owasp.md");
agent.setSystemPrompt(guide.content);

@review-skill/vite

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.

Links & license

MIT

About

Turn Markdown Agent Skills into type-safe, token-aware runtime artifacts

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages