Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# markdocs

Go CLI that compiles library documentation into Claude Code skill files (`.claude/skills/<category>/<library>.md`).
Go CLI that helps you draft and create custom Claude Code skill files from existing documentation (`.claude/skills/<category>/<library>.md`).

## Commands

Expand All @@ -17,11 +17,12 @@ go run main.go <command> # run locally
- **No SQLite / no CGO** — metadata lives entirely in each skill file's YAML frontmatter (`SkillMeta`), not a database
- **Single binary** — no runtime requirements for end users; distributed via npm and goreleaser
- **Skills are plain markdown** — written to `.claude/skills/<category>/<library>.md`; Claude Code picks them up automatically next session
- **Draft-review before write** — after LLM compilation, users review and optionally edit the skill in `$EDITOR` before it's written to disk; `--no-interactive` skips this

## Directory Structure

```
cmd/ → cobra commands (root, add, scan, update, list, init, pipeline)
cmd/ → cobra commands (root, add, draft, scan, update, list, init, pipeline)
internal/
config/ → read/write ~/.markdocs/config.json
llm/ → LLMProvider interface + Anthropic / OpenAI / Ollama backends
Expand Down Expand Up @@ -75,9 +76,26 @@ Followed by LLM-compiled markdown with sections: What This Is, Installation, Key
1. Create `cmd/<name>.go` with a `cobra.Command`
2. Register it in `cmd/root.go` `init()`

See `cmd/draft.go` as a recent example.

## Shared Pipeline

`cmd/pipeline.go:runAddPipeline` contains the reusable scrape → compile → write flow used by both `add` and `scan --add-all`. Keep command-specific logic in the command files; pipeline logic here.
`cmd/pipeline.go:runAddPipeline` contains the reusable scrape → compile → [review] → write flow used by `add`, `scan --add-all`, and `update`.

Signature: `runAddPipeline(ctx, library, url, provider, cwd, noInteractive bool) (*PipelineResult, error)`

- `noInteractive = false` (default for `add`): shows the draft review prompt (`ui.ReviewDraft`) after compile, before write. Returns `nil, nil` if the user discards.
- `noInteractive = true` (used by `scan --add-all` and `update`): skips review, writes immediately.

### Draft Review (`ui.ReviewDraft`)

Defined in `internal/ui/prompts.go`. Shows a bordered preview of the first 50 lines of the compiled skill, then prompts: Accept / Edit in $EDITOR / Discard.

- **Edit**: writes content to a temp `.md` file, opens `$EDITOR` (fallback `nano`), re-reads on exit
- **Accept**: passes markdown through unchanged
- **Discard**: returns `ReviewDiscard`; caller returns `nil, nil` to signal cancellation

`ReviewAction` constants: `ReviewAccept`, `ReviewEdit`, `ReviewDiscard` (in `internal/ui/prompts.go`).

## Code Style

Expand Down
45 changes: 36 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@

<img src="assets/markdocs-logo.png" alt="markdocs" width="420" />

CLI to compile library documentation into Claude Code skill files
[![Build](https://github.com/Nithin-Valiyaveedu/markdocs/actions/workflows/build.yml/badge.svg)](https://github.com/Nithin-Valiyaveedu/markdocs/actions/workflows/build.yml)
[![Test](https://github.com/Nithin-Valiyaveedu/markdocs/actions/workflows/test.yml/badge.svg)](https://github.com/Nithin-Valiyaveedu/markdocs/actions/workflows/test.yml)
[![npm](https://img.shields.io/npm/v/@val-nithin/markdocs)](https://www.npmjs.com/package/@val-nithin/markdocs)
[![Downloads](https://img.shields.io/npm/dm/@val-nithin/markdocs)](https://www.npmjs.com/package/@val-nithin/markdocs)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Go Report Card](https://goreportcard.com/badge/github.com/Nithin-Valiyaveedu/markdocs)](https://goreportcard.com/report/github.com/Nithin-Valiyaveedu/markdocs)

Draft and create custom Claude Code skill files from existing documentation — with LLM assistance and a review step before anything is written to disk.

## How It Works

`markdocs add react` does five things:
`markdocs add react` does six things:

1. Asks the configured LLM for official documentation URLs
2. Lets you pick which URL(s) to scrape (interactive)
3. Scrapes the page with a built-in Go scraper (no external APIs)
4. Compiles the content into a structured skill file using the LLM
5. Writes `.claude/skills/frontend/react.md` — Claude Code picks it up next session
4. Compiles the content into a structured skill draft using the LLM
5. Shows you the draft in the terminal — edit it in `$EDITOR` if you want
6. Writes `.claude/skills/frontend/react.md` — Claude Code picks it up next session

## Installation

Expand Down Expand Up @@ -41,15 +49,18 @@ brew install markdocs
# 1. Configure your LLM provider (runs once)
markdocs init

# 2. Add a skill for any library
# 2. Add a skill for any library (shows draft review before writing)
markdocs add shadcn
markdocs add stripe
markdocs add drizzle-orm

# 3. Scan your project for missing skills
# 3. Create a skill from your own notes or docs
markdocs draft internal-api

# 4. Scan your project for missing skills
markdocs scan

# 4. Keep skills up to date
# 5. Keep skills up to date
markdocs update --all
```

Expand All @@ -71,7 +82,7 @@ Config is saved to `~/.markdocs/config.json`.

### `markdocs add <library>`

Find, scrape, compile, and write a skill file for the given library.
Find, scrape, compile, and write a skill file for the given library. After compilation, a draft review prompt lets you inspect and optionally edit the skill before it's written to disk.

```bash
markdocs add react
Expand All @@ -83,12 +94,28 @@ markdocs add stripe --no-interactive

| Flag | Description |
|------|-------------|
| `--no-interactive` | Skip URL selection prompt, use first suggested URL |
| `--no-interactive` | Skip URL selection and draft review prompts; write immediately |

The skill is written to `.claude/skills/<category>/<library>.md` in the current directory.

---

### `markdocs draft <name>`

Compile your own notes, internal docs, or any text content into a skill file. Useful for private APIs, internal tools, or any library not well-covered by public documentation.

```bash
# Interactive — paste content in the terminal
markdocs draft internal-api

# Pipe content from a file or command
cat docs/api.md | markdocs draft internal-api
```

After compilation, the same draft review prompt is shown — accept as-is, edit in `$EDITOR`, or discard.

---

### `markdocs scan`

Detect libraries in the current project that don't have skill files yet. Reads `package.json`, `go.mod`, and `requirements.txt`.
Expand Down
29 changes: 14 additions & 15 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"os"
"strings"

"github.com/Nithin-Valiyaveedu/markdocs/internal/llm"
"github.com/Nithin-Valiyaveedu/markdocs/internal/skill"
"github.com/Nithin-Valiyaveedu/markdocs/internal/ui"
"github.com/spf13/cobra"
)
Expand All @@ -16,32 +14,30 @@ var addNoInteractive bool
var addCmd = &cobra.Command{
Use: "add <library>",
Short: "Find, scrape, compile, and write a skill for a library.",
Long: `Discovers documentation URLs for the given library using the configured LLM,
presents them for interactive selection, scrapes the pages, compiles the content
into a structured skill file, and writes it to .claude/skills/<category>/<library>.md.`,
Long: `Discovers documentation URLs for the given library, scrapes the pages,
compiles the content into a structured skill file, and writes it to
.claude/skills/<category>/<library>.md.

By default, structured extraction is used (no API key required).
Pass --llm to use an LLM provider for richer content (requires 'markdocs init').`,
Args: cobra.MinimumNArgs(1),
RunE: runAdd,
}

func init() {
addCmd.Flags().BoolVar(&addNoInteractive, "no-interactive", false, "Skip URL selection prompt, use first suggested URL")
addCmd.Flags().BoolVar(&addNoInteractive, "no-interactive", false, "Skip URL selection and draft review prompts")
}

func runAdd(cmd *cobra.Command, args []string) error {
library := strings.Join(args, " ")
ctx := cmd.Context()

provider, err := llm.NewProvider(appConfig)
if err != nil {
ui.Error(fmt.Sprintf("Failed to create provider: %s", err))
os.Exit(2)
}
compiler, providerName, modelName := newCompiler()

// Step 1: Discover URLs
var urls []string
ui.Step(1, fmt.Sprintf("Asking LLM for %s documentation URLs...", library))
err = ui.Spin("", func() error {
compiler := skill.NewLLMCompiler(provider)
ui.Step(1, fmt.Sprintf("Discovering %s documentation URLs...", library))
err := ui.Spin("", func() error {
var err error
urls, err = compiler.SuggestURLs(ctx, library)
return err
Expand Down Expand Up @@ -70,11 +66,14 @@ func runAdd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("getting working directory: %w", err)
}

result, err := runAddPipeline(ctx, library, selectedURL, provider, cwd)
result, err := runAddPipeline(ctx, library, selectedURL, compiler, providerName, modelName, cwd, addNoInteractive)
if err != nil {
ui.Error(fmt.Sprintf("Pipeline failed: %s", err))
os.Exit(2)
}
if result == nil {
return nil // user discarded the draft
}

ui.Blank()
ui.Success(fmt.Sprintf("Skill ready: %s", result.SkillPath))
Expand Down
132 changes: 132 additions & 0 deletions cmd/draft.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package cmd

import (
"fmt"
"io"
"os"
"strings"

"github.com/Nithin-Valiyaveedu/markdocs/internal/skill"
"github.com/Nithin-Valiyaveedu/markdocs/internal/ui"
"github.com/charmbracelet/huh"
"github.com/spf13/cobra"
)

var draftCmd = &cobra.Command{
Use: "draft <name>",
Short: "Compile your own notes or docs into a skill file.",
Long: `Prompts you to paste documentation, notes, or any text content.
The LLM compiles it into a structured skill file, shows a draft preview,
and lets you edit before writing to .claude/skills/<category>/<name>.md.

You can also pipe content directly:

cat docs.md | markdocs draft mylib`,
Args: cobra.ExactArgs(1),
RunE: runDraft,
}

func runDraft(cmd *cobra.Command, args []string) error {
library := args[0]
ctx := cmd.Context()

compiler, providerName, modelName := newCompiler()

// Step 1: Get content from stdin (pipe) or interactive textarea
var content string
if isStdinPipe() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("reading stdin: %w", err)
}
content = string(raw)
ui.Info(fmt.Sprintf("Read %d chars from stdin", len(content)))
} else {
ui.Info("Paste your documentation, notes, or any relevant content below.")
ui.Info("Press Ctrl+D or Esc to finish.")
ui.Blank()
err := huh.NewText().
Title(fmt.Sprintf("Content for %q skill", library)).
Placeholder("Paste documentation, API references, examples...").
Value(&content).
Run()
if err != nil {
return fmt.Errorf("content input: %w", err)
}
}

if len(strings.TrimSpace(content)) < 50 {
ui.Warning("Content is too short — add more detail and try again.")
return nil
}

// Step 2: Compile
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("getting working directory: %w", err)
}

var compiled *skill.CompileOutput
err = ui.Spin("Compiling skill...", func() error {
framework := skill.DetectFramework(cwd)
var err error
compiled, err = compiler.Compile(ctx, skill.CompileInput{
Library: library,
URL: "user-provided notes",
ScrapedMarkdown: content,
ProjectFramework: framework,
Model: modelName,
})
return err
})
if err != nil {
return fmt.Errorf("compiling skill for %s: %w", library, err)
}
ui.StepDone(1, fmt.Sprintf("Compiled skill (category: %s)", compiled.Category))

// Step 3: Review draft
action, finalMarkdown, err := ui.ReviewDraft(compiled.Markdown)
if err != nil {
return fmt.Errorf("draft review: %w", err)
}
if action == ui.ReviewDiscard {
ui.Warning("Discarded — no skill written.")
return nil
}
if action == ui.ReviewEdit {
compiled.Markdown = finalMarkdown
}

// Step 4: Write
framework := skill.DetectFramework(cwd)
meta := skill.NewSkillMeta(
library,
providerName,
modelName,
compiled.Category,
framework,
compiled.Description,
compiled.WhenToUse,
[]string{"user-provided notes"},
content,
)
writer := skill.NewFSWriter(cwd)
path, err := writer.Write(library, compiled.Category, compiled.Markdown, meta)
if err != nil {
return fmt.Errorf("writing skill for %s: %w", library, err)
}

ui.Blank()
ui.Success(fmt.Sprintf("Skill ready: %s", path))
ui.Info("Claude Code will pick it up automatically next session.")
return nil
}

// isStdinPipe reports whether stdin is a pipe (non-interactive).
func isStdinPipe() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
return fi.Mode()&os.ModeCharDevice == 0
}
Loading