diff --git a/.eslintrc.js b/.eslintrc.js index 7d6108d36b..0ae9576963 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -148,6 +148,40 @@ module.exports = { rules: { '@typescript-eslint/no-non-null-assertion': 'off', } - } + }, + { + files: ['scripts/*.js'], + env: { + node: true, + }, + rules: { + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + 'no-undef': 'off', + }, + }, + { + // Plain-Node Jest specs (e.g. docs tooling tests that `require` JS modules + // rather than importing typed sources). Same relaxations as `scripts/*.js`. + files: ['**/test/**/*.spec.js'], + env: { + node: true, + }, + rules: { + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + 'no-undef': 'off', + }, + }, ], } diff --git a/CHANGELOG.md b/CHANGELOG.md index 683e1da144..5ff2d8700b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- Added agent-friendly documentation: per-page `.md` companions, a Copy-Markdown button, an `llms.txt` index, and a coding-agent setup guide. [#1696](https://github.com/handsontable/hyperformula/pull/1696) - Added an Indonesian (Bahasa Indonesia) language pack. [#1674](https://github.com/handsontable/hyperformula/pull/1674) ## [3.3.0] - 2026-05-20 diff --git a/context7.json b/context7.json new file mode 100644 index 0000000000..529df28606 --- /dev/null +++ b/context7.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "projectTitle": "HyperFormula", + "description": "Headless, Excel-compatible spreadsheet engine in TypeScript — parses and evaluates ~400 functions in the browser or Node.js. In-process library (no REST API).", + "folders": ["docs"], + "excludeFolders": ["docs/.vuepress/dist", "docs/api"], + "rules": [ + "HyperFormula is an in-process library, not a REST API — there is no HTTP endpoint or base URL.", + "Public API cell addresses are 0-indexed: { sheet, col, row }.", + "There is no #CALC! error type.", + "EmptyValue is exported as a Symbol, not null/undefined.", + "A license key is required when constructing the engine (use 'gpl-v3' for open-source use)." + ] +} diff --git a/docs/.vuepress/components/CodingAgentWizard.vue b/docs/.vuepress/components/CodingAgentWizard.vue new file mode 100644 index 0000000000..e5208374e2 --- /dev/null +++ b/docs/.vuepress/components/CodingAgentWizard.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/docs/.vuepress/components/CopyMarkdownButton.vue b/docs/.vuepress/components/CopyMarkdownButton.vue new file mode 100644 index 0000000000..018ec1727c --- /dev/null +++ b/docs/.vuepress/components/CopyMarkdownButton.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index d185119bc9..2e4862a6a5 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -5,6 +5,7 @@ const searchBoxPlugin = require('./plugins/search-box'); const examples = require('./plugins/examples/examples'); const HyperFormula = require('../../dist/hyperformula.full'); const includeCodeSnippet = require('./plugins/markdown-it-include-code-snippet'); +const mdCompanions = require('./plugins/md-companions'); const searchPattern = new RegExp('^/api', 'i'); @@ -31,7 +32,7 @@ const DOCS_HOSTNAME = process.env.DOCS_HOSTNAME || buildConfigOverrides.hostname module.exports = { title: 'HyperFormula (v' + HyperFormula.version + ')', description: 'HyperFormula is an open-source, high-performance calculation engine for spreadsheets and web applications.', - globalUIComponents: [], + globalUIComponents: ['CopyMarkdownButton'], head: [ // Import HF (required for the examples) [ 'script', { src: 'https://cdn.jsdelivr.net/npm/hyperformula/dist/hyperformula.full.min.js' } ], @@ -89,6 +90,7 @@ module.exports = { exclude: ['/404.html'], changefreq: 'weekly' }], + [mdCompanions, { hostname: DOCS_HOSTNAME }], searchBoxPlugin, ['container', examples()], { @@ -206,6 +208,7 @@ module.exports = { ['/guide/advanced-usage', 'Advanced usage'], ['/guide/configuration-options', 'Configuration options'], ['/guide/license-key', 'License key'], + ['/guide/setup-coding-agent', 'Set up your coding agent'], ] }, { diff --git a/docs/.vuepress/plugins/md-companions/index.js b/docs/.vuepress/plugins/md-companions/index.js new file mode 100644 index 0000000000..ef0c2044cc --- /dev/null +++ b/docs/.vuepress/plugins/md-companions/index.js @@ -0,0 +1,44 @@ +const fs = require('fs'); +const path = require('path'); +const { stripVuePressSyntax } = require('./strip'); + +/** + * VuePress plugin: after build, write a clean `.md` companion next to each + * rendered `.html`, plus an aggregate `llms-full.txt`. Respects ctx.outDir + * (which already includes the configured base segment). + * @param {object} options plugin options + * @param {object} ctx VuePress app context + */ +module.exports = (options, ctx) => ({ + name: 'md-companions', + async generated() { + const hostname = (options && options.hostname) || 'https://hyperformula.handsontable.com'; + const base = ctx.base || '/'; + const pages = ctx.pages.filter(p => /\.html$/.test(p.path) && p.path !== '/404.html'); + const corpus = [ + '# HyperFormula Documentation', + '', + '> Full documentation corpus for LLM consumption.', + `> Individual pages also available at ${hostname}${base}guide/.md`, + '', + ]; + + for (const page of pages) { + try { + const clean = stripVuePressSyntax(page._strippedContent || ''); + const relPath = page.path.replace(/\.html$/, '.md'); + const outFile = path.join(ctx.outDir, relPath.replace(/^\//, '')); + await fs.promises.mkdir(path.dirname(outFile), { recursive: true }); + await fs.promises.writeFile(outFile, clean, 'utf8'); + + const url = hostname + base.replace(/\/$/, '') + page.path.replace(/\.html$/, ''); + corpus.push('---', '', `## ${page.title || page.path}`, '', `URL: ${url}`, '', clean, ''); + } catch (err) { + console.warn(`[md-companions] skipping ${page.path}: ${err.message}`); + } + } + + const llmsFull = path.join(ctx.outDir, 'llms-full.txt'); + await fs.promises.writeFile(llmsFull, corpus.join('\n'), 'utf8'); + } +}); diff --git a/docs/.vuepress/plugins/md-companions/strip.js b/docs/.vuepress/plugins/md-companions/strip.js new file mode 100644 index 0000000000..f6036576fe --- /dev/null +++ b/docs/.vuepress/plugins/md-companions/strip.js @@ -0,0 +1,75 @@ +/** + * Strips VuePress-specific markdown syntax, producing clean markdown + * suitable for LLM consumption. Fence-aware: never edits inside code blocks. + * @param {string} src raw markdown (frontmatter already removed) + * @returns {string} cleaned markdown + */ +function stripVuePressSyntax(src) { + const lines = src.split('\n'); + const out = []; + let inFence = false; + let fenceMarker = ''; + let inScript = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + + const fenceMatch = trimmed.match(/^(```+|~~~+)/); + if (fenceMatch && !inScript) { + if (!inFence) { + inFence = true; + fenceMarker = fenceMatch[1]; // full marker e.g. "```" or "````" + } else { + const closeMatch = trimmed.match(/^(```+|~~~+)/); + if (closeMatch && closeMatch[1][0] === fenceMarker[0] && closeMatch[1].length >= fenceMarker.length) { + inFence = false; + } + } + out.push(line); + continue; + } + if (inFence) { + out.push(line); + continue; + } + + if (/^<(script|style)[\s>]/i.test(trimmed)) { inScript = true; continue; } + if (inScript) { + if (/<\/(script|style)>/i.test(trimmed)) inScript = false; + continue; + } + + if (/^<[A-Z][A-Za-z0-9]*(\s[^>]*)?\/?>$/.test(trimmed)) continue; + + if (/^\[\[toc\]\]$/i.test(trimmed)) continue; + + const open = trimmed.match(/^:::\s*(\w+)\s*(.*)$/i); + if (open) { + const type = open[1].toLowerCase(); + const title = open[2].trim(); + const body = []; + i++; + const bodyStart = i; + while (i < lines.length && lines[i].trim() !== ':::') { body.push(lines[i]); i++; } + // If we hit EOF without finding closing :::, emit verbatim (not a real container). + if (i >= lines.length) { + out.push(lines[bodyStart - 1]); // re-emit the opening line + body.forEach(b => out.push(b)); + continue; + } + // Demo/example containers (live code runners) are not prose — omit entirely. + if (type === 'example') { continue; } + if (title) { out.push(`> **${title}**`); out.push('>'); } + body.forEach(b => out.push(b.trim() === '' ? '>' : `> ${b}`)); + while (out.length && out[out.length - 1] === '>') out.pop(); + continue; + } + + out.push(line); + } + + return out.join('\n').replace(/\n{3,}/g, '\n\n').trim(); +} + +module.exports = { stripVuePressSyntax }; diff --git a/docs/.vuepress/public/llms.txt b/docs/.vuepress/public/llms.txt new file mode 100644 index 0000000000..16bdfd167d --- /dev/null +++ b/docs/.vuepress/public/llms.txt @@ -0,0 +1,10 @@ +# HyperFormula + +> HyperFormula is an open-source, high-performance calculation engine for spreadsheets and web applications. + +## Docs + +- Guide: https://hyperformula.handsontable.com/docs/guide/ +- API reference: https://hyperformula.handsontable.com/docs/api/ +- Full corpus: https://hyperformula.handsontable.com/docs/llms-full.txt +- Official Claude skill: https://github.com/handsontable/handsontable-skills diff --git a/docs/.vuepress/public/robots.txt b/docs/.vuepress/public/robots.txt index ef083139b3..8ac6ab69c4 100644 --- a/docs/.vuepress/public/robots.txt +++ b/docs/.vuepress/public/robots.txt @@ -2,3 +2,6 @@ User-agent: * Allow: / Sitemap: https://hyperformula.handsontable.com/sitemap.xml + +# AI/LLM agent index +# Full documentation corpus: https://hyperformula.handsontable.com/docs/llms-full.txt diff --git a/docs/guide/setup-coding-agent.md b/docs/guide/setup-coding-agent.md new file mode 100644 index 0000000000..889a5466a2 --- /dev/null +++ b/docs/guide/setup-coding-agent.md @@ -0,0 +1,45 @@ +# Set up your coding agent + +HyperFormula ships an official Claude skill and machine-readable docs so your AI coding agent can scaffold, configure, and debug HyperFormula correctly. Pick your tool below, or use the interactive wizard. + + + +## Claude Code + +Install the official skill from the plugin marketplace: + +``` +/plugin marketplace add handsontable/handsontable-skills +/plugin install handsontable-skills@handsontable-skills +``` + +Claude Code loads the `hyperformula` skill automatically based on what you ask. + +## Cursor, Copilot & other agents + +These tools don't yet support the Claude skill format. Point your agent at the machine-readable docs instead: + +- **Full corpus:** [`llms-full.txt`](../llms-full.txt) — the entire documentation in one LLM-friendly file. +- **Per-page Markdown:** append `.md` to any docs URL, or use the **Copy Markdown** button on any page. + +For agents that read a rules file (e.g. Cursor's `AGENTS.md`), add a line pointing at the corpus URL so the agent fetches authoritative docs on demand. + +## Live docs via MCP (any agent) + +Two zero-setup ways to let an agent pull authoritative HyperFormula docs on demand — both read this site's `llms.txt`: + +- **GitMCP** — add the MCP server `https://gitmcp.io/handsontable/hyperformula` to your agent (e.g. `claude mcp add --transport http hyperformula https://gitmcp.io/handsontable/hyperformula`). No install, no auth. +- **Context7** — run `npx -y @upstash/context7-mcp` (or use the Context7 skill / `ctx7` CLI) and ask for the `hyperformula` library. Context7 indexes this site's `llms.txt` / `llms-full.txt` (see `context7.json` in the repo root). + +## Manual install (any Claude Code setup) + +```bash +git clone https://github.com/handsontable/handsontable-skills.git +cp -r handsontable-skills/skills/hyperformula ~/.claude/skills/ +``` + +## Resources + +- [Official skill repository](https://github.com/handsontable/handsontable-skills) +- [`llms-full.txt`](../llms-full.txt) +- [API reference](/api/) diff --git a/docs/superpowers/plans/2026-05-31-hf154-agent-friendly-docs.md b/docs/superpowers/plans/2026-05-31-hf154-agent-friendly-docs.md new file mode 100644 index 0000000000..bce12c9cc5 --- /dev/null +++ b/docs/superpowers/plans/2026-05-31-hf154-agent-friendly-docs.md @@ -0,0 +1,644 @@ +# HF-154 Agent-Friendly Docs Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make HyperFormula's VuePress docs agent-friendly by emitting clean `.md` companions + `llms-full.txt`, adding a Copy Markdown button, and a "Set up your coding agent" page (static + interactive wizard). + +**Architecture:** Five components targeting VuePress 1.x under `docs/`. A post-build plugin (`generated` hook) writes `.md` companions and `llms-full.txt` into the dist dir. A global Vue component renders the Copy Markdown button using `$page.path`. A new guide page hosts both a static IDE matrix and an interactive wizard component. All paths respect `base: '/docs/'`. + +**Tech Stack:** VuePress 1.9.10, Vue 2 SFCs, markdown-it, Node.js fs, plain `node` test scripts (matching existing `docs/.vuepress/plugins/*/test.js` pattern). + +**Critical constraint:** `docs/.vuepress/build.config.js` sets `base: '/docs/'`, `dest: 'docs/.vuepress/dist/docs'`. `ctx.outDir` already includes the `/docs` segment. Page paths (`$page.path`) do NOT include base — prepend with `$withBase`. + +**Parallelization units** (for subagent dispatch): +- **Unit A** = Task 1 (C1 plugin) — independent +- **Unit B** = Task 2 (C2 button) — independent +- **Unit C** = Tasks 3+4 (C3 page + C4 wizard, coupled, same page) — independent +- **Unit D** = Task 5 (C5 llms.txt/robots.txt) — independent +- **Task 6** = integration (wire into config.js, full build, verify) — runs LAST, after A–D merge + +--- + +## Task 1: C1 — `md-companions` VuePress plugin + +**Files:** +- Create: `docs/.vuepress/plugins/md-companions/strip.js` (pure stripping function) +- Create: `docs/.vuepress/plugins/md-companions/strip.test.js` (node test) +- Create: `docs/.vuepress/plugins/md-companions/index.js` (plugin: generated hook) +- Create: `docs/.vuepress/plugins/md-companions/fixture.md` (test fixture) + +- [ ] **Step 1: Write the failing test for `stripVuePressSyntax`** + +Create `docs/.vuepress/plugins/md-companions/strip.test.js`: + +```js +const assert = require('assert'); +const { stripVuePressSyntax } = require('./strip'); + +let passed = 0; +const check = (name, actual, expected) => { + assert.strictEqual(actual, expected, `FAIL: ${name}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); + passed++; +}; + +// 1. :::tip container -> blockquote, body kept +check('tip container', + stripVuePressSyntax(':::tip Heads up\nBe careful here.\n:::'), + '> **Heads up**\n>\n> Be careful here.'); + +// 2. :::warning without title +check('warning no title', + stripVuePressSyntax(':::warning\nDanger zone.\n:::'), + '> Danger zone.'); + +// 3. CRITICAL: ::: inside a fenced code block is NOT touched +check('code fence with ::: inside', + stripVuePressSyntax('```js\nconst x = ":::tip";\n```'), + '```js\nconst x = ":::tip";\n```'); + +// 4. \nText after'), + 'Text before\nText after'); + +// 5. standalone Vue component removed +check('vue component removed', + stripVuePressSyntax('Intro\n\nOutro'), + 'Intro\nOutro'); + +// 6. [[toc]] removed +check('toc removed', + stripVuePressSyntax('# Title\n[[toc]]\nBody'), + '# Title\nBody'); + +// 7. headings, code, links, tables preserved +check('content preserved', + stripVuePressSyntax('# H\n\n`code`\n\n[link](/guide/x)\n\n| a | b |\n|---|---|'), + '# H\n\n`code`\n\n[link](/guide/x)\n\n| a | b |\n|---|---|'); + +console.log(`PASS md-companions/strip (${passed} assertions)`); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node docs/.vuepress/plugins/md-companions/strip.test.js` +Expected: FAIL — `Cannot find module './strip'` + +- [ ] **Step 3: Implement `strip.js`** + +Create `docs/.vuepress/plugins/md-companions/strip.js`: + +```js +/** + * Strips VuePress-specific markdown syntax, producing clean markdown + * suitable for LLM consumption. Fence-aware: never edits inside code blocks. + * @param {string} src raw markdown (frontmatter already removed) + * @returns {string} cleaned markdown + */ +function stripVuePressSyntax(src) { + const lines = src.split('\n'); + const out = []; + let inFence = false; + let fenceMarker = ''; + let inScript = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + + // Track fenced code blocks (``` or ~~~). Inside a fence, copy verbatim. + const fenceMatch = trimmed.match(/^(```+|~~~+)/); + if (fenceMatch && !inScript) { + if (!inFence) { + inFence = true; + fenceMarker = fenceMatch[1][0]; + } else if (trimmed.startsWith(fenceMarker)) { + inFence = false; + } + out.push(line); + continue; + } + if (inFence) { + out.push(line); + continue; + } + + // Remove + + +``` + +- [ ] **Step 2: Verify it parses (lint)** + +Run: `npx eslint docs/.vuepress/components/CopyMarkdownButton.vue --no-eslintrc --parser vue-eslint-parser 2>/dev/null || echo "manual review: confirm template/script/style blocks well-formed"` +Expected: no parse error (or fall back to manual confirmation — the project's ESLint targets `.js,.ts`, not `.vue`). + +- [ ] **Step 3: Commit** + +```bash +git add docs/.vuepress/components/CopyMarkdownButton.vue +git commit -m "feat(docs): add Copy Markdown button global component" +``` + +--- + +## Task 3: C3 — `setup-coding-agent.md` static page + +**Files:** +- Create: `docs/guide/setup-coding-agent.md` + +**Install-command source of truth:** `handsontable/handsontable-skills` README. Verified real commands: +- Claude Code marketplace: `/plugin marketplace add handsontable/handsontable-skills` then `/plugin install handsontable-skills@handsontable-skills` +- Manual: `git clone https://github.com/handsontable/handsontable-skills.git` then `cp -r handsontable-skills/skills/hyperformula ~/.claude/skills/` +- Cowork/web: zip from GitHub releases +- API: folder upload + +The published skill has **no Cursor- or Copilot-specific installer**. Do NOT invent one. Cursor/Copilot/Other sections point to `llms-full.txt` + the manual clone, framed honestly. + +- [ ] **Step 1: Write the page** + +Create `docs/guide/setup-coding-agent.md`: + +```markdown +# Set up your coding agent + +HyperFormula ships an official Claude skill and machine-readable docs so your AI coding agent can scaffold, configure, and debug HyperFormula correctly. Pick your tool below, or use the interactive wizard. + + + +## Claude Code + +Install the official skill from the plugin marketplace: + +\``` +/plugin marketplace add handsontable/handsontable-skills +/plugin install handsontable-skills@handsontable-skills +\``` + +Claude Code loads the `hyperformula` skill automatically based on what you ask. + +## Cursor, Copilot & other agents + +These tools don't yet support the Claude skill format. Point your agent at the machine-readable docs instead: + +- **Full corpus:** [`/docs/llms-full.txt`](/llms-full.txt) — the entire documentation in one LLM-friendly file. +- **Per-page Markdown:** append `.md` to any docs URL, or use the **Copy Markdown** button on any page. + +For agents that read a rules file (e.g. Cursor's `AGENTS.md`), add a line pointing at the corpus URL so the agent fetches authoritative docs on demand. + +## Manual install (any Claude Code setup) + +\```bash +git clone https://github.com/handsontable/handsontable-skills.git +cp -r handsontable-skills/skills/hyperformula ~/.claude/skills/ +\``` + +## Resources + +- [Official skill repository](https://github.com/handsontable/handsontable-skills) +- [`llms-full.txt`](/llms-full.txt) +- [API reference](/api/) +``` + +(Note: the `\``` fences above are escaped only in this plan; write real triple-backtick fences in the file.) + +- [ ] **Step 2: Verify links resolve under base** + +Run: `grep -n "llms-full.txt\|handsontable-skills" docs/guide/setup-coding-agent.md` +Expected: links present. `$withBase` is applied by VuePress to root-absolute links like `/llms-full.txt` automatically in rendered output. + +- [ ] **Step 3: Commit** + +```bash +git add docs/guide/setup-coding-agent.md +git commit -m "docs: add Set up your coding agent guide page" +``` + +--- + +## Task 4: C4 — `CodingAgentWizard.vue` interactive component + +**Files:** +- Create: `docs/.vuepress/components/CodingAgentWizard.vue` + +Embedded in the Task 3 page via ``. Local Vue state only. Content per IDE mirrors the verified commands from Task 3 — no invented commands. + +- [ ] **Step 1: Create the component** + +Create `docs/.vuepress/components/CodingAgentWizard.vue`: + +```vue + + + + + +``` + +- [ ] **Step 2: Verify wizard snippets match Task 3 verified commands** + +Run: `grep -c "handsontable/handsontable-skills" docs/.vuepress/components/CodingAgentWizard.vue` +Expected: `>= 2` (Claude Code marketplace + Other/API reference). Confirm NO command absent from the published skill README appears. + +- [ ] **Step 3: Commit** + +```bash +git add docs/.vuepress/components/CodingAgentWizard.vue +git commit -m "feat(docs): add interactive coding-agent setup wizard" +``` + +--- + +## Task 5: C5 — `llms.txt` + `robots.txt` + +**Files:** +- Create: `docs/.vuepress/public/llms.txt` +- Modify: `docs/.vuepress/public/robots.txt` + +Files in `docs/.vuepress/public/` are copied verbatim to the dist root (under base `/docs/`). So `llms.txt` lands at `/docs/llms.txt`. + +- [ ] **Step 1: Create `llms.txt`** + +Create `docs/.vuepress/public/llms.txt`: + +``` +# HyperFormula + +> HyperFormula is an open-source, high-performance calculation engine for spreadsheets and web applications. + +## Docs + +- Guide: https://hyperformula.handsontable.com/docs/guide/ +- API reference: https://hyperformula.handsontable.com/docs/api/ +- Full corpus: https://hyperformula.handsontable.com/docs/llms-full.txt +- Official Claude skill: https://github.com/handsontable/handsontable-skills +``` + +- [ ] **Step 2: Append corpus pointer to `robots.txt`** + +Current `docs/.vuepress/public/robots.txt`: + +``` +User-agent: * +Allow: / + +Sitemap: https://hyperformula.handsontable.com/sitemap.xml +``` + +Add at the end: + +``` +# AI/LLM agent index +# Full documentation corpus: https://hyperformula.handsontable.com/docs/llms-full.txt +``` + +- [ ] **Step 3: Verify files** + +Run: `cat docs/.vuepress/public/llms.txt && echo "---" && tail -3 docs/.vuepress/public/robots.txt` +Expected: both show the corpus URL `.../docs/llms-full.txt`. + +- [ ] **Step 4: Commit** + +```bash +git add docs/.vuepress/public/llms.txt docs/.vuepress/public/robots.txt +git commit -m "docs: add llms.txt and link llms-full.txt from robots.txt" +``` + +--- + +## Task 6: Integration — wire into `config.js` and verify build + +**Files:** +- Modify: `docs/.vuepress/config.js` (require plugin, register in `plugins`, add `globalUIComponents`, add sidebar entry) + +Runs AFTER Tasks 1–5 are merged. + +- [ ] **Step 1: Require the plugin at top of `config.js`** + +Add after the existing `includeCodeSnippet` require (around line 7): + +```js +const mdCompanions = require('./plugins/md-companions'); +``` + +- [ ] **Step 2: Register the plugin in the `plugins` array** + +In the `plugins: [` array (after the sitemap entry), add: + +```js +[mdCompanions, { hostname: DOCS_HOSTNAME }], +``` + +- [ ] **Step 3: Register the global button component** + +Change line 34 from `globalUIComponents: [],` to: + +```js +globalUIComponents: ['CopyMarkdownButton'], +``` + +- [ ] **Step 4: Add sidebar entry for the new page** + +In `themeConfig.sidebar['/']`, inside the `Getting started` group's `children` array, add after the `license-key` entry: + +```js +['/guide/setup-coding-agent', 'Set up your coding agent'], +``` + +- [ ] **Step 5: Run the full docs build** + +Run: `npm run docs:build 2>&1 | tail -20` +Expected: build succeeds, "Generated static files in ...". + +- [ ] **Step 6: Verify companion + corpus output exists** + +Run: +```bash +ls docs/.vuepress/dist/docs/guide/basic-usage.md docs/.vuepress/dist/docs/llms-full.txt docs/.vuepress/dist/docs/llms.txt +echo "--- companion is clean markdown (no \nText after')) + .toBe('Text before\nText after') + }) + + it('removes self-closing Vue components', () => { + expect(stripVuePressSyntax('Intro\n\nOutro')) + .toBe('Intro\nOutro') + }) + + it('removes the [[toc]] marker', () => { + expect(stripVuePressSyntax('# Title\n[[toc]]\nBody')) + .toBe('# Title\nBody') + }) + + it('preserves plain markdown content', () => { + expect(stripVuePressSyntax('# H\n\n`code`\n\n[link](/guide/x)\n\n| a | b |\n|---|---|')) + .toBe('# H\n\n`code`\n\n[link](/guide/x)\n\n| a | b |\n|---|---|') + }) + + it('strips :::example (live demo) containers entirely', () => { + expect(stripVuePressSyntax('## Demo\n\n::: example #ex1 --html 1\n@[code](example.html)\n:::\n\nOutro')) + .toBe('## Demo\n\nOutro') + }) + + it('does not let an inner 3-backtick fence close a 4-backtick outer fence', () => { + expect(stripVuePressSyntax('````markdown\n```js\ncode\n```\n````')) + .toBe('````markdown\n```js\ncode\n```\n````') + }) +})