diff --git a/.agents/skills/inth-docs/SKILL.md b/.agents/skills/inth-docs/SKILL.md
index c1ca11f..028a5de 100644
--- a/.agents/skills/inth-docs/SKILL.md
+++ b/.agents/skills/inth-docs/SKILL.md
@@ -22,10 +22,10 @@ Use the packaged agent docs as reference data. Prefer the installed package copy
Start with `docs/llms.txt`, then open the smallest matching topic page:
-- `components.md` for `mdxComponents`, `PackageCommandTabs`, `TypeTable`, and MDX rendering.
-- `convert.md` for `convertMdxFile`, `convertSingleMdxFile`, and `convertAllMdx`.
+- `components.md` for `mdxComponents`, `CommandTabs`, `TypeTable`, `ExtractedTypeTable`, and MDX rendering.
+- `convert.md` for `convertMdxToMarkdown`, `writeMdxFileAsMarkdown`, and `convertAllMdx`.
- `remark.md` for `defaultRemarkPlugins`, `remarkInclude`, and plugin ordering.
-- `llm.md` for `generateLLMSummaries`, `generateLLMFullFiles`, and topic design.
+- `llm.md` for `generateLlmsTxt`, `generateLLMFullContextFiles`, and topic design.
- `lint.md` for `lintDocs`, schema overrides, and `inth-docs-lint`.
Open `docs/llms-full.txt` only when the summary page is insufficient.
diff --git a/README.md b/README.md
index 3201894..ff36ab3 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,17 @@
# @inth/docs
-Shared MDX-to-markdown tooling for Inth docs properties.
+Shared docs tooling for Inth docs projects: React MDX rendering, MDX-to-markdown conversion, LLM bundles, validation, and static search.
-`@inth/docs` is split into five main surfaces:
+`@inth/docs` is split into focused public entry points:
- `@inth/docs`: React MDX component adapters via `mdxComponents`
- `@inth/docs/remark`: remark plugins plus `defaultRemarkPlugins`
- `@inth/docs/convert`: MDX-to-markdown conversion APIs
- `@inth/docs/llm`: `llms.txt` and topic-scoped full-context generation
+- `@inth/docs/search`: edge-safe search runtime, content readers, guards, and rate limiter helpers
+- `@inth/docs/search/node`: Node-only search index generation
+- `@inth/docs/search/ai`: AI SDK answer streaming helper
+- `@inth/docs/search/bash`: optional bash-tool docs inspection adapter
- `@inth/docs/lint`: docs validation and the `inth-docs-lint` CLI
## Install
@@ -34,7 +38,7 @@ The repo includes a canonical consumer demo at `apps/docs-smoke`.
- Renders real `.mdx` fixture files through the package's exported `mdxComponents`.
- Uses TanStack Start for SSR and hydration coverage.
-- Shows extracted `AutoTypeTable` output while keeping pipeline fixtures in the validation path.
+- Shows extracted `ExtractedTypeTable` output while keeping pipeline fixtures in the validation path.
Local workflow:
@@ -54,9 +58,24 @@ bun run --filter docs-smoke test:e2e
Validation layers:
- Package unit tests in `packages/docs/src/**/*.test.ts*` cover component semantics and pure library behavior.
-- Pipeline fixtures in `apps/docs-smoke/scripts` and `apps/docs-smoke/content` cover MDX conversion, LLM generation, and `AutoTypeTable`.
+- Pipeline fixtures in `apps/docs-smoke/scripts` and `apps/docs-smoke/content` cover MDX conversion, LLM generation, and `ExtractedTypeTable`.
- The TanStack Start demo app in `apps/docs-smoke/src` covers real browser rendering and hydration.
+## Where This Fits
+
+`@inth/docs` is not a hosted docs platform or a complete docs-site framework. Use tools such as Mintlify, Fumadocs, or Starlight when the primary job is shipping a polished docs website quickly.
+
+Use this package when the primary job is shared docs infrastructure: MDX rendering adapters, MDX-to-markdown conversion, LLM bundles, linting, static search artifacts, answer helpers, and agent-facing docs output that can feed multiple apps and tools.
+
+## Wiring It Into An App
+
+In a c15t-style repo with a top-level `docs/` directory, wire `@inth/docs` into the docs app and docs scripts:
+
+- The docs app imports `mdxComponents` only if it renders MDX directly.
+- A conversion script runs `convertAllMdx({ srcDir: process.cwd(), outDir: "public" })`.
+- LLM and search scripts read the converted markdown under `public/docs/`.
+- Product code does not import `@inth/docs` unless it also renders docs pages.
+
### Convert MDX to markdown
```ts
@@ -73,7 +92,7 @@ await convertAllMdx({
### Generate agent-facing docs bundles
```ts
-import { generateLLMFullFiles, generateLLMSummaries } from "@inth/docs/llm";
+import { generateLLMFullContextFiles, generateLlmsTxt } from "@inth/docs/llm";
```
Run the packaged agent-doc generator locally with:
@@ -84,6 +103,19 @@ INTH_DOCS_AGENT_BASE_URL=https://docs.example.com/@inth/docs bun run docs:agent
This writes a bundled reference set into `packages/docs/agent-docs/`.
+### Generate a static search index
+
+```ts
+import { generateDocsSearchFiles } from "@inth/docs/search/node";
+
+await generateDocsSearchFiles({
+ outDir: "public",
+ baseUrl: "https://docs.example.com",
+});
+```
+
+At runtime, query the generated JSON with `@inth/docs/search`. Add `@inth/docs/search/ai` only when a user explicitly asks for a source-grounded answer.
+
## Agent Docs
The package now ships a small, topic-scoped agent reference bundle:
@@ -93,6 +125,7 @@ The package now ships a small, topic-scoped agent reference bundle:
- `agent-docs/docs/convert.md`
- `agent-docs/docs/remark.md`
- `agent-docs/docs/llm.md`
+- `agent-docs/docs/search.md`
- `agent-docs/docs/lint.md`
Set `INTH_DOCS_AGENT_BASE_URL` to the hosted docs base before generating publishable `llms*.txt` files.
diff --git a/apps/docs-smoke/.gitignore b/apps/docs-smoke/.gitignore
index 015841b..b7607d9 100644
--- a/apps/docs-smoke/.gitignore
+++ b/apps/docs-smoke/.gitignore
@@ -1,4 +1,6 @@
+.output/
content-fixtures/
public/
public-real/
public-real2/
+test-results/
diff --git a/apps/docs-smoke/content/docs/guides/components-fixture.mdx b/apps/docs-smoke/content/docs/guides/components-fixture.mdx
index bb01b28..3fac356 100644
--- a/apps/docs-smoke/content/docs/guides/components-fixture.mdx
+++ b/apps/docs-smoke/content/docs/guides/components-fixture.mdx
@@ -1,70 +1,58 @@
---
-title: "Components Fixture"
-description: "Render the runtime-facing adapters from @inth/docs in one browser route."
+title: "Runtime Components"
+description: "Render the browser-facing @inth/docs adapters through authored MDX."
---
-# Components Fixture
+# Runtime Components
- This page intentionally exercises the browser-facing adapters without replacing them with shadcn variants.
+ This page exercises the exported MDX adapters without replacing them with app-local variants.
-## Authoring Example
+## Authoring Contract
```mdx
- Render the exported adapters through your shared `mdxComponents` map.
+ Render exported adapters through your shared `mdxComponents` map.
-
+
+
+Tabs hydrate in the browser.
- Use `TypeTable` when the type data already exists in MDX.
+ Use `TypeTable` when type data already exists in MDX. B[mdxComponents]
B --> C[Rendered route]
`} />
-
-
-
-
```
-`AutoTypeTable` still needs extracted `type` data from the route or conversion pipeline. This demo renders that extracted output on `/docs`.
+## Navigation Cards
-
-
+
+
+## Browser Flow
+
- Start with semantic components such as `Callout`, `Tabs`, `Cards`, and `TypeTable`.
+ Use semantic components such as `Callout`, `Tabs`, `Cards`, `Steps`, `CommandTabs`, and `TypeTable`.
-
+
Import the `.mdx` file directly and provide `mdxComponents` through the shared runtime map.
-
- Keep `AutoTypeTable` in pipeline coverage where source extraction actually happens.
+
+ Keep `ExtractedTypeTable` coverage in the conversion pipeline where source extraction has a stable file-system base path.
-
+
@@ -74,25 +62,29 @@ description: "Render the runtime-facing adapters from @inth/docs in one browser
`TypeTable` is safe to render live because all of its data is already present in the MDX payload.
- `AutoTypeTable` is not shown live here because extraction depends on a stable build-time file system base path.
+ `ExtractedTypeTable` is rendered on `/docs` with extracted type data and verified in `content/docs/guides/extracted-type-table-fixture.mdx`.
B[mdxComponents]
+ A[Authored MDX] --> B[mdxComponents]
B --> C[TanStack Start route]
C --> D[Playwright coverage]
`} />
",
+ type: "Partial>",
description: "Explicit per-manager overrides when templates are not enough.",
},
defaultManager: {
@@ -103,6 +95,6 @@ description: "Render the runtime-facing adapters from @inth/docs in one browser
}}
/>
-
- `AutoTypeTable` is verified by the markdown conversion fixture at `content/docs/guides/auto-type-table-fixture.mdx` and by the dedicated pipeline test script.
+
+ `ExtractedTypeTable` needs extracted type data. The live extracted output appears on `/docs`, and the pipeline fixture verifies markdown output from `PipelineExampleOptions`.
diff --git a/apps/docs-smoke/content/docs/guides/auto-type-table-fixture.mdx b/apps/docs-smoke/content/docs/guides/extracted-type-table-fixture.mdx
similarity index 67%
rename from apps/docs-smoke/content/docs/guides/auto-type-table-fixture.mdx
rename to apps/docs-smoke/content/docs/guides/extracted-type-table-fixture.mdx
index ee03c2d..c251f69 100644
--- a/apps/docs-smoke/content/docs/guides/auto-type-table-fixture.mdx
+++ b/apps/docs-smoke/content/docs/guides/extracted-type-table-fixture.mdx
@@ -1,11 +1,11 @@
---
-title: "AutoTypeTable Fixture"
+title: "ExtractedTypeTable Fixture"
description: "Pipeline-only fixture for type extraction coverage."
---
-# AutoTypeTable Fixture
+# ExtractedTypeTable Fixture
-
diff --git a/apps/docs-smoke/content/docs/guides/quickstart.mdx b/apps/docs-smoke/content/docs/guides/quickstart.mdx
index c458b52..53ae3f8 100644
--- a/apps/docs-smoke/content/docs/guides/quickstart.mdx
+++ b/apps/docs-smoke/content/docs/guides/quickstart.mdx
@@ -1,23 +1,252 @@
---
title: "Quickstart"
-description: "Install and run your first command."
+description: "Wire @inth/docs into a docs app and docs pipeline."
---
# Quickstart
-
- Install the package.
- Import `convertAllMdx` from `@inth/docs/convert`.
- Run `bun run pipeline:build`.
-
-
-
-
-
-
- Basic usage is one call to `convertAllMdx({ srcDir, outDir, remarkPlugins })`.
-
-
- Pass custom remark plugins alongside `defaultRemarkPlugins` for extensions and set a stable `basePath` when `AutoTypeTable` needs to resolve project files.
-
-
+`@inth/docs` is docs infrastructure. In a product repo such as c15t, you wire it into the docs app and docs scripts, not into the product runtime unless that runtime renders docs pages.
+
+## What You Are Wiring
+
+
+
+For a c15t-style repo with docs under `docs/`, the wiring usually looks like this:
+
+```txt
+repo/
+ docs/
+ meta.json
+ frameworks/react/quickstart.mdx
+ self-host/quickstart.mdx
+ scripts/
+ docs-convert.ts
+ docs-llm.ts
+ docs-search.ts
+ public/
+ docs/
+ frameworks/react/quickstart.md
+ search-index.json
+ search-content.json
+ llms.txt
+```
+
+If your docs live under `content/docs/` instead, set `srcDir: "content"` and the generated markdown still lands under `{outDir}/docs`.
+
+## 1. Install
+
+
+
+## 2. Render MDX In Your Docs App
+
+Use this only in the app that renders documentation pages.
+
+```tsx
+import { mdxComponents } from "@inth/docs";
+
+export const components = {
+ ...mdxComponents,
+ // Add or override project-specific MDX components here.
+ // Example: Icon, APIExample, FrameworkTabs, or branded Callout.
+};
+```
+
+`@inth/docs` does not own your routing, sidebar, layout, hosting, or framework. A Next.js, TanStack Start, Vite, Fumadocs, or Astro-backed docs app can still own those pieces.
+
+## 3. Convert Authored MDX To Markdown
+
+Create a docs conversion script in the consuming repo.
+
+```ts
+// scripts/docs-convert.ts
+import { rm } from "node:fs/promises";
+import { join } from "node:path";
+import { convertAllMdx } from "@inth/docs/convert";
+import { defaultRemarkPlugins, remarkInclude } from "@inth/docs/remark";
+
+const root = process.cwd();
+const outDir = join(root, "public");
+
+await rm(outDir, { recursive: true, force: true });
+await convertAllMdx({
+ // c15t-style repos have a top-level docs/ directory, so use the repo root.
+ srcDir: root,
+ outDir,
+ remarkPlugins: [remarkInclude, ...defaultRemarkPlugins],
+ enrichFrontmatterFromGit: true,
+});
+```
+
+Run it with:
+
+```bash
+bun run scripts/docs-convert.ts
+```
+
+The output preserves the source structure:
+
+```txt
+docs/frameworks/react/quickstart.mdx
+public/docs/frameworks/react/quickstart.md
+```
+
+## 4. Generate Agent-Facing Files
+
+Run this after conversion.
+
+```ts
+// scripts/docs-llm.ts
+import { join } from "node:path";
+import {
+ generateLLMFullContextFiles,
+ generateLlmsTxt,
+} from "@inth/docs/llm";
+
+const root = process.cwd();
+const outDir = join(root, "public");
+
+await generateLlmsTxt({
+ srcDir: root,
+ outDir,
+ baseUrl: "https://c15t.com",
+ product: {
+ name: "c15t",
+ summary: "Open source consent and privacy platform.",
+ bestStartingPoints: [{ urlPath: "/docs/frameworks" }],
+ agentGuidance:
+ "Start with the framework guide that matches the user's stack.",
+ },
+ docsSections: [
+ {
+ title: "Frameworks",
+ links: [{ urlPath: "/docs/frameworks" }],
+ },
+ {
+ title: "Self-host",
+ links: [{ urlPath: "/docs/self-host/quickstart" }],
+ },
+ ],
+});
+
+await generateLLMFullContextFiles({
+ outDir,
+ baseUrl: "https://c15t.com",
+ product: { name: "c15t" },
+ topics: [
+ {
+ slug: "frameworks",
+ title: "Frameworks",
+ description: "Framework integrations. Pick the matching stack.",
+ topics: [
+ {
+ slug: "react",
+ title: "React",
+ description: "React integration docs.",
+ includePrefixes: ["frameworks/react/"],
+ },
+ {
+ slug: "next",
+ title: "Next.js",
+ description: "Next.js integration docs.",
+ includePrefixes: ["frameworks/next/"],
+ },
+ ],
+ },
+ {
+ slug: "self-host",
+ title: "Self-host",
+ description: "Self-host c15t in your infrastructure.",
+ includePrefixes: ["self-host/"],
+ },
+ ],
+});
+```
+
+This writes files such as:
+
+```txt
+public/llms.txt
+public/docs/llms.txt
+public/docs/llms-full/frameworks/react.txt
+public/docs/llms-full/frameworks/next.txt
+```
+
+## 5. Generate Search Data
+
+Run this after conversion too.
+
+```ts
+// scripts/docs-search.ts
+import { generateDocsSearchFiles } from "@inth/docs/search/node";
+
+await generateDocsSearchFiles({
+ outDir: "public",
+ baseUrl: "https://c15t.com",
+});
+```
+
+The generator reads `public/docs/**/*.md` and writes:
+
+```txt
+public/docs/search-index.json
+public/docs/search-content.json
+```
+
+## 6. Query Search In Your App
+
+Import the generated JSON wherever your docs app handles search.
+
+```ts
+import {
+ searchDocs,
+ type DocsSearchContentStore,
+ type DocsSearchIndex,
+} from "@inth/docs/search";
+import contentJson from "../public/docs/search-content.json";
+import indexJson from "../public/docs/search-index.json";
+
+const index = indexJson as DocsSearchIndex;
+const content = contentJson as DocsSearchContentStore;
+
+export function search(query: string) {
+ return searchDocs(index, query, { content });
+}
+```
+
+If the app also supports AI answers, keep model calls behind an explicit user action and use `@inth/docs/search/ai` from a server route.
+
+## 7. Add Package Scripts
+
+```json
+{
+ "scripts": {
+ "docs:convert": "bun run scripts/docs-convert.ts",
+ "docs:llm": "bun run scripts/docs-llm.ts",
+ "docs:search": "bun run scripts/docs-search.ts",
+ "docs:build": "bun run docs:convert && bun run docs:llm && bun run docs:search"
+ }
+}
+```
+
+That is the minimum wiring. Rendering is optional if another framework owns the docs UI. Conversion, LLM output, and search generation are the pieces that make the docs useful outside the website.
diff --git a/apps/docs-smoke/content/docs/index.mdx b/apps/docs-smoke/content/docs/index.mdx
index 1bb2b83..0f6801a 100644
--- a/apps/docs-smoke/content/docs/index.mdx
+++ b/apps/docs-smoke/content/docs/index.mdx
@@ -1,61 +1,100 @@
---
title: "@inth/docs"
-description: "Package docs for runtime adapters, remark plugins, conversion, LLM output, and linting."
+description: "Developer reference for rendering MDX, converting docs, generating LLM bundles, linting content, and serving search."
---
# @inth/docs
-`@inth/docs` has five package surfaces:
+`@inth/docs` is the shared package for docs rendering, docs pipelines, LLM-friendly output, validation, and local search. Use the smallest entry point that matches where your code runs.
+
+## Package Surfaces
-## Install
+## Common Implementation Paths
-
+
+
+ Import `mdxComponents` from `@inth/docs`, spread it into your MDX provider, and override individual entries only when your app needs custom styling.
+
+
+ Use `@inth/docs/convert` with `@inth/docs/remark` to flatten authored MDX into markdown that works in LLM bundles and search indexes.
+
+
+ Generate static search JSON with `@inth/docs/search/node`, query it with `@inth/docs/search`, and add `@inth/docs/search/ai` only for explicit answer requests.
+
+
-## Runtime integration
+## Methodology
-Use the root package when you want to render authored MDX in a React app.
+`@inth/docs` is not trying to replace hosted docs platforms or full docs-site frameworks. It is a portable toolkit for the docs pipeline around your site.
+
+Use Mintlify, Fumadocs, or Starlight when the primary job is to ship a complete docs website quickly. Use `@inth/docs` when the primary job is to keep docs content useful across the website, CI, generated markdown, local search, AI answer routes, and coding-agent workflows.
+
+| Tool | Primary job | Best fit |
+| --- | --- | --- |
+| [Mintlify](https://mintlify.com/) | Managed developer documentation platform with hosted docs, API docs, and AI-oriented product features. | Teams that want a polished docs product without owning the whole site pipeline. |
+| [Fumadocs](https://fumadocs.dev/) | Documentation framework and toolchain for React docs sites, especially Next.js. | Teams that want framework-level routing, navigation, UI, content adapters, search, and OpenAPI support. |
+| [Starlight](https://starlight.astro.build/) | Full-featured documentation framework built on Astro. | Teams already using Astro or wanting an Astro-native docs site. |
+| `@inth/docs` | Shared docs infrastructure package: React MDX adapters, MDX-to-markdown conversion, LLM bundles, linting, static search, and answer helpers. | Teams that want to own the app shell while reusing one docs pipeline across sites, agents, search APIs, and internal tools. |
+
+These tools are not mutually exclusive. A site framework can own the public docs experience while `@inth/docs` owns conversion, validation, search artifacts, and agent-facing bundles.
+
+## Render MDX
```tsx
import { mdxComponents } from "@inth/docs";
-const components = {
+export const components = {
...mdxComponents,
};
```
-The root export is the runtime contract. It is not tied to shadcn, TanStack Start, or any specific docs shell.
-
-## Convert MDX
+The root export is a runtime contract. It is not tied to TanStack Start, shadcn, or this demo shell.
-Use the conversion and remark packages when you want markdown output instead of browser rendering.
+## Convert And Generate
```ts
import { convertAllMdx } from "@inth/docs/convert";
@@ -68,31 +107,43 @@ await convertAllMdx({
});
```
-## What to open in this app
+After conversion, use `@inth/docs/llm` to write `llms.txt` and topic-scoped full-context files, or `@inth/docs/search/node` to generate a compact search index.
+
+## What To Open In This App
+
+
-## Validation layers
+## Validation Layers
Cover semantic HTML and safe runtime behavior in `packages/docs/src/**/*.test.ts*`.
- Cover conversion, extraction, and LLM output in `apps/docs-smoke/scripts` and `apps/docs-smoke/content`.
+ Cover conversion, type extraction, LLM output, and search generation in `apps/docs-smoke/scripts` and `apps/docs-smoke/content`.
- Cover hydration and interactive adapters in the Playwright suite for this app.
+ Cover SSR, hydration, keyboard behavior, link safety, search APIs, and recipe interactions in Playwright.
diff --git a/apps/docs-smoke/content/docs/meta.json b/apps/docs-smoke/content/docs/meta.json
index b736ac2..c5ca28b 100644
--- a/apps/docs-smoke/content/docs/meta.json
+++ b/apps/docs-smoke/content/docs/meta.json
@@ -2,8 +2,9 @@
"title": "Docs",
"pages": [
"index",
+ "search",
"guides/quickstart",
"guides/components-fixture",
- "guides/auto-type-table-fixture"
+ "guides/extracted-type-table-fixture"
]
}
diff --git a/apps/docs-smoke/content/docs/search.mdx b/apps/docs-smoke/content/docs/search.mdx
new file mode 100644
index 0000000..dcf28e2
--- /dev/null
+++ b/apps/docs-smoke/content/docs/search.mdx
@@ -0,0 +1,94 @@
+---
+title: "Search APIs"
+description: "Generate static docs search data, query it at runtime, and stream source-grounded answers."
+---
+
+# Search APIs
+
+`@inth/docs` ships headless search primitives. Your app owns the UI; the package owns index generation, local ranking, source reads, answer context, and request guards.
+
+
+ Open [/search](/search) to test the generated index. Typing calls local search only. The `Ask` button is the only action that can call the model.
+
+
+## Build The Index
+
+Use the Node-only entry point after MDX has been converted to markdown.
+
+```ts
+import { generateDocsSearchFiles } from "@inth/docs/search/node";
+
+await generateDocsSearchFiles({
+ outDir: "public",
+ baseUrl: "https://docs.example.com",
+});
+```
+
+The generator writes `docs/search-index.json` for compact metadata and `docs/search-content.json` for answer-source text.
+
+## Runtime Search
+
+Use the edge-safe runtime with generated JSON.
+
+```ts
+import {
+ searchDocs,
+ type DocsSearchContentStore,
+ type DocsSearchIndex,
+} from "@inth/docs/search";
+import contentJson from "./generated/docs-search-content.json";
+import indexJson from "./generated/docs-search-index.json";
+
+const index = indexJson as DocsSearchIndex;
+const content = contentJson as DocsSearchContentStore;
+
+const results = searchDocs(index, "package tabs", { content });
+```
+
+Results include heading paths, excerpts, `urlWithHash`, and `absoluteUrlWithHash` so a docs UI can link directly to the matched section.
+
+## Source-Grounded Answers
+
+Use `@inth/docs/search/ai` only when the user explicitly asks for an answer.
+
+```ts
+import { streamDocsAnswer } from "@inth/docs/search/ai";
+
+const { response, sources } = streamDocsAnswer({
+ index,
+ content,
+ query,
+ model: process.env.DOCS_SEARCH_MODEL ?? "moonshotai/kimi-k2.6",
+ productName: "@inth/docs",
+});
+```
+
+The prompt includes retrieved docs context, asks for citations, treats docs text as untrusted reference content, and tells the model to say when the retrieved context is not enough.
+
+## Abuse Protection
+
+
+
+ Debounced typing should call only a local search route such as `/api/docs/search`.
+
+
+ Keep model calls behind a clear button such as `Ask`.
+
+
+ Use `validateDocsQuery`, `readJsonWithLimit`, `getClientIdentifier`, and a `RateLimiter` implementation around public routes.
+
+
+
+The demo uses an in-memory limiter for local smoke coverage. Production apps should adapt the `RateLimiter` interface to a shared store.
+
+## Bash Adapter
+
+Use `@inth/docs/search/bash` when an AI SDK agent should inspect docs with safe shell commands.
+
+```ts
+import { createDocsBashTool } from "@inth/docs/search/bash";
+
+const { tools, instructions } = await createDocsBashTool(index, content);
+```
+
+The adapter creates a read-only `/docs` filesystem for `just-bash` and wraps it with `bash-tool`. Agents can use commands such as `ls`, `cat`, `find`, `grep`, and `rg`; network commands and writes are disabled by default.
diff --git a/apps/docs-smoke/package.json b/apps/docs-smoke/package.json
index 76450e2..d81304e 100644
--- a/apps/docs-smoke/package.json
+++ b/apps/docs-smoke/package.json
@@ -4,9 +4,9 @@
"private": true,
"type": "module",
"scripts": {
- "dev": "bun run --filter @inth/docs build && vite dev --host 0.0.0.0 --port 3000",
+ "dev": "bun run --filter @inth/docs build && portless run vite dev",
"build": "bun run --filter @inth/docs build && vite build",
- "preview": "vite preview --host 0.0.0.0 --port 3000",
+ "preview": "portless run vite preview",
"check-types": "tsc --noEmit",
"test:e2e": "bun run --filter @inth/docs build && playwright test",
"convert": "bun run pipeline:convert",
@@ -16,7 +16,8 @@
"bench": "bun run pipeline:bench",
"pipeline:convert": "bun run scripts/mdx-convert.ts",
"pipeline:llm": "bun run scripts/llm-generate.ts",
- "pipeline:build": "bun run pipeline:convert && bun run pipeline:llm",
+ "pipeline:search": "bun run scripts/search-generate.ts",
+ "pipeline:build": "bun run pipeline:convert && bun run pipeline:llm && bun run pipeline:search",
"pipeline:test": "bun run scripts/test-pipeline.ts",
"pipeline:setup-real": "bun run scripts/setup-real-content.ts",
"pipeline:test-real": "bun run pipeline:setup-real && bun run scripts/test-real.ts",
@@ -33,11 +34,13 @@
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-router": "^1.167.4",
"@tanstack/react-start": "^1.166.15",
+ "ai": "^6.0.168",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"nitro": "3.0.260415-beta",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "streamdown": "^2.5.0",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0"
diff --git a/apps/docs-smoke/playwright.config.ts b/apps/docs-smoke/playwright.config.ts
index afe4b25..47e0f0e 100644
--- a/apps/docs-smoke/playwright.config.ts
+++ b/apps/docs-smoke/playwright.config.ts
@@ -1,18 +1,49 @@
+import { execFileSync } from "node:child_process";
import { defineConfig, devices } from "@playwright/test";
const isCI = Boolean(process.env.CI);
+const HTTPS_PROTOCOL = "https://";
+const HTTP_PROTOCOL = "http://";
+const DEFAULT_BASE_URL = "http://localhost:3000";
+
+function getDocsSmokeBaseUrl(): string {
+ const configuredBaseUrl = process.env.PLAYWRIGHT_BASE_URL?.trim();
+ if (configuredBaseUrl) {
+ return configuredBaseUrl;
+ }
+
+ let portlessUrl: string;
+ try {
+ portlessUrl = execFileSync("portless", ["get", "docs-smoke"], {
+ encoding: "utf8",
+ }).trim();
+ } catch {
+ process.stderr.write(
+ `Unable to resolve docs-smoke through portless. Falling back to ${DEFAULT_BASE_URL}. Set PLAYWRIGHT_BASE_URL to override this value.\n`
+ );
+ return DEFAULT_BASE_URL;
+ }
+
+ // Playwright drives the local Vite server over HTTP; portlessUrl can be HTTPS
+ // in the shell, which makes browser tests fail on local TLS.
+ return portlessUrl.startsWith(HTTPS_PROTOCOL)
+ ? `${HTTP_PROTOCOL}${portlessUrl.slice(HTTPS_PROTOCOL.length)}`
+ : portlessUrl;
+}
+
+const docsSmokeBaseUrl = getDocsSmokeBaseUrl();
export default defineConfig({
testDir: "./tests/e2e",
testMatch: /.*\.e2e\.ts/,
fullyParallel: true,
use: {
- baseURL: "http://127.0.0.1:3000",
+ baseURL: docsSmokeBaseUrl,
trace: "on-first-retry",
},
webServer: {
command: "bun run dev",
- port: 3000,
+ url: docsSmokeBaseUrl,
reuseExistingServer: !isCI,
},
projects: [
diff --git a/apps/docs-smoke/scripts/bench.ts b/apps/docs-smoke/scripts/bench.ts
index 5edd45d..53fb518 100644
--- a/apps/docs-smoke/scripts/bench.ts
+++ b/apps/docs-smoke/scripts/bench.ts
@@ -12,8 +12,8 @@ import { appendFile, readdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { convertAllMdx } from "../../../packages/docs/src/convert/index.ts";
import {
- generateLLMFullFiles,
- generateLLMSummaries,
+ generateLLMFullContextFiles,
+ generateLlmsTxt,
} from "../../../packages/docs/src/llm/index.ts";
import {
defaultRemarkPlugins,
@@ -88,7 +88,7 @@ async function bench(): Promise {
);
const llmMs = await timed(async () => {
- await generateLLMSummaries({
+ await generateLlmsTxt({
srcDir: SRC_DIR,
outDir: OUT_DIR,
baseUrl: "https://docs.example.com",
@@ -108,7 +108,7 @@ async function bench(): Promise {
},
],
});
- await generateLLMFullFiles({
+ await generateLLMFullContextFiles({
outDir: OUT_DIR,
baseUrl: "https://docs.example.com",
product: { name: "Bench SDK" },
diff --git a/apps/docs-smoke/scripts/llm-generate-real.ts b/apps/docs-smoke/scripts/llm-generate-real.ts
index 82ef857..435fbed 100644
--- a/apps/docs-smoke/scripts/llm-generate-real.ts
+++ b/apps/docs-smoke/scripts/llm-generate-real.ts
@@ -10,15 +10,15 @@
import { join } from "node:path";
import {
- generateLLMFullFiles,
- generateLLMSummaries,
+ generateLLMFullContextFiles,
+ generateLlmsTxt,
} from "../../../packages/docs/src/llm/index.ts";
const FIXTURE_DIR = join(process.cwd(), "content-fixtures", "c15t");
const SRC_DIR = FIXTURE_DIR;
const OUT_DIR = join(process.cwd(), "public-real2");
-await generateLLMSummaries({
+await generateLlmsTxt({
srcDir: SRC_DIR,
outDir: OUT_DIR,
baseUrl: "https://c15t.com",
@@ -52,7 +52,7 @@ await generateLLMSummaries({
],
});
-await generateLLMFullFiles({
+await generateLLMFullContextFiles({
outDir: OUT_DIR,
baseUrl: "https://c15t.com",
product: { name: "c15t" },
diff --git a/apps/docs-smoke/scripts/llm-generate.ts b/apps/docs-smoke/scripts/llm-generate.ts
index e110625..1ddf647 100644
--- a/apps/docs-smoke/scripts/llm-generate.ts
+++ b/apps/docs-smoke/scripts/llm-generate.ts
@@ -5,18 +5,23 @@
import { join } from "node:path";
import {
- generateLLMFullFiles,
- generateLLMSummaries,
+ generateLLMFullContextFiles,
+ generateLlmsTxt,
} from "../../../packages/docs/src/llm/index.ts";
const scriptsRoot = process.cwd();
const srcDir = join(scriptsRoot, "content");
const outDir = join(scriptsRoot, "public");
+const baseUrl =
+ process.env.DOCS_SMOKE_BASE_URL?.trim() ||
+ process.env.BASE_URL?.trim() ||
+ process.env.PORTLESS_URL?.trim() ||
+ "https://docs.example.com";
-await generateLLMSummaries({
+await generateLlmsTxt({
srcDir,
outDir,
- baseUrl: "https://docs.example.com",
+ baseUrl,
product: {
name: "Smoke SDK",
summary: "Exercise the @inth/docs pipeline end-to-end.",
@@ -40,9 +45,9 @@ await generateLLMSummaries({
],
});
-await generateLLMFullFiles({
+await generateLLMFullContextFiles({
outDir,
- baseUrl: "https://docs.example.com",
+ baseUrl,
product: { name: "Smoke SDK" },
topics: [
{
diff --git a/apps/docs-smoke/scripts/mdx-convert.ts b/apps/docs-smoke/scripts/mdx-convert.ts
index 9659f91..0591b2a 100644
--- a/apps/docs-smoke/scripts/mdx-convert.ts
+++ b/apps/docs-smoke/scripts/mdx-convert.ts
@@ -9,7 +9,7 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
convertAllMdx,
- type MdxToMarkdownConfig,
+ type MdxToMarkdownOptions,
} from "../../../packages/docs/src/convert/index.ts";
import {
defaultRemarkPlugins,
@@ -19,12 +19,13 @@ import {
const scriptsRoot = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(scriptsRoot, "..", "..");
-const srcDir = join(scriptsRoot, "content");
-const outDir = join(scriptsRoot, "public");
+const appRoot = join(scriptsRoot, "..");
+const srcDir = join(appRoot, "content");
+const outDir = join(appRoot, "public");
const typeTableRemarkPlugin: NonNullable<
- MdxToMarkdownConfig["remarkPlugins"]
+ MdxToMarkdownOptions["remarkPlugins"]
>[number] = [remarkTypeTableToMarkdown, { basePath: repoRoot }];
-const remarkPlugins: NonNullable = [
+const remarkPlugins: NonNullable = [
remarkInclude,
...defaultRemarkPlugins.filter(
(plugin) => plugin !== remarkTypeTableToMarkdown
diff --git a/apps/docs-smoke/scripts/search-generate.ts b/apps/docs-smoke/scripts/search-generate.ts
new file mode 100644
index 0000000..2354a8c
--- /dev/null
+++ b/apps/docs-smoke/scripts/search-generate.ts
@@ -0,0 +1,32 @@
+#!/usr/bin/env bun
+/**
+ * Generates a static docs search index from converted markdown.
+ */
+
+import { copyFile, mkdir } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { generateDocsSearchFiles } from "../../../packages/docs/src/search/node-index.ts";
+
+const scriptsRoot = dirname(fileURLToPath(import.meta.url));
+const appRoot = join(scriptsRoot, "..");
+const outDir = join(appRoot, "public");
+const generatedDir = join(appRoot, "src", "generated");
+const generatedIndexPath = join(generatedDir, "docs-search-index.json");
+const generatedContentPath = join(generatedDir, "docs-search-content.json");
+
+const result = await generateDocsSearchFiles({
+ outDir,
+ baseUrl: "https://docs.example.com",
+});
+
+await mkdir(generatedDir, { recursive: true });
+await copyFile(result.outputPath, generatedIndexPath);
+if (!result.contentOutputPath) {
+ throw new Error("Search content output was not generated.");
+}
+await copyFile(result.contentOutputPath, generatedContentPath);
+
+process.stdout.write(
+ `Search index generated: ${result.docs} docs, ${result.chunks} chunks, ${result.terms} terms\n`
+);
diff --git a/apps/docs-smoke/scripts/test-pipeline.ts b/apps/docs-smoke/scripts/test-pipeline.ts
index 8c834a7..b7e65a1 100644
--- a/apps/docs-smoke/scripts/test-pipeline.ts
+++ b/apps/docs-smoke/scripts/test-pipeline.ts
@@ -1,7 +1,7 @@
#!/usr/bin/env bun
import { join } from "node:path";
-import { convertMdxFile } from "../../../packages/docs/src/convert/index.ts";
+import { convertMdxToMarkdown } from "../../../packages/docs/src/convert/index.ts";
import {
defaultRemarkPlugins,
remarkTypeTableToMarkdown,
@@ -14,9 +14,9 @@ const fixturePath = join(
"content",
"docs",
"guides",
- "auto-type-table-fixture.mdx"
+ "extracted-type-table-fixture.mdx"
);
-type RemarkPlugins = NonNullable[1]>;
+type RemarkPlugins = NonNullable[1]>;
const typeTableRemarkPlugin: RemarkPlugins[number] = [
remarkTypeTableToMarkdown,
@@ -29,7 +29,7 @@ const remarkPlugins: RemarkPlugins = [
typeTableRemarkPlugin,
];
-const result = await convertMdxFile(fixturePath, remarkPlugins);
+const result = await convertMdxToMarkdown(fixturePath, remarkPlugins);
if (
!(
@@ -40,7 +40,7 @@ if (
) {
process.stderr.write(result.markdown);
process.stderr.write(
- "\nFAIL: expected AutoTypeTable fixture to resolve PipelineExampleOptions into markdown rows.\n"
+ "\nFAIL: expected ExtractedTypeTable fixture to resolve PipelineExampleOptions into markdown rows.\n"
);
process.exit(1);
}
diff --git a/apps/docs-smoke/src/components/component-matrix.tsx b/apps/docs-smoke/src/components/component-matrix.tsx
index c150522..8da0676 100644
--- a/apps/docs-smoke/src/components/component-matrix.tsx
+++ b/apps/docs-smoke/src/components/component-matrix.tsx
@@ -1,17 +1,21 @@
-import { type ComponentCoverage, componentMatrix } from "@/lib/docs";
+import { componentMatrix, type SmokeCoverage } from "@/lib/docs";
function assertNever(value: never): never {
throw new Error(`Unhandled coverage variant: ${value}`);
}
-function coverageClassName(coverage: ComponentCoverage): string {
+function coverageClassName(coverage: SmokeCoverage): string {
switch (coverage) {
- case "interactive":
+ case "agent docs":
+ return "bg-accent-soft text-accent-strong";
+ case "browser hydration":
return "bg-foreground text-background";
- case "pipeline-only":
- return "border border-border bg-background text-muted-foreground";
- case "runtime":
+ case "pipeline conversion":
+ return "bg-warning-soft text-warning-strong";
+ case "runtime render":
return "bg-secondary text-foreground";
+ case "search/API":
+ return "bg-success-soft text-success-strong";
default:
return assertNever(coverage);
}
@@ -19,13 +23,13 @@ function coverageClassName(coverage: ComponentCoverage): string {
export function ComponentMatrix() {
return (
-
+
-
Component
+
Surface
Coverage
-
Notes
+
What it proves
diff --git a/apps/docs-smoke/src/components/docs-shell.tsx b/apps/docs-smoke/src/components/docs-shell.tsx
index f4d3ec8..9a4f8d7 100644
--- a/apps/docs-smoke/src/components/docs-shell.tsx
+++ b/apps/docs-smoke/src/components/docs-shell.tsx
@@ -2,7 +2,7 @@
import { Link, useRouterState } from "@tanstack/react-router";
import type { ReactNode } from "react";
-import { demoRoutes } from "@/lib/docs";
+import { navigationRoutes } from "@/lib/docs";
import { cn } from "@/lib/utils";
import { SiteHeader } from "./site-header";
@@ -14,11 +14,11 @@ export function DocsShell({ children }: { children: ReactNode }) {
return (
-
-
diff --git a/apps/docs-smoke/src/components/site-header.tsx b/apps/docs-smoke/src/components/site-header.tsx
index 22a762d..f148e71 100644
--- a/apps/docs-smoke/src/components/site-header.tsx
+++ b/apps/docs-smoke/src/components/site-header.tsx
@@ -1,7 +1,7 @@
"use client";
import { Link, useRouterState } from "@tanstack/react-router";
-import { demoRoutes } from "@/lib/docs";
+import { navigationRoutes } from "@/lib/docs";
import { cn } from "@/lib/utils";
export function SiteHeader() {
@@ -11,15 +11,18 @@ export function SiteHeader() {
return (
-