Skip to content

Latest commit

 

History

History
190 lines (144 loc) · 13.4 KB

CLAUDE.md

File metadata and controls

190 lines (144 loc) · 13.4 KB

vercel-plugin Development Guide

Quick Reference

  • Build hooks: bun run build:hooks (compiles hooks/src/*.mts → hooks/*.mjs via tsup)
  • Build from skills: bun run build:from-skills (compiles *.md.tmpl → *.md by resolving {{include:skill:…}} markers)
  • Check from skills: bun run build:from-skills:check (verify generated .md files are up-to-date; exits non-zero on drift)
  • Build all: bun run build:all (skills + hooks + from-skills). It is not named build: npm then "prepares" Git installs, which breaks opencode plugin add github:vercel/vercel-plugin (anomalyco/opencode#49704)
  • Test: bun test (typecheck + 42 test files)
  • Single test: bun test tests/<file>.test.ts
  • Typecheck only: bun run typecheck (tsc on hooks/tsconfig.json)
  • Validate skills: bun run validate (structural validation of skill sources; does not write generated files)
  • Doctor: bun run doctor (self-diagnosis: skill validation, hook timeouts, dedup health)
  • Update snapshots: bun test:update-snapshots (regenerate golden snapshot baselines)
  • Playground: bun run playground:generate (generate static skill files for external tools)

Run bun run build:hooks after editing any .mts file. A pre-commit hook auto-compiles when .mts files are staged.

Run bun run build:from-skills after editing any skill referenced by a .md.tmpl template. The build:all script includes this step automatically.

Architecture

Hook Registration (hooks/hooks.json)

All hooks are registered in hooks/hooks.json and run via node "${CLAUDE_PLUGIN_ROOT}/hooks/<file>.mjs". Hook output is type-checked against SyncHookJSONOutput from @anthropic-ai/claude-agent-sdk.

Event Hook Matcher Timeout
SessionStart session-start-seen-skills.mjs startup|resume|clear|compact —
SessionStart session-start-profiler.mjs startup|resume|clear|compact —
SessionStart inject-claude-md.mjs startup|resume|clear|compact —
PostToolUse posttooluse-skill-telemetry.mjs Skill 5s
SessionEnd session-end-cleanup.mjs — —

Hook Source Files (hooks/src/*.mts)

Source lives in hooks/src/*.mts (TypeScript) and compiles to hooks/*.mjs (ESM, committed).

Entry-point hooks (wired in hooks.json):

  • session-start-seen-skills.mts — initializes VERCEL_PLUGIN_SEEN_SKILLS="" in CLAUDE_ENV_FILE
  • session-start-profiler.mts — activates only for greenfield directories or detected Vercel, Next.js, or eve projects, then scans config files + package deps → sets VERCEL_PLUGIN_LIKELY_SKILLS (+5 priority boost)
  • inject-claude-md.mts — outputs the thin session-start Vercel context plus knowledge update guidance for that same activation set
  • posttooluse-skill-telemetry.mts — on Skill tool use, reports the bare name of a plugin-namespaced (vercel:/vercel-plugin:), plugin-shipped skill/command (skill:invoked); other namespaces, bare names, and arguments are dropped. Also the --send entrypoint that the detached sender runs for every skill event
  • session-end-cleanup.mts — deletes session-scoped temp files

Library modules (imported by entry-point hooks):

  • hook-env.mts — shared runtime helpers (env parsing, path resolution)
  • skill-map-frontmatter.mts — YAML parser + frontmatter extraction + buildSkillMap() + validateSkillMap()
  • patterns.mts — glob→regex conversion, seen-skills helpers, ranking, atomic file claims
  • prompt-patterns.mts — prompt signal compiler + scorer (phrases/allOf/anyOf/noneOf)
  • prompt-analysis.mts — dry-run analysis reports for prompt matching
  • vercel-config.mts — vercel.json key→skill routing (±10 priority)
  • logger.mts — structured JSON logging to stderr (off/summary/debug/trace)
  • telemetry.mts — opt-out telemetry (VERCEL_PLUGIN_TELEMETRY=off): daily DAU ping, installation ID, skill slug/namespace validation, and skill:invoked / skill:injected event builders/senders
  • skill-telemetry.mts — shared skill-telemetry plumbing: per-session telemetry UUID + harness temp files, payload assembly, and queueSkillTelemetry() which spawns the detached sender (used by the Skill hook and both inject hooks)

OpenCode 2 Entry (hooks/src/opencode-plugin.mts)

OpenCode loads hooks/opencode-plugin.mjs through package.json exports (opencode plugin add github:vercel/vercel-plugin). It runs inside the long-lived OpenCode server, not as per-event subprocesses, and reuses the hook modules:

  • Skills, commands (vercel:<name>), agents (subagents), and the .mcp.json servers are registered through OpenCode transforms. User definitions with the same ID win.
  • The SessionStart profiler and inject-claude-md output becomes a context hook, computed once per lead session and reused verbatim. Child sessions get nothing, like Claude Code subagents.
  • Skill telemetry is a tool execute.after hook on OpenCode's skill tool, reported with the opencode harness. State is in memory, so there is no SessionEnd cleanup.

To try a local checkout, create ~/.config/opencode/plugins/vercel-plugin.js containing export { default } from "<checkout>/hooks/opencode-plugin.mjs";. A configured local directory path does not work: OpenCode only reads index.* from local plugin directories (anomalyco/opencode#52300).

Skill Injection Flow

  1. SessionStart: For greenfield directories or detected Vercel, Next.js, or eve projects, the profiler scans the project → sets VERCEL_PLUGIN_LIKELY_SKILLS
  2. PreToolUse (on Read/Edit/Write/Bash): Match file paths (glob), bash commands (regex), imports (regex+flags) → apply vercel.json routing → apply profiler boost → rank by priority → dedup → inject up to 3 skills within 18KB budget
  3. UserPromptSubmit: Score prompt text against promptSignals (phrases/allOf/anyOf/noneOf) → inject up to 2 skills within 8KB budget
    • 3b. Lexical fallback (when VERCEL_PLUGIN_LEXICAL_PROMPT=on): If phrase/allOf/anyOf scoring yields no matches above minScore, re-score using a lexical stemmer that normalizes prompt tokens before comparison — catches natural phrasing that exact-substring matching misses
  4. PostToolUse (Skill): If the loaded skill is one shipped by this plugin under its namespace, emit a skill:invoked telemetry event (name only, tagged with the session's harness) from a detached background process. The inject hooks in steps 2–3 emit skill:injected the same way when they are wired in
  5. SessionEnd: Clean up session-scoped temp files

Special triggers in PreToolUse:

  • TSX review: After N .tsx edits (default 3), injects react-best-practices
  • Dev server detection: Boosts agent-browser-verify when dev server patterns appear
  • Vercel env help: One-time injection for vercel env commands

Skill Structure (skills/<name>/SKILL.md)

38 skills in skills/. Each has a SKILL.md with YAML frontmatter:

---
name: skill-slug
description: "One-line description"
summary: "Brief fallback (injected when budget exceeded)"
metadata:
  priority: 6                    # 4-8 range; higher = injected first
  pathPatterns: ["glob1"]        # File glob patterns
  bashPatterns: ["regex1"]       # Bash command regex patterns
  importPatterns: ["package"]    # Import/require patterns
  promptSignals:                 # UserPromptSubmit scoring
    phrases: ["key phrase"]      # +6 each (exact substring, case-insensitive)
    allOf: [["term1", "term2"]]  # +4 per group (all must match)
    anyOf: ["optional"]          # +1 each, capped at +2
    noneOf: ["exclude"]          # Hard suppress (score → -Infinity)
    minScore: 6                  # Threshold (default 6)
  validate:                      # PostToolUse validation rules
    - pattern: "regex"
      message: "Error description"
      severity: "error|recommended|warn"
      skipIfFileContains: "regex" # Optional conditional skip
---
# Skill body (markdown, injected as additionalContext)

Skill Metadata

Hooks and developer tools read metadata directly from skills/*/SKILL.md using buildSkillMap() and validateSkillMap(). Matching patterns are compiled in memory for each invocation, so skill edits are picked up without rebuilding a separate index. CLI scripts share src/shared/skill-map-loader.ts.

Dedup Contract

Prevents the same skill from being injected twice in a session. Shared across PreToolUse and UserPromptSubmit hooks.

  • Claim dir: <tmpdir>/vercel-plugin-<sessionId>-seen-skills.d/ — one empty file per claimed skill, created atomically with openSync(path, "wx") (O_EXCL)
  • Session file: <tmpdir>/vercel-plugin-<sessionId>-seen-skills.txt — comma-delimited snapshot synced from claim dir
  • Env var: VERCEL_PLUGIN_SEEN_SKILLS — initialized by session-start, updated by hooks
  • State merge: mergeSeenSkillStates() unions all three sources
  • Cleanup: session-end-cleanup.mjs deletes temp files + claim dirs
  • Strategies (debug mode): "file" (atomic claims) → "env-var" (fallback) → "memory-only" (single invocation) → "disabled" (VERCEL_PLUGIN_HOOK_DEDUP=off)

YAML Parser

Uses inline parseSimpleYaml in skill-map-frontmatter.mjs, not js-yaml:

  • Bare null → string "null", not JavaScript null
  • Bare true/false → strings, not booleans
  • Unclosed [ → scalar string, not parse error
  • Tab indentation → explicit error

CLI (src/cli/)

  • vercel-plugin explain <target> [--json] [--project <path>] [--likely-skills s1,s2] [--budget <bytes>] — shows which skills match a file path or bash command, with priority breakdown and budget simulation
  • vercel-plugin doctor — validates skill metadata, hook timeout risk, dedup correctness, and template freshness

Playground (.playground/)

Generates static skill files for external tools (Cursor, VSCode Copilot, Gemini CLI, etc.). Run bun run playground:generate. Generators live in .playground/<tool-name>/, fixtures in .playground/_fixtures/, snapshots in .playground/_snapshots/.

Template Include Engine (scripts/build-from-skills.ts)

Agents and commands derive instructions from skills via .md.tmpl templates. Skills are the single source of truth — templates pull content at build time so agents/commands stay in sync without duplicating prose.

Convention: agents/<name>.md.tmpl and commands/<name>.md.tmpl compile to agents/<name>.md and commands/<name>.md (committed). Two include marker formats:

{{include:skill:<name>:<heading>}}            — extracts a markdown section by heading
{{include:skill:<name>:frontmatter:<field>}}  — extracts a frontmatter field value

Heading extraction is case-insensitive and captures everything from the heading to the next heading of equal or higher level.

Build: bun run build:from-skills resolves all includes and writes output files. It is part of bun run build. bun run build:from-skills:check verifies outputs are up-to-date in CI. Dependency queries (--skill <name>) and coverage reports (--audit) are calculated from templates on demand.

Current templates (7): agents/ai-architect.md.tmpl, agents/deployment-expert.md.tmpl, agents/performance-optimizer.md.tmpl, commands/bootstrap.md.tmpl, commands/deploy.md.tmpl, commands/env.md.tmpl, commands/status.md.tmpl.

Testing

42 test files across tests/. Key categories:

  • Hook integration: session-start-profiler, session-start-seen-skills
  • Pattern matching: patterns, fuzz-glob, fuzz-yaml, prompt-signals, prompt-analysis
  • Snapshots: snapshot-runner (golden snapshots of skill injection metadata per vercel.json fixture), snapshots (snapshot assertions)
  • Validation: validate, validate-rules, build-skill-map
  • Benchmark: benchmark-pipeline, benchmark-analyze
  • CLI: cli-explain
  • Specialized: notion-clone-patterns, slack-clone-patterns, tsx-review-trigger, dev-server-verify, subagent-fresh-env, session-timeline-subagent

Snapshot updates: bun run test:update-snapshots (sets UPDATE_SNAPSHOTS=1).

Environment Variables

Variable Default Description
VERCEL_PLUGIN_LOG_LEVEL off off / summary / debug / trace
VERCEL_PLUGIN_DEBUG — Legacy: 1 maps to debug level
VERCEL_PLUGIN_HOOK_DEBUG — Legacy: 1 maps to debug level
VERCEL_PLUGIN_SEEN_SKILLS "" Comma-delimited already-injected skills
VERCEL_PLUGIN_HOOK_DEDUP — off to disable dedup entirely
VERCEL_PLUGIN_LIKELY_SKILLS — Profiler-set comma-delimited skills (+5 boost)
VERCEL_PLUGIN_GREENFIELD — true if project is empty (profiler sets)
VERCEL_PLUGIN_INJECTION_BUDGET 18000 PreToolUse byte budget
VERCEL_PLUGIN_PROMPT_INJECTION_BUDGET 8000 UserPromptSubmit byte budget
VERCEL_PLUGIN_REVIEW_THRESHOLD 3 TSX edits before react-best-practices injection
VERCEL_PLUGIN_TSX_EDIT_COUNT 0 Current .tsx edit count (PreToolUse tracks)
VERCEL_PLUGIN_AUDIT_LOG_FILE — Audit log path or off
VERCEL_PLUGIN_LEXICAL_PROMPT on 0 to disable lexical stemmer fallback in UserPromptSubmit scoring
VERCEL_PLUGIN_TELEMETRY — off disables all telemetry (DAU ping, skill:invoked, skill:injected). bunfig.toml preloads tests/_preload-telemetry-off.ts so the test suite never phones home