diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2828c98..763e380 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 + - uses: astral-sh/setup-uv@v4 with: version: "0.4.x" - uses: extractions/setup-just@v2 @@ -26,7 +26,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 + - uses: astral-sh/setup-uv@v4 with: version: "0.4.x" - uses: extractions/setup-just@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..784d891 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + release: + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write # For trusted publishing + + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v4 + with: + version: "0.4.x" + + - name: Build package + run: | + cd packages/clerk + uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: packages/clerk/dist/ diff --git a/.gitignore b/.gitignore index 8a9645f..cf8bbc7 100644 --- a/.gitignore +++ b/.gitignore @@ -221,3 +221,7 @@ tmp_mcp_030/ # Apple .DS_Store + +outputs/ +*.docx +*.doc \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 4b77778..678eef6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,9 @@ uv sync uv run clerk list # List reasoning kits uv run clerk run # Run a kit uv run clerk run --evaluate # Run with evaluation +uv run clerk run --dynamic-resource resource_1="text" # Inline dynamic resource +uv run clerk run --stdin resource_1 # Pipe dynamic resource +uv run clerk validate # Validate kit structure # Testing uv run pytest # Run all tests @@ -148,10 +151,28 @@ frontend/src/ ## Reasoning Kit Conventions - Resources: `resource_*.txt` or `resource_*.csv` +- Dynamic resources: `dynamic_resource_*.txt` (provided at runtime) - Instructions: `instruction_*.txt` (numbered: 1, 2, 3...) -- Placeholders: `{resource_N}` and `{workflow_N}` +- Tools: `tool_*.json` referencing the global registry (e.g., `{"tool_name": "read_url"}`) +- MCP servers: `mcp_servers.json` (kit-local override, merges with project-root config) +- Placeholders: `{resource_N}`, `{workflow_N}`, and `{tool_N}` - Evaluations: stored in `evaluations/*.json` +### MCP Server Config (`mcp_servers.json`) + +Supported transports: `stdio` (default), `sse`, `http`. + +```json +{ + "mcpServers": { + "opencaselaw": { + "transport": "sse", + "url": "https://mcp.opencaselaw.ch" + } + } +} +``` + ## Environment Setup ```bash diff --git a/Justfile b/Justfile index 6ef78d9..20cf6ce 100644 --- a/Justfile +++ b/Justfile @@ -43,6 +43,30 @@ dev-backend: dev-frontend: cd apps/website && npm run dev +# Sync documentation to website +sync-docs: + ./scripts/sync-docs.sh + +# Build website for production (includes docs sync) +build-website: sync-docs + cd apps/website && npm run build + +# Build package for distribution +build: + uv build packages/clerk + +# Publish to TestPyPI +publish-test: build + uv publish --index testpypi dist/openclerk-* + +# Publish to PyPI (requires credentials) +publish: build + uv publish dist/openclerk-* + +# Bump package version (e.g., just version patch) +version BUMP: + cd packages/clerk && uvx bump-my-version bump {{BUMP}} + # Clean build artifacts clean: find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true diff --git a/SKILL.md b/SKILL.md index 8f16089..6f8c9cf 100644 --- a/SKILL.md +++ b/SKILL.md @@ -6,15 +6,16 @@ This document gives an AI agent the exact knowledge needed to use, create, updat ## Package Identity -| Item | Value | -|------|-------| -| Package name | `openclerk` | -| CLI commands | `clerk` / `openclerk` | -| Entry point | `openclerk.cli:main` | -| Python requirement | `>=3.13` | -| Version | `0.1.0` | +| Item | Value | +| ------------------ | --------------------- | +| Package name | `openclerk` | +| CLI commands | `clerk` / `openclerk` | +| Entry point | `openclerk.cli:main` | +| Python requirement | `>=3.13` | +| Version | `0.1.2` | Install from source: + ```bash just setup # preferred # or @@ -25,15 +26,17 @@ uv sync --all-groups ## Core Concept: Reasoning Kits -A **reasoning kit** is a directory of plain text files that define a multi-step LLM workflow. No Python code is needed to define a kit — only files with strict naming conventions. +A **reasoning kit** is a directory of plain text files that define a multi-step LLM workflow. No Python code is needed to define or run a kit — only files with strict naming conventions. **Always use the CLI (`openclerk run`) to execute kits.** Do not write custom Python scripts for running kits; the CLI already handles MCP tool discovery, dynamic resource injection, and workflow execution. ### File naming rules -| File pattern | Role | Notes | -|---|---|---| -| `instruction_N.txt` | Workflow step N | N starts at 1; steps run in ascending order | -| `resource_N.EXT` | Static resource N | Read at load time; any extension supported | -| `dynamic_resource_N.EXT` | Dynamic resource N | Content is empty at load time; user provides it at runtime | +| File pattern | Role | Notes | +| ------------------------ | ---------------------------- | --------------------------------------------------------------- | +| `instruction_N.txt` | Workflow step N | N starts at 1; steps run in ascending order | +| `resource_N.EXT` | Static resource N | Read at load time; any extension supported | +| `dynamic_resource_N.EXT` | Dynamic resource N | Content is empty at load time; user provides it at runtime | +| `tool_N.json` | Tool reference N | References a tool from the global registry (built-in or MCP) | +| `mcp_servers.json` | MCP server config (optional) | Kit-local override; merges with project-root `mcp_servers.json` | Directories named `evaluations/` inside a kit are created automatically and hold evaluation JSON files — do not create them manually. @@ -41,15 +44,31 @@ Directories named `evaluations/` inside a kit are created automatically and hold Instructions use `{placeholder}` to reference content: -| Placeholder | Resolves to | -|---|---| -| `{resource_1}` | Content of `resource_1.*` | -| `{resource_2}` | Content of `resource_2.*` | -| `{workflow_1}` | Output of step 1 (available from step 2 onward) | -| `{workflow_N}` | Output of step N | -| `{tool_1}` | Result of calling tool 1 (LLM decides when to invoke) | +| Placeholder | Resolves to | +| -------------- | ----------------------------------------------------- | +| `{resource_1}` | Content of `resource_1.*` | +| `{resource_2}` | Content of `resource_2.*` | +| `{workflow_1}` | Output of step 1 (available from step 2 onward) | +| `{workflow_N}` | Output of step N | +| `{tool_1}` | Result of calling tool 1 (LLM decides when to invoke) | + +For resources exceeding 4 000 characters, the loader automatically uses RAG (chunking + embeddings) to retrieve only the most relevant chunks. RAG status is logged at `DEBUG` level only — no console warnings. + +### Tool reference file (`tool_N.json`) + +A `tool_N.json` file references a tool from the global registry by name. The tool can be built-in or provided by an MCP server. + +```json +{ + "tool_name": "read_url", + "display_name": "Web Fetcher", + "configuration": null +} +``` + +Only `tool_name` is required. `display_name` is optional (used in prompts). `configuration` is optional JSON that overrides the tool's default behavior. -For resources exceeding 4 000 characters, the loader automatically uses RAG (chunking + embeddings) to retrieve only the most relevant chunks. +The placeholder `{tool_1}` in an instruction resolves to the tool's display name and makes the tool available to the LLM for that step. ### Minimal kit example @@ -61,6 +80,15 @@ my-kit/ └── resource_2.csv # Rating criteria ``` +### Tool-enabled kit example + +``` +my-kit/ +├── instruction_1.txt # "Use {tool_1} to read https://example.com and summarise." +├── tool_1.json # {"tool_name": "read_url", "display_name": "Web Fetcher"} +└── mcp_servers.json # Optional: kit-local MCP server override +``` + ### Dynamic resource example (user provides input at runtime) ``` @@ -151,40 +179,60 @@ kits = await list_reasoning_kits_from_db() ## Executing a Kit -### Programmatic API +### CLI (recommended — always use this) -```python -from openclerk.loader import load_reasoning_kit -from openclerk.graph import run_reasoning_kit, run_reasoning_kit_async +```bash +# Run a local kit +openclerk run my-kit --local --base-path reasoning_kits -kit = load_reasoning_kit("reasoning_kits/demo") +# With dynamic resource inline +openclerk run my-kit --local --base-path reasoning_kits \ + --dynamic-resource resource_1="Art. 41 OR" -# Sync -outputs = run_reasoning_kit(kit, model="gpt-4o-mini") +# With dynamic resource from stdin +echo "Art. 41 OR" | openclerk run my-kit --local --base-path reasoning_kits \ + --stdin resource_1 -# Async (preferred) -outputs = await run_reasoning_kit_async(kit, model="gpt-4o-mini") +# With dynamic resource from file +openclerk run my-kit --local --base-path reasoning_kits \ + --dynamic-resource-file resource_1=./input.txt +``` -# outputs is dict[str, str]: -# {"workflow_1": "...", "workflow_2": "..."} -print(outputs["workflow_1"]) +The CLI automatically: + +- Discovers and loads the kit from the filesystem +- Detects `mcp_servers.json` in the kit directory and connects to MCP servers +- Discovers MCP tools and registers them in the global tool registry +- Injects dynamic resources +- Runs the full workflow step by step +- Cleans up MCP connections + +### Programmatic API (advanced use only) + +Only use the Python API when embedding openclerk inside another application. For standalone kit execution, always use the CLI. + +```python +from openclerk.loader import load_reasoning_kit +from openclerk.graph import run_reasoning_kit_async + +kit = load_reasoning_kit("reasoning_kits/demo") +outputs = await run_reasoning_kit_async(kit) ``` -### Full parameter reference +When using the programmatic API with MCP tools, you must manually initialize MCP servers: ```python -outputs = await run_reasoning_kit_async( - kit=kit, - evaluate=False, # Prompt user to score each step 0-100 - evaluation_mode="transparent", # "transparent" | "anonymous" - save_to_db=False, # Persist run + step results to DB - db_version_id=None, # UUID — required when save_to_db=True - model="gpt-4o-mini", # LLM model string - user_id=None, # UUID string — loads user's LLM provider config -) +from openclerk.mcp_client import init_mcp_servers, close_mcp_servers + +await init_mcp_servers(config_path="mcp_servers.json") +try: + outputs = await run_reasoning_kit_async(kit) +finally: + await close_mcp_servers() ``` **Evaluation modes:** + - `transparent` — stores full prompt + output text in DB - `anonymous` — stores only character counts (privacy-preserving) @@ -204,6 +252,7 @@ llm = await get_llm( ``` Supported providers (configured via env vars or DB per-user): + - `OpenAI` (`OPENAI_API_KEY`) - `Anthropic` (`ANTHROPIC_API_KEY`) - `Mistral` @@ -217,9 +266,9 @@ Supported providers (configured via env vars or DB per-user): ### Built-in tools (always available) -| Tool name | Description | -|---|---| -| `read_url` | Fetches a URL and extracts readable text via BeautifulSoup | +| Tool name | Description | +| ------------- | --------------------------------------------------------------------- | +| `read_url` | Fetches a URL and extracts readable text via BeautifulSoup | | `jina_reader` | Fetches a URL via `https://r.jina.ai/{url}` — bypasses bot protection | ### Register a custom tool @@ -270,11 +319,33 @@ kit.tools["1"] = Tool(tool_name="read_url", tool_id="tool_1") The LLM decides when to invoke the tool; results flow back automatically. +### Tool execution logging + +When tools are invoked, structured logs are emitted at `INFO` level: + +``` +Step 1 - Tools enabled: read_url +Tool call: read_url(url='https://example.com') +Tool result: Example Domain... +``` + +These appear in the terminal during `clerk run` and in the server logs during web execution. Set `LOGLEVEL=DEBUG` to see RAG chunking details as well. + --- ## MCP Integration -Configure external MCP servers in `mcp_servers.json` (project root): +MCP servers can be configured at the project root or inside a kit directory. Kit-local configs override global configs by server name. + +### Supported transports + +| Transport | Required fields | Notes | +| ----------------- | -------------------------- | ---------------------------- | +| `stdio` (default) | `command`, `args?`, `env?` | Spawns a subprocess | +| `sse` | `url` | Server-Sent Events over HTTP | +| `http` | `url` | Streamable HTTP | + +### Project-root `mcp_servers.json` ```json { @@ -290,58 +361,102 @@ Configure external MCP servers in `mcp_servers.json` (project root): } ``` -MCP tools are auto-registered into the global tool registry on startup and can be referenced in kits as `{tool_N}` exactly like built-in tools. +### Kit-local `mcp_servers.json` + +Place an `mcp_servers.json` inside a kit directory to add or override servers for that kit only. It merges with the project-root config (kit-local wins by server name). + +```json +{ + "mcpServers": { + "opencaselaw": { + "transport": "sse", + "url": "https://mcp.opencaselaw.ch" + } + } +} +``` + +**MCP tools are auto-registered into the global tool registry when the kit runs via CLI or web.** No custom Python code is needed to discover or register MCP tools. Place `mcp_servers.json` in the kit directory (or project root) and run the kit with `openclerk run` — everything is handled automatically. --- ## CLI Reference +**The CLI is the primary and recommended interface for all kit operations.** Do not write custom Python scripts to run kits — the CLI handles kit loading, MCP server connection, tool discovery, dynamic resource injection, and workflow execution automatically. + ### Kit discovery ```bash -clerk list # List kits from database -clerk list --local --path reasoning_kits # List local kits -clerk info # Show kit details +openclerk list # List kits from database +openclerk list --local --base-path reasoning_kits # List local kits +openclerk info # Show kit details ``` ### Running a kit ```bash -clerk run demo # From database -clerk run demo --local # From filesystem (looks in reasoning_kits/) -clerk run demo --evaluate # Enable step-by-step evaluation -clerk run demo --mode anonymous # Privacy-preserving evaluation -clerk run demo --model gpt-4o # Override model +openclerk run demo # From database +openclerk run demo --local # From filesystem (looks in reasoning_kits/) +openclerk run demo --evaluate # Enable step-by-step evaluation +openclerk run demo --mode anonymous # Privacy-preserving evaluation + +# Dynamic resources (skip interactive prompts) +openclerk run demo --dynamic-resource resource_1="inline text" +openclerk run demo --dynamic-resource-file resource_1=./my-input.txt +echo "piped text" | openclerk run demo --stdin resource_1 +``` + +**Dynamic resource flags:** + +- `--dynamic-resource resource_N="text"` — Provide content inline +- `--dynamic-resource-file resource_N=./file.txt` — Read content from a file +- `--stdin resource_N` — Read content from standard input (useful for piping) + +If all dynamic resources are satisfied via flags, the interactive prompt is skipped entirely. + +### Validating a kit + +```bash +openclerk validate my-kit --local ``` +Checks for: + +- Sequential instruction numbering +- All `{resource_N}` placeholders have matching resource files +- All `{tool_N}` placeholders have matching tool definitions (from `tool_*.json`, DB, or global registry) +- All `{workflow_N}` placeholders refer to earlier steps +- Dynamic resources are present but empty +- Approximate prompt size per step is within model context limits + ### Creating and managing kits ```bash -clerk kit create --description "What this kit does" -clerk kit delete --force +openclerk kit create --description "What this kit does" +openclerk kit delete --force ``` ### Syncing local ↔ database ```bash -clerk sync push -m "Commit message" # Upload local kit to DB -clerk sync pull # Download DB kit to filesystem -clerk sync list # Compare local vs. DB +openclerk sync push -m "Commit message" # Upload local kit to DB +openclerk sync pull # Download DB kit to filesystem +openclerk sync list # Compare local vs. DB ``` ### Database management ```bash -clerk db setup # Initialize database and storage bucket -clerk db migrate # Run pending Alembic migrations -clerk db status # Check migration status +openclerk db setup # Initialize database and storage bucket +openclerk db migrate # Run pending Alembic migrations +openclerk db status # Check migration status ``` ### Web server ```bash -clerk web # Start API + React UI on port 8000 -clerk web --port 3001 # Custom port +openclerk web # Start API + React UI on port 8000 +openclerk web --port 3001 # Custom port ``` --- @@ -442,15 +557,15 @@ CORS_ORIGINS=http://localhost:3000,https://example.com ## Database Schema Quick Reference -| Table | Key columns | -|---|---| -| `user_profiles` | `id`, `display_name`, `is_premium` | -| `reasoning_kits` | `id`, `slug` (unique), `name`, `owner_id`, `is_public`, `current_version_id` | -| `kit_versions` | `id`, `kit_id`, `version_number`, `commit_message`, `is_draft` | -| `resources` | `id`, `version_id`, `resource_number`, `resource_id`, `filename`, `storage_path`, `extracted_text`, `is_dynamic` | -| `workflow_steps` | `id`, `version_id`, `step_number`, `output_id`, `prompt_template` | -| `tools` | `id`, `version_id`, `tool_number`, `tool_name`, `configuration` | -| `execution_runs` | `id`, `version_id`, `user_id`, `status`, `storage_mode`, `started_at`, `completed_at` | +| Table | Key columns | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `user_profiles` | `id`, `display_name`, `is_premium` | +| `reasoning_kits` | `id`, `slug` (unique), `name`, `owner_id`, `is_public`, `current_version_id` | +| `kit_versions` | `id`, `kit_id`, `version_number`, `commit_message`, `is_draft` | +| `resources` | `id`, `version_id`, `resource_number`, `resource_id`, `filename`, `storage_path`, `extracted_text`, `is_dynamic` | +| `workflow_steps` | `id`, `version_id`, `step_number`, `output_id`, `prompt_template` | +| `tools` | `id`, `version_id`, `tool_number`, `tool_name`, `configuration` | +| `execution_runs` | `id`, `version_id`, `user_id`, `status`, `storage_mode`, `started_at`, `completed_at` | | `step_executions` | `id`, `run_id`, `step_number`, `input_text`, `output_text`, `evaluation_score`, `model_used`, `tokens_used`, `latency_ms` | `resource_id` on the `resources` table matches the placeholder used in prompts (e.g., `resource_1`). @@ -459,15 +574,37 @@ CORS_ORIGINS=http://localhost:3000,https://example.com ## Common Patterns -### Create a kit programmatically and run it +### Create and run a kit via CLI + +**Create the kit directory and files:** + +```bash +mkdir -p reasoning_kits/my-kit +cat > reasoning_kits/my-kit/instruction_1.txt << 'EOF' +Summarise in one sentence: {resource_1} +EOF +cat > reasoning_kits/my-kit/resource_1.txt << 'EOF' +The sun is a star at the center of the Solar System. +EOF +``` + +**Run it:** + +```bash +openclerk run my-kit --local --base-path reasoning_kits +``` + +### Create a kit programmatically (advanced only) + +Only use the Python API when embedding openclerk inside another application. For normal kit creation, write files to disk and use the CLI. ```python from openclerk.models import ReasoningKit, Resource, WorkflowStep -from openclerk.graph import run_reasoning_kit +from openclerk.graph import run_reasoning_kit_async kit = ReasoningKit( name="my-kit", - path="reasoning_kits/my-kit", # can be any string when constructing manually + path="reasoning_kits/my-kit", resources={ "1": Resource( file="resource_1.txt", @@ -481,19 +618,33 @@ kit = ReasoningKit( output_id="workflow_1", prompt="Summarise in one sentence: {resource_1}", ), - "2": WorkflowStep( - file="instruction_2.txt", - output_id="workflow_2", - prompt="Expand this summary into a paragraph: {workflow_1}", - ), }, ) -outputs = run_reasoning_kit(kit, model="gpt-4o-mini") -print(outputs["workflow_2"]) +outputs = await run_reasoning_kit_async(kit) +print(outputs["workflow_1"]) +``` + +### Add a tool to a kit via filesystem + +Create `tool_1.json` inside the kit directory: + +```json +{ + "tool_name": "read_url", + "display_name": "Web Fetcher" +} ``` -### Add a web-fetch tool to a kit +Then reference it in `instruction_1.txt`: + +``` +Fetch {tool_1} and summarise the content. +``` + +No Python code required — the loader auto-discovers `tool_*.json` files. + +### Add a tool to a kit programmatically ```python from openclerk.models import Tool @@ -520,17 +671,17 @@ outputs = await run_reasoning_kit_async( ## Key Source Files -| File | Purpose | -|---|---| -| `packages/clerk/src/openclerk/models.py` | Pydantic data models | -| `packages/clerk/src/openclerk/loader.py` | Kit loading (filesystem + DB) | -| `packages/clerk/src/openclerk/graph.py` | LangGraph execution engine | -| `packages/clerk/src/openclerk/tools.py` | Tool registry + built-in tools | -| `packages/clerk/src/openclerk/llm_factory.py` | Multi-provider LLM factory | -| `packages/clerk/src/openclerk/cli.py` | CLI commands | -| `packages/clerk/src/openclerk/web/routes/api.py` | REST API endpoints | -| `packages/clerk/src/openclerk/db/models.py` | SQLAlchemy ORM models | -| `packages/clerk/src/openclerk/db/repository.py` | Database access layer | -| `packages/clerk/src/openclerk/mcp_client.py` | MCP server integration | -| `packages/clerk/src/openclerk/evaluation.py` | Evaluation scoring | -| `reasoning_kits/demo/` | Reference kit (2 steps, 2 resources) | +| File | Purpose | +| ------------------------------------------------ | ------------------------------------ | +| `packages/clerk/src/openclerk/models.py` | Pydantic data models | +| `packages/clerk/src/openclerk/loader.py` | Kit loading (filesystem + DB) | +| `packages/clerk/src/openclerk/graph.py` | LangGraph execution engine | +| `packages/clerk/src/openclerk/tools.py` | Tool registry + built-in tools | +| `packages/clerk/src/openclerk/llm_factory.py` | Multi-provider LLM factory | +| `packages/clerk/src/openclerk/cli.py` | CLI commands | +| `packages/clerk/src/openclerk/web/routes/api.py` | REST API endpoints | +| `packages/clerk/src/openclerk/db/models.py` | SQLAlchemy ORM models | +| `packages/clerk/src/openclerk/db/repository.py` | Database access layer | +| `packages/clerk/src/openclerk/mcp_client.py` | MCP server integration | +| `packages/clerk/src/openclerk/evaluation.py` | Evaluation scoring | +| `reasoning_kits/demo/` | Reference kit (2 steps, 2 resources) | diff --git a/apps/frontend/README.md b/apps/frontend/README.md index 5192c07..1bebebf 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -1,15 +1,16 @@ -# OpenClerk Website +# OpenClerk Frontend -The official website for OpenClerk - built with React, TypeScript, and Vite. +The main OpenClerk application frontend - built with React, TypeScript, and Vite. ## Overview -This is the website application for openclerk.dev, providing: +This is the primary application interface for OpenClerk, providing: -- Documentation and guides -- Interactive reasoning kit browser -- Kit execution interface +- Interactive reasoning kit browser and management +- Visual kit editor with step-by-step workflow builder +- Kit execution interface with real-time streaming - User authentication and settings +- Execution history and evaluations ## Tech Stack @@ -29,7 +30,7 @@ This is the website application for openclerk.dev, providing: ### Setup ```bash -cd apps/website +cd apps/frontend npm install ``` @@ -66,10 +67,16 @@ npm run lint ## Project Structure ``` -apps/website/ +apps/frontend/ ├── src/ │ ├── components/ # Reusable UI components │ ├── pages/ # Route/page components +│ │ ├── HomePage.tsx # Dashboard/home +│ │ ├── KitDetailPage.tsx # Kit detail view +│ │ ├── KitEditorPage.tsx # Kit creation/editing +│ │ ├── KitRunPage.tsx # Kit execution +│ │ ├── DocsPage.tsx # Documentation viewer +│ │ └── ... │ ├── hooks/ # Custom React hooks │ ├── lib/ # Utilities and API clients │ ├── App.tsx # Main app component with routing @@ -81,7 +88,7 @@ apps/website/ ## Environment Variables -Create a `.env` file in `apps/website/` with: +Create a `.env` file in `apps/frontend/` with: ``` VITE_API_URL=http://localhost:8000 @@ -89,7 +96,9 @@ VITE_API_URL=http://localhost:8000 ## Deployment -The website is deployed to openclerk.dev via [deployment platform]. +The frontend is typically deployed alongside the backend in a Docker container (see `docker-compose.yml`), or can be deployed separately to platforms like Vercel or Netlify. + +**Note:** For the standalone marketing website and documentation (hosted on Vercel), see `apps/website/`. ## Links diff --git a/apps/frontend/src/pages/DocsPage.tsx b/apps/frontend/src/pages/DocsPage.tsx index 8c7a885..a12643c 100644 --- a/apps/frontend/src/pages/DocsPage.tsx +++ b/apps/frontend/src/pages/DocsPage.tsx @@ -7,22 +7,22 @@ import { getDocsList, getDocContent, type DocItem } from '../lib/api'; export default function DocsPage() { return (
-
- Documentation Menu - -
- -
+
+ Documentation Menu + +
+ +
} /> } /> @@ -33,17 +33,12 @@ export default function DocsPage() { } function DocViewerWrapper() { - // We use a splat route so this works for nested stuff like ui/overview const [slug, setSlug] = useState(''); const location = useLocation(); useEffect(() => { - // Remove leading "/docs/" from the pathname let path = location.pathname.replace(/^\/docs\/?/, '') || 'README'; - // Strip trailing .md if present to fix 404s - if (path.endsWith('.md')) { - path = path.slice(0, -3); - } + if (path.endsWith('.md')) path = path.slice(0, -3); setSlug(path); }, [location.pathname]); @@ -78,8 +73,6 @@ function CopyButton({ text }: { text: string }) { ); } - - function DocsSidebar() { const [docs, setDocs] = useState([]); const [loading, setLoading] = useState(true); @@ -134,8 +127,7 @@ function DocsSidebar() { groupedDocs[dir].push(doc); }); - // Custom order - User-centric docs first - const order = ['User Guide', 'Integration', 'Deployment', 'Contributing', 'General', 'CLI Commands', 'UI Features']; + const order = ['General', 'User Guide', 'Integration', 'Deployment', 'Contributing', 'CLI Commands', 'UI Features']; const groups = Object.keys(groupedDocs).sort((a, b) => { const indexA = order.indexOf(a); const indexB = order.indexOf(b); @@ -151,37 +143,84 @@ function DocsSidebar() { Documentation
); } +/** Convert React children to a plain text string for generating heading IDs */ +function nodeToText(node: any): string { + if (typeof node === 'string') return node; + if (typeof node === 'number') return String(node); + if (Array.isArray(node)) return node.map(nodeToText).join(''); + if (node?.props?.children) return nodeToText(node.props.children); + return ''; +} + +/** Generate a GitHub-style anchor ID from heading text */ +function toHeadingId(children: any): string { + return nodeToText(children) + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .trim(); +} + function DocViewer({ slug }: { slug: string }) { const [content, setContent] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const navigate = useNavigate(); + const location = useLocation(); + // Fetch document content useEffect(() => { setLoading(true); setError(''); @@ -197,6 +236,20 @@ function DocViewer({ slug }: { slug: string }) { }); }, [slug]); + // Scroll to top on doc change, or to the anchor if there's a hash + useEffect(() => { + if (loading) return; + const hash = location.hash; + if (hash) { + setTimeout(() => { + const el = document.getElementById(hash.slice(1)); + if (el) el.scrollIntoView({ behavior: 'smooth' }); + }, 50); + } else { + window.scrollTo(0, 0); + } + }, [slug, loading, location.hash]); + if (loading) { return (
@@ -224,9 +277,18 @@ function DocViewer({ slug }: { slug: string }) {

, - h2: ({node, ...props}) =>

, - h3: ({node, ...props}) =>

, + h1: ({ node, children, ...props }) => { + const id = toHeadingId(children); + return

{children}

; + }, + h2: ({ node, children, ...props }) => { + const id = toHeadingId(children); + return

{children}

; + }, + h3: ({ node, children, ...props }) => { + const id = toHeadingId(children); + return

{children}

; + }, p: ({node, ...props}) =>

, ul: ({node, ...props}) =>