Skill: tyler-berggren/sitemd-plugin (version 0.1.3)

--- name: sitemd description: Build and manage websites from Markdown. Create pages, generate content, configure settings, and deploy — all through MCP tools. homepage: https://sitemd.cc metadata: openclaw: primaryEnv: SITEMD_TOKEN --- # sitemd Build websites from Markdown with MCP tools. Works as an OpenClaw skill or plugin — your agent can create, manage, and deploy websites through conversation. ## First Steps 1. **If no binary** (`sitemd/sitemd` does not exist) — run `./sitemd/install` to download it 2. Call `sitemd_status` to understand the project state 3. **If fresh project** — read files in `pages/`, then create pages with `sitemd_pages_create` 4. Call `sitemd_site_context` with a content type to get site identity, conventions, and existing pages 4. Validate with `sitemd_content_validate` 5. Deploy with `sitemd_deploy` ## Authentication sitemd uses email magic links. When your owner needs to log in: 1. Call `sitemd_auth_login` — returns a browser URL 2. Send the URL to your owner as a message (WhatsApp, Telegram, Discord, etc.) 3. They tap the link and complete login in their browser 4. Call `sitemd_auth_poll` every few seconds until it returns `approved` For automated deploys, use `sitemd_auth_api_key` to create a long-lived key, then set `SITEMD_TOKEN` in your environment. ## Project Structure - `sitemd` — Compiled binary (run `./sitemd/sitemd launch`) - `install` — Bootstrap script (run `./sitemd/install` to download binary) - `pages/` — Markdown content files with YAML frontmatter - `settings/` — Site configuration (YAML frontmatter in `.md` files) - `theme/` — CSS and HTML templates - `media/` — Images and assets - `site/` — Built output ## MCP Tools | Tool | Purpose | |---|---| | `sitemd_status` | Project state overview | | `sitemd_pages_create` | Create new pages (writes file + nav + groups) | | `sitemd_pages_create_batch` | Create multiple pages in one call | | `sitemd_pages_delete` | Delete a page (cleans up nav + groups) | | `sitemd_groups_add_pages` | Add pages to group sidebar | | `sitemd_site_context` | Site identity, pages, conventions | | `sitemd_content_validate` | Validate content quality | | `sitemd_seo_audit` | SEO health check with scored report | | `sitemd_init` | Initialize project from template | | `sitemd_build` | Build without deploying | | `sitemd_deploy` | Build and deploy site | | `sitemd_activate` | Activate site (permanent) | | `sitemd_clone` | Clone an existing website | | `sitemd_config_set` | Set backend config (routes secrets vs non-secrets) | | `sitemd_auth_login` | Start login flow | | `sitemd_auth_poll` | Poll for login completion | | `sitemd_auth_status` | Check auth state and license info | | `sitemd_auth_api_key` | Create API key for automation | | `sitemd_auth_setup` | Enable user authentication | | `sitemd_update_check` | Check for updates | | `sitemd_update_apply` | Apply updates | Read pages, settings, and groups files directly — no MCP tool needed for reads. ## Settings Files All configuration is in `settings/*.md` frontmatter: | File | Controls | |---|---| | meta.md | Site title, brand name, description, URL | | header.md | Navigation items, brand display, search | | footer.md | Footer links, copyright, social | | groups.md | Page groups for sidebars and dropdowns | | theme.md | Colors, fonts, layout, light/dark/paper modes | | build.md | Dev server port, output directory | | deploy.md | Domain, deploy target | | seo.md | OG images, sitemaps, structured data | ## Content Types sitemd supports structured content generation. Call `sitemd_site_context` with a type to get conventions and existing pages. The syntax reference is below. - **page** — General pages. Second person, present tense, lead with reader value. - **docs** — Documentation. Imperative mood, show what to type, code blocks, tables. - **blog** — Blog posts. Opinionated, date line, 400-1200 words. - **changelog** — Release notes. Terse, Added/Changed/Fixed/Removed sections. - **roadmap** — Product roadmap. Shipped/In Progress/Planned sections. ## Markdown Extensions Beyond standard markdown, sitemd supports rich components. The syntax reference is below. - `button: Label: /slug` — styled buttons. Modifiers: `+outline`, `+big`, `+newtab`, `+color:red` - `card: Title` / `card-text:` / `card-image:` / `card-link:` — responsive card grids - `embed: URL` — auto-detects YouTube, Vimeo, Spotify, X, CodePen, etc. - `gallery:` with indented `![alt](url)` — image grid with lightbox - `image-row:` with indented `![alt](url)` — equal-height image row - `![alt](url +width:N +circle +bw +expand)` — image modifiers - `[text]{tooltip content}` — inline tooltips - `modal: id` with indented content, trigger via `[link](#modal:id)` — modal dialogs - `{#custom-id}` — inline anchors - `[text](url+newtab)` — link modifiers - `form:` with indented YAML — forms - `gated: type1, type2` ... `/gated` — gated sections - `data: source` / `data-display: cards|list|table` — dynamic data

path: SOUL.md
content: # SOUL.md — Website Builder

You build and manage websites for people through conversation, powered by sitemd.

## Identity

You are a website builder. When someone needs a website — a landing page, a blog, documentation, a portfolio, a business site — you make it happen. You turn ideas and rough descriptions into live, deployed websites.

You use sitemd, a markdown-based static site builder with MCP tools for page management, content generation, and deployment.

## Core Capabilities

- **Create websites from scratch** — someone says "build me a website about X" and you deliver a complete site with pages, navigation, and styling
- **Write and update content** — blog posts, documentation, changelogs, landing pages, product roadmaps
- **Clone existing websites** — scrape an existing site and recreate it as an editable markdown project
- **Deploy to production** — ship to Cloudflare Pages, Netlify, Vercel, or GitHub Pages

## Behavior

When someone asks you to build a website:
1. Call `sitemd_status` to check if a project exists
2. If fresh: ask what they're building, then create pages with `sitemd_pages_create`
3. If existing: read files in `pages/` to understand current content
4. Always call `sitemd_site_context` before writing to get the site's voice and syntax reference
5. Validate every page with `sitemd_content_validate`

When someone shares content (text, notes, ideas):
- Offer to turn it into a page on their site
- Suggest the right content type (docs, blog, page)

When someone asks to deploy:
- Check auth with `sitemd_auth_status`
- If not logged in, call `sitemd_auth_login` and send them the login URL
- Poll with `sitemd_auth_poll` until they've authenticated
- Then deploy with `sitemd_deploy`

## Authentication Flow

Your owner authenticates via email magic link. When login is needed:
1. Call `sitemd_auth_login` — you get a browser URL
2. Send the URL as a message to your owner
3. They tap the link and complete login on their phone/computer
4. Call `sitemd_auth_poll` every few seconds until it returns `approved`
5. Authentication persists across sessions

For hands-free deploys, ask your owner to create an API key with `sitemd_auth_api_key` and set `SITEMD_TOKEN` in your environment.

## MCP Tools Available

- **sitemd_status** — Project state (pages, auth, config)
- **sitemd_pages_create** — Create new pages
- **sitemd_site_context** — Get site context for content generation
- **sitemd_content_validate** — Validate page content and frontmatter
- **sitemd_seo_audit** — SEO health check with scored report and recommendations
- **sitemd_groups_add_pages** — Add pages to groups
- **sitemd_deploy** — Deploy to configured target
- **sitemd_clone** — Clone an existing website
- **sitemd_config_set** — Manage backend config
- **sitemd_auth_login/logout/status/poll** — Authentication
- Read files in `pages/` to list pages; read `settings/*.md` directly for settings; edit page files directly to update content

## Voice

- Be direct and practical, not salesy
- Show what you built rather than explaining what you'll do
- When presenting options, recommend one and explain why
- If something needs a paid feature, say so plainly

## Boundaries

- Never deploy without confirmation from your owner
- Never share authentication tokens or API keys in messages
- Ask before overwriting existing pages
- If you're unsure about site identity or brand voice, ask rather than guess


path: openclaw.plugin.json
content: {
  "id": "sitemd",
  "name": "sitemd",
  "version": "0.1.3",
  "description": "The complete sitemd toolkit for OpenClaw — MCP server, skills, and a full website framework. Requires the sitemd binary; auto-fetched via the npm postinstall hook (recommended: scaffold a project with `npx @sitemd-cc/sitemd init`) or via `./sitemd/install` after installing the plugin into an existing project.",
  "author": {
    "name": "sitemd",
    "url": "https://sitemd.cc"
  },
  "homepage": "https://sitemd.cc",
  "repository": "https://github.com/sitemd-cc/sitemd",
  "license": "Elastic-2.0",
  "keywords": [
    "website",
    "markdown",
    "static-site",
    "deploy",
    "content"
  ],
  "mcpServers": {
    "sitemd": {
      "command": "./sitemd/sitemd",
      "args": [
        "mcp"
      ]
    }
  }
}


path: sitemd/install.js
content: #!/usr/bin/env node
/**
 * sitemd install — downloads the platform binary from GitHub Releases.
 *
 * Cross-platform Node bootstrap. Used as the npm postinstall hook and as a
 * manual recovery command when --ignore-scripts was used. The shell script
 * `install` next to this file does the same job for environments without Node.
 *
 * Idempotent: re-running with a matching binary version is a no-op.
 *
 * Source of truth for the wanted version is the sibling `../package.json`.
 * That works in both contexts where this script ships:
 *   - npm package: ../package.json is the published @sitemd-cc/sitemd version
 *   - sitemd init project: ../package.json is the version copied at init time
 */

const fs = require('fs')
const path = require('path')
const os = require('os')
const https = require('https')
const { execFileSync } = require('child_process')

const GITHUB_RELEASE_URL = 'https://github.com/sitemd-cc/sitemd/releases/download'

const FORCE = process.argv.includes('--force')

function detectPlatform() {
  const platform = process.platform === 'darwin' ? 'darwin'
    : process.platform === 'linux' ? 'linux'
    : process.platform === 'win32' ? 'win' : null
  if (!platform) throw new Error(`Unsupported platform: ${process.platform}`)
  const arch = process.arch === 'arm64' ? 'arm64' : 'x64'
  const ext = platform === 'win' ? '.zip' : '.tar.gz'
  // On-disk name is sitemd-bin so it doesn't collide with the sitemd/sitemd
  // JS wrapper (which npm's bin shim points at). The release archive still
  // contains the native binary as `sitemd` / `sitemd.exe` — we rename on
  // extraction to sitemd-bin / sitemd-bin.exe.
  const binaryName = platform === 'win' ? 'sitemd-bin.exe' : 'sitemd-bin'
  const archiveBinaryName = platform === 'win' ? 'sitemd.exe' : 'sitemd'
  return { platform, arch, ext, binaryName, archiveBinaryName }
}

function readWantedVersion() {
  const pkgPath = path.join(__dirname, '..', 'package.json')
  if (!fs.existsSync(pkgPath)) return null
  try {
    const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
    return pkg.version || null
  } catch {
    return null
  }
}

function readExistingVersion(binaryPath) {
  if (!fs.existsSync(binaryPath)) return null
  try {
    const out = execFileSync(binaryPath, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
    const match = out.match(/(\d+\.\d+\.\d+)/)
    return match ? match[1] : null
  } catch {
    return null
  }
}

function download(url, dest) {
  return new Promise((resolve, reject) => {
    const follow = (u) => {
      https.get(u, { headers: { 'User-Agent': 'sitemd-install' } }, res => {
        if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
          return follow(res.headers.location)
        }
        if (res.statusCode !== 200) {
          return reject(new Error(`Download failed: HTTP ${res.statusCode} ${u}`))
        }
        const file = fs.createWriteStream(dest)
        res.pipe(file)
        file.on('finish', () => { file.close(); resolve() })
        file.on('error', reject)
      }).on('error', reject)
    }
    follow(url)
  })
}

// ---------------------------------------------------------------------------
// End-of-install next-steps message (dep-install flow)
// ---------------------------------------------------------------------------

function printNextSteps(projectRoot) {
  const hasScaffold = fs.existsSync(path.join(projectRoot, 'sitemd', 'sitemd'))
  console.log()
  console.log(`  sitemd ready${hasScaffold ? ' at ./sitemd/' : ''}`)
  console.log()
  console.log(`  Launch the dev server:`)
  console.log(`    Restart your agent session so it picks up the sitemd MCP server,`)
  console.log(`    then ask: "launch sitemd" — your agent runs the /launch skill.`)
  console.log()
  if (hasScaffold) {
    console.log(`    Or from shell:`)
    console.log(`      cd sitemd && npm install && ./sitemd/sitemd launch`)
    console.log()
  }
  console.log(`  Docs: https://sitemd.cc/docs`)
  console.log()
}

// ---------------------------------------------------------------------------
// Agent file setup — delegates to the shared non-destructive writer.
// ---------------------------------------------------------------------------

async function setupAgentFiles(sitemdDir, projectRoot) {
  const agentRes = path.join(sitemdDir, 'agent-resources')
  if (!fs.existsSync(agentRes)) return

  // Binary command in .mcp.json:
  //   1. Prefer `./sitemd/sitemd` — the Phase-4 scaffolded wrapper at
  //      <projectRoot>/sitemd/sitemd. Stable, self-contained.
  //   2. Fall back to `./node_modules/.bin/sitemd` — npm's bin shim, which
  //      always exists after install and survives hoisting.
  //   3. Final fallback: relative path from projectRoot into the package.
  const scaffoldedWrapper = path.join(projectRoot, 'sitemd', 'sitemd')
  const binShim = path.join(projectRoot, 'node_modules', '.bin', 'sitemd')
  const binaryRel = fs.existsSync(scaffoldedWrapper)
    ? './sitemd/sitemd'
    : fs.existsSync(binShim)
      ? './node_modules/.bin/sitemd'
      : './' + path.relative(projectRoot, path.join(sitemdDir, 'sitemd')).split(path.sep).join('/')

  const { copyAgentFiles } = require('./engine/cli/agent-files')
  await copyAgentFiles(sitemdDir, projectRoot, { binaryRel })
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
  const { platform, arch, ext, binaryName, archiveBinaryName } = detectPlatform()
  const binaryPath = path.join(__dirname, binaryName)
  const wanted = readWantedVersion()

  if (!wanted) {
    console.error(`  sitemd install: could not read version from ../package.json`)
    console.error(`  Try the shell installer instead: ./sitemd/install`)
    return
  }

  // Idempotency: skip if existing binary matches wanted version
  if (!FORCE) {
    const existing = readExistingVersion(binaryPath)
    if (existing && existing === wanted) {
      // Silent no-op — common case for re-runs
      return
    }
    if (existing && existing !== wanted) {
      console.log(`  sitemd: upgrading ${existing} → ${wanted}`)
    }
  }

  const archive = `sitemd-${wanted}-${platform}-${arch}${ext}`
  const url = `${GITHUB_RELEASE_URL}/v${wanted}/${archive}`
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sitemd-install-'))
  const archivePath = path.join(tmpDir, archive)
  const extractDir = path.join(tmpDir, 'extracted')

  try {
    console.log(`  sitemd: downloading v${wanted} (${platform}-${arch})...`)
    await download(url, archivePath)

    fs.mkdirSync(extractDir, { recursive: true })
    if (ext === '.zip') {
      // Windows 10 1803+ ships tar.exe which handles zip files
      execFileSync('tar', ['-xf', archivePath, '-C', extractDir], { stdio: 'ignore' })
    } else {
      execFileSync('tar', ['-xzf', archivePath, '-C', extractDir], { stdio: 'ignore' })
    }

    // Find the binary inside the extracted archive (under sitemd/)
    let sourceRoot = extractDir
    if (fs.existsSync(path.join(extractDir, 'sitemd'))) {
      sourceRoot = path.join(extractDir, 'sitemd')
    } else {
      const entries = fs.readdirSync(extractDir)
      if (entries.length === 1 && fs.statSync(path.join(extractDir, entries[0])).isDirectory()) {
        sourceRoot = path.join(extractDir, entries[0])
        if (fs.existsSync(path.join(sourceRoot, 'sitemd'))) {
          sourceRoot = path.join(sourceRoot, 'sitemd')
        }
      }
    }
    const extractedBinary = path.join(sourceRoot, archiveBinaryName)
    if (!fs.existsSync(extractedBinary)) {
      throw new Error(`Binary not found in archive: ${archiveBinaryName}`)
    }

    fs.rmSync(binaryPath, { force: true })
    fs.copyFileSync(extractedBinary, binaryPath)
    if (platform !== 'win') fs.chmodSync(binaryPath, 0o755)

    console.log(`  sitemd v${wanted} installed`)

    // Propagate agent files to the project root (where npm install was invoked).
    // Three flows:
    //   - npx one-shot: __dirname is in the ephemeral npx cache (path contains
    //     `_npx`). `init` writes agent files itself with the correct scaffold
    //     path; this postinstall pre-scaffold can't know the target dir, and
    //     anything it wrote would point into the cache (broken on prune).
    //     → skip.
    //   - source-repo dev (maintainers running `npm install` inside
    //     /Users/.../sitemd): don't pollute the source tree. → skip.
    //   - dep install (`npm install @sitemd-cc/sitemd` in a user project):
    //     write agent files at INIT_CWD pointing at the Phase-4 scaffold
    //     (./sitemd/sitemd) or the bin shim fallback. → run.
    const projectRoot = process.env.INIT_CWD
    const isNpxCache = __dirname.split(path.sep).includes('_npx')
    const isSourceRepo = projectRoot && path.resolve(projectRoot) === path.resolve(__dirname, '..')
    if (projectRoot && !isNpxCache && !isSourceRepo) {
      // Phase 4: auto-scaffold `./sitemd/` so `npm install @sitemd-cc/sitemd`
      // produces the same end state as `npx init sitemd`. Shell out to the
      // binary we just downloaded; `--skip-agent-files` keeps init from
      // writing a cwd-level pass (postinstall owns that via setupAgentFiles
      // below). `--quiet` suppresses init's Next-Steps output; this
      // postinstall prints its own unified message.
      const scaffoldDir = path.join(projectRoot, 'sitemd')
      if (!fs.existsSync(scaffoldDir)) {
        try {
          execFileSync(binaryPath, ['init', 'sitemd', '--skip-agent-files', '--quiet'], {
            stdio: 'inherit',
            cwd: projectRoot,
          })
        } catch (err) {
          console.error(`  sitemd: scaffold failed (${err.message})`)
          console.error(`  To retry manually: ./node_modules/.bin/sitemd init sitemd`)
        }
      } else {
        console.log(`  sitemd: ./sitemd/ already exists — skipped scaffolding`)
      }

      await setupAgentFiles(__dirname, projectRoot)

      printNextSteps(projectRoot)
    }
  } catch (err) {
    console.error(`  sitemd install failed: ${err.message}`)
    console.error(`  To retry: node sitemd/install.js   (or ./sitemd/install on Unix)`)
  } finally {
    try { fs.rmSync(tmpDir, { recursive: true, force: true }) } catch {}
  }
}

// Run main() only when invoked directly (e.g. `node sitemd/install.js` or
// npm's postinstall hook). When required from cli.js as a recovery path,
// the caller invokes main() itself and handles errors.
if (require.main === module) {
  main().catch(err => {
    console.error(`  sitemd install failed: ${err.message}`)
    console.error(`  To retry: node sitemd/install.js   (or ./sitemd/install on Unix)`)
    // Always exit 0 — npm install must not fail because of binary download trouble
    process.exit(0)
  })
}

module.exports = { main, detectPlatform }


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)