diff --git a/.changeset/pnpm-release-age.md b/.changeset/pnpm-release-age.md new file mode 100644 index 00000000..416f554e --- /dev/null +++ b/.changeset/pnpm-release-age.md @@ -0,0 +1,4 @@ +--- +"@cloudhal/cli": patch +--- +New projects exempt `@cloudhal/*` and `create-cloudhal` from pnpm's `minimumReleaseAge` in their `pnpm-workspace.yaml`, so pnpm installs a Cloudhal release on the day it ships instead of refusing it for a day. diff --git a/README.md b/README.md index 9081d0fe..6ce858a5 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,13 @@ Blogs and newsrooms first; documentation sites and research or data documentatio ## Five minutes: create → ask an agent → preview → publish → your domain ```bash -npm create cloudhal@latest my-site && cd my-site +npm create cloudhal@latest my-site && cd my-site && npm install npx cloudhal dev # local site with drafts visible claude mcp add cloudhal -- npx cloudhal mcp --mode edits # connect your agent (any MCP client works) ``` +Step by step, with pnpm, Yarn and Bun: the [quickstart](https://cloudhal.dev/docs/start/quickstart). Codex, Cursor, VS Code and other clients: [connect an agent](https://cloudhal.dev/docs/agents/clients). + Or put a blog on your Cloudflare account in one click, with nothing installed: [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudhal-dev/cloudhal-template) diff --git a/apps/docs/astro.config.ts b/apps/docs/astro.config.ts index be04c7ff..6b7dad7b 100644 --- a/apps/docs/astro.config.ts +++ b/apps/docs/astro.config.ts @@ -28,10 +28,20 @@ export default defineConfig({ label: "Start", items: [ { label: "Overview", link: "/docs" }, + { label: "Installation", link: "/docs/start/installation" }, + { label: "Quickstart", link: "/docs/start/quickstart" }, { label: "Deploy to Cloudflare", link: "/docs/guides/deploy-button" }, { label: "Playground", link: "https://try.cloudhal.com" }, ], }, + { + label: "Agents", + items: [ + { label: "Agents and MCP", link: "/docs/agents" }, + { label: "Connect an agent", link: "/docs/agents/clients" }, + { label: "Docs for agents", link: "/docs/agents/docs-for-agents" }, + ], + }, { label: "Guides", items: [{ autogenerate: { directory: "docs/guides" } }] }, { label: "Contributing", @@ -60,6 +70,7 @@ export default defineConfig({ { label: "Reference", items: [ + { label: "CLI", link: "/docs/reference/cli" }, { label: "JSON Schemas v1", link: "/schemas/v1/index.json" }, { label: "Cloudhal-Fetch", link: "/fetch" }, { label: "llms.txt", link: "/llms.txt" }, diff --git a/apps/docs/scripts/postbuild.ts b/apps/docs/scripts/postbuild.ts index b9408e12..9636e1d2 100644 --- a/apps/docs/scripts/postbuild.ts +++ b/apps/docs/scripts/postbuild.ts @@ -21,6 +21,9 @@ const DIST = path.join(APP, "dist"); /** Sections with their own `llms.txt`, in index order. */ const SECTIONS = [ + { key: "start", prefix: "docs/start/", title: "Start" }, + { key: "agents", prefix: "docs/agents", title: "Agents and MCP" }, + { key: "reference", prefix: "docs/reference/", title: "Reference" }, { key: "guides", prefix: "docs/guides/", title: "Guides" }, { key: "contributing", prefix: "docs/", title: "Contributing and internals" }, { key: "adr", prefix: "docs/adr", title: "Architecture decisions" }, @@ -86,7 +89,7 @@ function sectionOf(page: Page): (typeof SECTIONS)[number] | undefined { if (!page.id.startsWith("docs/")) return undefined; return SECTIONS.find((s) => s.key === "contributing" - ? !/^docs\/(guides|adr|rfcs)(\/|$)/.test(page.id) + ? !/^docs\/(start|agents|reference|guides|adr|rfcs)(\/|$)/.test(page.id) : page.id === s.prefix || page.id.startsWith(s.prefix), ); } diff --git a/apps/docs/src/lib/pages.ts b/apps/docs/src/lib/pages.ts index 2eaead26..1467b641 100644 --- a/apps/docs/src/lib/pages.ts +++ b/apps/docs/src/lib/pages.ts @@ -3,6 +3,7 @@ * generated folder indexes. The content loader renders them; the post-build step writes their * Markdown copies and `llms.txt` from the same list. */ +import { cliReference } from "./reference.ts"; import { folderIndex, listDocs, readDoc } from "./repo-docs.ts"; import { SITE_PAGES } from "./site-pages.ts"; @@ -25,5 +26,5 @@ const FOLDER_INDEXES = [{ dir: "docs/rfcs", title: "Proposals (RFCs)" }] as cons export function allPages(repoRoot: string, absolute = false): Page[] { const docs = listDocs(repoRoot).map((file) => readDoc(repoRoot, file, absolute)); const indexes = FOLDER_INDEXES.map(({ dir, title }) => folderIndex(repoRoot, dir, title)); - return [...SITE_PAGES, ...indexes, ...docs]; + return [...SITE_PAGES, cliReference(), ...indexes, ...docs]; } diff --git a/apps/docs/src/lib/redirects.ts b/apps/docs/src/lib/redirects.ts index 0648029f..6157847d 100644 --- a/apps/docs/src/lib/redirects.ts +++ b/apps/docs/src/lib/redirects.ts @@ -10,6 +10,8 @@ export const REDIRECTS: readonly (readonly [from: string, to: string])[] = [ ["/docs/design", "/docs/design/design"], // Folders without an index page. ["/docs/guides", "/docs"], + ["/docs/start", "/docs/start/quickstart"], + ["/docs/reference", "/docs/reference/cli"], ["/docs/engineering", "/docs/engineering/code-structure"], ["/docs/security", "/docs/security/threat-model"], ["/github", "https://github.com/cloudhal-dev/cloudhal"], diff --git a/apps/docs/src/lib/reference.ts b/apps/docs/src/lib/reference.ts new file mode 100644 index 00000000..cf5ad1f1 --- /dev/null +++ b/apps/docs/src/lib/reference.ts @@ -0,0 +1,34 @@ +/** + * Reference pages generated from source (ADR-0039 rule 2), so they cannot drift from the code: + * the CLI reference is `cloudhal --help` itself. + */ +import { HELP } from "../../../../packages/cli/src/cli/help.ts"; +import type { SitePage } from "./site-pages.ts"; + +/** One `##` section per command, with its usage lines as written in `cloudhal --help`. */ +export function cliReference(help: string = HELP): SitePage { + const usage = help.split("\nUsage:\n")[1] ?? ""; + const blocks: { name: string; lines: string[] }[] = []; + for (const line of usage.split("\n")) { + const command = /^ {2}cloudhal ([a-z-]+)/.exec(line); + if (command?.[1] && command[1] !== "--help") { + const name = command[1]; + const last = blocks.at(-1); + if (last?.name === name) last.lines.push(line.slice(2)); + else blocks.push({ name, lines: [line.slice(2)] }); + } else if (line.trim() && blocks.length) blocks.at(-1)?.lines.push(line.slice(2)); + } + const body = [ + "Every command of the `cloudhal` CLI (`@cloudhal/cli`), as `cloudhal --help` prints it. Run it from a project folder: `npx cloudhal `, `pnpm exec cloudhal `, `yarn cloudhal ` or `bunx cloudhal ` ([installation](/docs/start/installation#running-the-cli)).", + "", + "Commands that take `--remote` act on the deployment instead of the local store, with the deployment token from `.cloudhal/deploy/secrets.json` (written by `cloudhal deploy`).", + "", + ...blocks.flatMap((b) => [`## ${b.name}`, "", "```txt", ...b.lines, "```", ""]), + ].join("\n"); + return { + id: "docs/reference/cli", + title: "CLI reference", + description: "Every command and flag of the cloudhal CLI, generated from `cloudhal --help`.", + body, + }; +} diff --git a/apps/docs/src/lib/site-pages.ts b/apps/docs/src/lib/site-pages.ts index 0b59df9f..1ea3875e 100644 --- a/apps/docs/src/lib/site-pages.ts +++ b/apps/docs/src/lib/site-pages.ts @@ -39,10 +39,11 @@ const home: SitePage = { \`\`\`sh npm create cloudhal@latest my-site cd my-site -npx cloudhal dev +npm install +npm run dev \`\`\` -Then connect your agent. Any MCP client works: +Then connect your agent. Any MCP client works ([Codex, Cursor, VS Code and more](/docs/agents/clients)): \`\`\`sh claude mcp add cloudhal -- npx cloudhal mcp --mode edits @@ -50,6 +51,8 @@ claude mcp add cloudhal -- npx cloudhal mcp --mode edits Or deploy a blog to your Cloudflare account in one click with the [Deploy to Cloudflare button](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudhal-dev/cloudhal-template), or try a private site for an hour at [try.cloudhal.com](https://try.cloudhal.com). +Step by step, with pnpm, Yarn and Bun: the [quickstart](/docs/start/quickstart). + ## Reference - [Docs](/docs): guides, decisions and proposals. @@ -69,8 +72,15 @@ const docsIndex: SitePage = { ## Get going -- [Install and run locally](https://github.com/cloudhal-dev/cloudhal#readme): \`npm create cloudhal@latest\`, \`cloudhal dev\`, connect an agent. -- [Deploy to Cloudflare with one click](/docs/guides/deploy-button). +- [Installation](/docs/start/installation): Node 24, npm, pnpm, Yarn or Bun. +- [Quickstart](/docs/start/quickstart): create a site, open Studio, connect an agent, publish, deploy. +- [Deploy to Cloudflare with one click](/docs/guides/deploy-button), with nothing installed. + +## Agents + +- [Agents and MCP](/docs/agents): local and remote servers, modes, approvals, skills. +- [Connect an agent](/docs/agents/clients): Claude Code, Codex, Cursor, VS Code, Claude Desktop and more. +- [Docs for agents](/docs/agents/docs-for-agents): Markdown pages, \`llms.txt\` and the docs MCP server. - [Harden a Cloudflare deployment](/docs/guides/cloudflare-hardening). ## Guides @@ -80,6 +90,11 @@ const docsIndex: SitePage = { - [Analytics and privacy](/docs/guides/analytics-privacy) - [Translations with Weblate](/docs/guides/translations-weblate) +## Reference + +- [CLI](/docs/reference/cli), generated from \`cloudhal --help\`. +- [JSON Schemas v1](/schemas/v1/index.json). + ## How Cloudhal is built - [Architecture decisions (ADRs)](/docs/adr): what was decided and why, with sources. diff --git a/apps/docs/test/repo-docs.test.ts b/apps/docs/test/repo-docs.test.ts index bc12ab5a..2d089148 100644 --- a/apps/docs/test/repo-docs.test.ts +++ b/apps/docs/test/repo-docs.test.ts @@ -156,3 +156,14 @@ describe("describe with list summaries", () => { ).toBe("What. A generator."); }); }); + +describe("the CLI reference", () => { + it("has a section per command of cloudhal --help, with its flags", async () => { + const { cliReference } = await import("../src/lib/reference.ts"); + const { body } = cliReference(); + for (const command of ["init", "dev", "studio", "mcp", "deploy", "push", "pull", "schedules"]) { + expect(body).toContain(`\n## ${command}\n`); + } + expect(body).toContain("cloudhal mcp [--mode plan|manual|edits|auto|bypass]"); + }); +}); diff --git a/docs/agents/README.md b/docs/agents/README.md new file mode 100644 index 00000000..9940f1dc --- /dev/null +++ b/docs/agents/README.md @@ -0,0 +1,68 @@ +# Agents and MCP + +Cloudhal is built for agents to work on sites, with people in charge. Agents connect over the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), call semantic tools instead of editing files, and work inside a policy engine: each agent has a mode and scopes, risky actions wait for a person, every change is a snapshot, and every call is in the activity log. + +## Two ways to connect + +| | Local | Remote | +|---|---|---| +| Server | `cloudhal mcp`, started by your client over stdio | `https:///mcp` on a deployment (streamable HTTP) | +| Works on | The project folder it runs in (`.cloudhal/` store) | The deployment's sites | +| Who the agent is | You, the local developer | An agent key created in Studio | +| Authentication | None (a local process) | `x-api-key: ` header | +| Approvals | Asked in your MCP client | Asked in your client, or wait in Studio's **Approvals** | +| Set up | [Connect an agent](/docs/agents/clients) | [Remote agents](#remote-agents-on-a-deployment) below | + +Both expose the same tools and the same policy. Use local while building a site, remote for agents that work on a live site, including scheduled ones. + +## Modes + +A mode says how much the agent does on its own. Choose it with `--mode` locally, or per key in Studio. + +| Mode | What the agent does on its own | +|---|---| +| `plan` | Reads and previews only | +| `manual` | Nothing: every change waits for you | +| `edits` | Content edits; publish, rollback and deletes wait (the default) | +| `auto` | Also small article-only publishes within the site's publishing window | +| `bypass` | Everything except what always needs a person. Local only; a remote key in `bypass` works as `edits` | + +Some actions always wait for a person, in every mode: anything that makes a person's data public, approving reader comments, and structural changes such as merging terms or adding redirects. An agent can ask for a more autonomous mode for a while (`request_elevation`); you grant it in Studio, possibly for less time, and it ends by itself. + +## Approvals + +When a call needs a person, the server asks your client with MCP elicitation. The question carries a summary of the change, such as the diff of a publish. The call goes through only if you accept it. Clients without elicitation get a `confirmToken`; the agent repeats the identical call with it after you confirm in the conversation. + +On a deployment, pending approvals also appear in Studio under **Approvals**, with the same summary. + +## What agents can do + +The tools work on Cloudhal's site graph: sites, pages and their sections, articles and other entries, taxonomies, menus, media, snapshots and releases. + +- **Read:** `list_sites`, `get_site`, `list_pages`, `get_page`, `list_entries`, `get_entry`, `search_site`, the taxonomy and redirect listings, analytics (`get_analytics`, `top_content`, `content_stats`, `broken_links`). +- **Write:** `create_page`, `update_page`, `create_entry`, `update_entry`, `add_correction`, `edit_body` and `patch_draft` for article bodies, taxonomy changes through `propose_term`, media (`upload_asset`, `fetch_asset_url`), menus (`set_menu`), design and chrome (`set_design`, `set_chrome`), translations (`save_translation`). Updates carry the node's current `rev`, so an agent never overwrites a change it has not seen. +- **Check:** `preview_page` returns a private preview link; `render_page` renders a page in Chrome and reports layout, contrast, alt text and heading problems. +- **Publish:** `create_snapshot`, `diff_snapshots`, `publish`, `rollback`, scheduled releases and embargoes. +- **Mode:** `get_mode`, `request_elevation`. + +About 100 tools in all; a client lists them with their JSON Schemas. Every mutation creates exactly one snapshot. Errors have stable codes and say what to do next. + +## Skills + +The server publishes skills as MCP resources: `skill:///SKILL.md` and its references. A skill is a short playbook for a task, such as writing an article or triaging comments. When a client follows a skill it sends `_meta["cloudhal.skill"]`, and the policy engine limits that call to the skill's declared scopes. Add your own with `cloudhal mcp --skills `. + +## Remote agents on a deployment + +1. In Studio, open **Agents** and create a key. Choose its mode and scopes; they can never exceed your own role. The secret is shown once. +2. Point your client at `https:///mcp` with the header `x-api-key: `. See [Connect an agent](/docs/agents/clients#remote-server) for each client's format. +3. The agent's calls run as that key: Studio's **Activity** shows them, **Approvals** collects what waits, and you can change the key's mode or revoke it at any time. + +Keep keys narrow. A key that reads reader comments should not also publish: comment text is written by strangers and reaches the agent. The [comments guide](/docs/guides/comments) and the [schedules guide](/docs/guides/schedules) (agent tasks with their own keys and budgets) go further. + +## Security + +- The local server trusts the person who started it; never expose `cloudhal studio --no-auth` beyond localhost. +- Remote keys are scoped, rate limited, revocable and audited. The `/mcp` endpoint refuses cross-origin browser requests. +- Treat content an agent reads (comments, fetched pages, imported files) as untrusted. Policy limits what any instruction in it can make the agent do. + +More: the [threat model](/docs/security/threat-model), [ADR-0011](/docs/adr/0011-agent-layer) (the agent layer) and [ADR-0015](/docs/adr/0015-agent-modes) (modes). diff --git a/docs/agents/clients.md b/docs/agents/clients.md new file mode 100644 index 00000000..761daed3 --- /dev/null +++ b/docs/agents/clients.md @@ -0,0 +1,214 @@ +# Connect an agent + +Every MCP client connects to Cloudhal in one of two ways ([which one?](/docs/agents#two-ways-to-connect)): + +- **Local server:** the client starts `cloudhal mcp` in your project folder. Nothing to authenticate; the agent works on the project's local store. +- **Remote server:** the client calls `https:///mcp` on a deployment, sending an agent key created in Studio under **Agents** in the `x-api-key` header. + +`cloudhal mcp` serves the project folder it starts in. Configure local servers **per project** (the project-scoped files below), or start the client from the project folder. + +## Run it with your package manager + +The CLI is a dependency of your project, so the client runs the project's copy. Use the line for your package manager wherever the examples below say `npx cloudhal mcp`: + +| Package manager | Command | `command` / `args` in JSON configs | +|---|---|---| +| npm | `npx cloudhal mcp --mode edits` | `"npx"`, `["cloudhal", "mcp", "--mode", "edits"]` | +| pnpm | `pnpm exec cloudhal mcp --mode edits` | `"pnpm"`, `["exec", "cloudhal", "mcp", "--mode", "edits"]` | +| Yarn | `yarn cloudhal mcp --mode edits` | `"yarn"`, `["cloudhal", "mcp", "--mode", "edits"]` | +| Bun | `bunx cloudhal mcp --mode edits` | `"bunx"`, `["cloudhal", "mcp", "--mode", "edits"]` | + +Do not use `npx -y @cloudhal/cli mcp` for a local server: it downloads a separate copy of the CLI that may not match your project's version. + +`--mode` is one of `plan`, `manual`, `edits` (default), `auto` or `bypass` ([modes](/docs/agents#modes)). `cloudhal mcp --preview-url http://localhost:4321` makes preview links point at your running `cloudhal dev`. + +For the remote examples, put the key in an environment variable instead of the file: + +```sh +export CLOUDHAL_API_KEY="" +``` + +## Claude Code + +Local, from the project folder. `--scope project` writes `.mcp.json`, which you can commit so everyone on the project gets the server: + +```sh +claude mcp add --scope project cloudhal -- npx cloudhal mcp --mode edits +``` + +Remote: + +```sh +claude mcp add --transport http cloudhal https://studio.example.com/mcp \ + --header "x-api-key: $CLOUDHAL_API_KEY" +``` + +Or in `.mcp.json`, with the key read from the environment when Claude Code starts: + +```json +{ + "mcpServers": { + "cloudhal": { + "type": "http", + "url": "https://studio.example.com/mcp", + "headers": { "x-api-key": "${CLOUDHAL_API_KEY}" } + } + } +} +``` + +Check the connection with `/mcp` inside Claude Code. Approvals appear as questions in the conversation. + +## Codex + +Local, from the project folder: + +```sh +codex mcp add cloudhal -- npx cloudhal mcp --mode edits +``` + +That writes `~/.codex/config.toml`, which applies to every folder. To keep the server with the project, put it in the project's `.codex/config.toml` instead (Codex reads it in trusted projects): + +```toml +[mcp_servers.cloudhal] +command = "npx" +args = ["cloudhal", "mcp", "--mode", "edits"] +``` + +Remote, with the key taken from the environment: + +```toml +[mcp_servers.cloudhal] +url = "https://studio.example.com/mcp" +env_http_headers = { "x-api-key" = "CLOUDHAL_API_KEY" } +``` + +## Cursor + +Local, in the project's `.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "cloudhal": { + "command": "npx", + "args": ["cloudhal", "mcp", "--mode", "edits"] + } + } +} +``` + +Remote: + +```json +{ + "mcpServers": { + "cloudhal": { + "url": "https://studio.example.com/mcp", + "headers": { "x-api-key": "${env:CLOUDHAL_API_KEY}" } + } + } +} +``` + +## VS Code (GitHub Copilot) + +Local, in the project's `.vscode/mcp.json`: + +```json +{ + "servers": { + "cloudhal": { + "type": "stdio", + "command": "npx", + "args": ["cloudhal", "mcp", "--mode", "edits"] + } + } +} +``` + +Remote. VS Code asks for the key once and stores it securely: + +```json +{ + "inputs": [ + { "type": "promptString", "id": "cloudhal-key", "description": "Cloudhal agent key", "password": true } + ], + "servers": { + "cloudhal": { + "type": "http", + "url": "https://studio.example.com/mcp", + "headers": { "x-api-key": "${input:cloudhal-key}" } + } + } +} +``` + +## Claude Desktop and claude.ai + +Use the remote server: **Settings › Connectors › Add custom connector**, the URL `https://studio.example.com/mcp`, and a request header `x-api-key` with your key. Claude connects from Anthropic's cloud, so this needs a deployment; `localhost` is not reachable. For a local project, use Claude Code. + +## Windsurf + +Remote, in `mcp_config.json` (global, so prefer the remote server): + +```json +{ + "mcpServers": { + "cloudhal": { + "serverUrl": "https://studio.example.com/mcp", + "headers": { "x-api-key": "${env:CLOUDHAL_API_KEY}" } + } + } +} +``` + +## Zed + +In the project's `.zed/settings.json`: + +```json +{ + "context_servers": { + "cloudhal": { + "command": "npx", + "args": ["cloudhal", "mcp", "--mode", "edits"] + } + } +} +``` + +Remote: `{ "url": "https://studio.example.com/mcp", "headers": { "x-api-key": "…" } }` in place of `command` and `args`. + +## Gemini CLI + +Local, in the project's `.gemini/settings.json`: + +```json +{ + "mcpServers": { + "cloudhal": { "command": "npx", "args": ["cloudhal", "mcp", "--mode", "edits"] } + } +} +``` + +Remote (Gemini uses `httpUrl` for streamable HTTP): + +```sh +gemini mcp add --transport http -H "x-api-key: $CLOUDHAL_API_KEY" cloudhal https://studio.example.com/mcp +``` + +## Any other client + +- **stdio:** command `npx`, arguments `cloudhal mcp --mode edits`, working directory: your project. +- **Streamable HTTP:** URL `https:///mcp`, header `x-api-key: `. + +A client that cannot send headers can reach the remote server through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) as a local stdio command: `npx -y mcp-remote https://studio.example.com/mcp --header "x-api-key:${CLOUDHAL_API_KEY}"`. + +## Troubleshooting + +- **The client lists no tools.** Run the command yourself in the project folder: `npx cloudhal mcp` should start and keep running (it waits for a client on stdin). An error there (Node older than 24, not a Cloudhal project) is what the client sees. +- **The agent edits a different site.** The local server serves the folder it started in. Use a project-scoped config, or start the client from the project. +- **`401 unauthorized` remotely.** The key is missing, revoked, or sent in another header. Cloudhal reads `x-api-key`. +- **`403 not_a_member`.** The key's owner no longer belongs to the deployment; create a new key. +- **A call says it is waiting for approval.** Accept it in your client, or in Studio under **Approvals** on a deployment. diff --git a/docs/agents/docs-for-agents.md b/docs/agents/docs-for-agents.md new file mode 100644 index 00000000..afe7c45f --- /dev/null +++ b/docs/agents/docs-for-agents.md @@ -0,0 +1,49 @@ +# Docs for agents + +These docs are published for agents as well as for people. Point your agent at them when it works on a Cloudhal project. + +## Markdown pages + +Every page is also available as Markdown. Add `.md` to its URL: + +```txt +https://cloudhal.dev/docs/start/quickstart → HTML +https://cloudhal.dev/docs/start/quickstart.md → Markdown +``` + +`/index.md` works too. Each Markdown file starts with frontmatter: `title`, `description`, `url` and `source` (the file in the repository). Links in it are absolute, so it reads correctly anywhere. + +## llms.txt + +[`/llms.txt`](/llms.txt) lists every page with a one-line description, grouped by section ([llmstxt.org](https://llmstxt.org)). [`/llms-full.txt`](/llms-full.txt) holds the whole documentation in one file, for tools that index or load everything at once. + +Each section has its own pair, smaller to load: + +| Section | Index | Full text | +|---|---|---| +| Start | [`/docs/start/llms.txt`](/docs/start/llms.txt) | [`/docs/start/llms-full.txt`](/docs/start/llms-full.txt) | +| Agents and MCP | [`/docs/agents/llms.txt`](/docs/agents/llms.txt) | [`/docs/agents/llms-full.txt`](/docs/agents/llms-full.txt) | +| Reference | [`/docs/reference/llms.txt`](/docs/reference/llms.txt) | [`/docs/reference/llms-full.txt`](/docs/reference/llms-full.txt) | +| Guides | [`/docs/guides/llms.txt`](/docs/guides/llms.txt) | [`/docs/guides/llms-full.txt`](/docs/guides/llms-full.txt) | +| Contributing and internals | [`/docs/contributing/llms.txt`](/docs/contributing/llms.txt) | [`/docs/contributing/llms-full.txt`](/docs/contributing/llms-full.txt) | +| Architecture decisions | [`/docs/adr/llms.txt`](/docs/adr/llms.txt) | [`/docs/adr/llms-full.txt`](/docs/adr/llms-full.txt) | +| Proposals | [`/docs/rfcs/llms.txt`](/docs/rfcs/llms.txt) | [`/docs/rfcs/llms-full.txt`](/docs/rfcs/llms-full.txt) | + +## Tell your agent about Cloudhal + +Add a few lines to your project's `AGENTS.md` (or `CLAUDE.md`): + +```md +This is a Cloudhal site. Content (pages, articles, menus, settings) lives in Cloudhal's store, +not in files: change it through the `cloudhal` MCP server's tools, never by editing `.cloudhal/`. +The theme is the Astro code in `src/`. +Docs: https://cloudhal.dev/llms.txt (every page also as Markdown: add .md to its URL). +``` + +## The JSON Schemas + +Cloudhal's contracts are JSON Schemas at permanent URLs: [`/schemas/v1/index.json`](/schemas/v1/index.json) lists them. Every node an MCP tool returns follows one of them. + +## Searching the docs from the MCP server + +Planned ([RFC-0037](/docs/rfcs/0037-cloudhal-dev-docs-site)): `search_docs` and `read_doc` tools in the Cloudhal MCP server, so the agent already connected to your site can look up how to use it, for the version you have installed. Until then, the Markdown pages and `llms.txt` above are the way in. diff --git a/docs/start/installation.md b/docs/start/installation.md new file mode 100644 index 00000000..69928f73 --- /dev/null +++ b/docs/start/installation.md @@ -0,0 +1,101 @@ +# Installation + +Create a Cloudhal project with your package manager. The project is an Astro site with a local Cloudhal store; it runs offline and deploys to Cloudflare when you are ready. + +## Requirements + +- **Node.js 24 or later.** Cloudhal's CLI runs TypeScript with Node's built-in type stripping, so older versions refuse to start. Check with `node --version`. +- **A package manager:** npm (comes with Node), pnpm, Yarn or Bun. +- Optional: + - **Google Chrome or Chromium**, for `cloudhal render` and the agent's `render_page` checks (screenshots and accessibility checks of a page). Without it, those tools say they are unavailable and everything else works. + - **A Cloudflare account and `wrangler login`**, only to deploy. Wrangler is installed with the project. + +## Create a project + +Pick your package manager. Each command creates `my-site/` with the blog starter and a seeded local site. + +```sh +# npm +npm create cloudhal@latest my-site + +# pnpm +pnpm create cloudhal@latest my-site + +# Yarn +yarn create cloudhal my-site + +# Bun +bun create cloudhal my-site +``` + +Name the site at the same time with `--name`. npm needs `--` before the scaffolder's own flags: + +```sh +npm create cloudhal@latest my-site -- --name "Field Notes" +pnpm create cloudhal@latest my-site --name "Field Notes" +``` + +Then install the project's dependencies: + +```sh +cd my-site +npm install # or: pnpm install · yarn · bun install +``` + +The scaffolder prints these steps with the package manager it was run with. + +## What you get + +| Path | What it is | +|---|---| +| `cloudhal.json` | The project's Cloudhal settings: site id, deployment name, analytics, comments, schedules | +| `src/` | The Astro theme: routes, layouts and `theme.ts`. Edit it like any Astro project | +| `.cloudhal/` | The local store (SQLite and media). Not committed; `cloudhal push` and `pull` move content between it and a deployment | +| `package.json` | Scripts: `dev`, `studio`, `mcp`, `deploy`, `build`, `snapshot` | + +The site's content (pages, articles, menus, settings) lives in the store, not in files. Agents and Studio change it; every change is a snapshot you can diff and roll back. + +## Running the CLI + +The `cloudhal` CLI is a dependency of the project (`@cloudhal/cli`), so you never install it globally. Inside the project, any of these runs it: + +| Package manager | Run a command | Run a script | +|---|---|---| +| npm | `npx cloudhal dev` | `npm run dev` | +| pnpm | `pnpm exec cloudhal dev` | `pnpm dev` | +| Yarn | `yarn cloudhal dev` | `yarn dev` | +| Bun | `bunx cloudhal dev` | `bun run dev` | + +Outside a project, `npx @cloudhal/cli@latest --help` (or `pnpm dlx @cloudhal/cli@latest --help`) runs the latest CLI without installing it. Most commands need a project, so `cd` into one first. + +## pnpm notes + +pnpm blocks dependency build scripts and releases younger than a day by default. A new project's `pnpm-workspace.yaml` already allows what Cloudhal needs (projects created before 0.3.4 lack the `minimumReleaseAgeExclude` lines; add them). In an existing pnpm workspace, add: + +```yaml +allowBuilds: + esbuild: true + better-sqlite3: true + workerd: true +minimumReleaseAgeExclude: + - '@cloudhal/*' + - 'create-cloudhal' +``` + +`better-sqlite3` is the local store's database driver and needs its build script; `workerd` runs Cloudflare's runtime locally. + +## Upgrading + +All `@cloudhal/*` packages share one version. Upgrade them together: + +```sh +npm install @cloudhal/cli@latest @cloudhal/core-ts@latest @cloudhal/astro-renderer@latest @cloudhal/cloudflare@latest +``` + +Then run `npx cloudhal deploy` to update a deployment. Release notes are in each package's changelog on npm and GitHub. + +## Next + +- [Quickstart](/docs/start/quickstart): run the site, open Studio, connect an agent, publish. +- [Connect an agent](/docs/agents): Claude Code, Codex, Cursor, VS Code and other MCP clients. +- [Deploy to Cloudflare with one click](/docs/guides/deploy-button), with nothing installed. diff --git a/docs/start/quickstart.md b/docs/start/quickstart.md new file mode 100644 index 00000000..2c9ba59e --- /dev/null +++ b/docs/start/quickstart.md @@ -0,0 +1,85 @@ +# Quickstart + +From an empty folder to a published site with an agent, in about ten minutes. You need Node.js 24 or later; see [Installation](/docs/start/installation) for pnpm, Yarn and Bun. + +## 1. Create the project + +```sh +npm create cloudhal@latest my-site +cd my-site +npm install +``` + +You get the blog starter with a seeded local site: a home page, an about page, a few articles and a menu. + +## 2. Preview the site + +```sh +npm run dev +``` + +Open . This is the live preview: drafts are visible, and it updates as content changes. Everything runs offline on the project's local store in `.cloudhal/`. + +## 3. Open Studio + +In a second terminal: + +```sh +npm run studio +``` + +Open . The first account you create becomes the Owner, and sign-up then closes. Studio is where people edit content, review what agents changed, approve publishes and roll back. + +## 4. Connect an agent + +Cloudhal's MCP server gives agents semantic tools (`create_entry`, `update_page`, `preview_page`, `publish`, …) behind a policy engine. With Claude Code, from the project folder: + +```sh +claude mcp add cloudhal -- npx cloudhal mcp --mode edits +``` + +With Codex: + +```sh +codex mcp add cloudhal -- npx cloudhal mcp --mode edits +``` + +Cursor, VS Code, Claude Desktop, Windsurf, Zed and Gemini CLI are in [Connect an agent](/docs/agents/clients). Start the client in the project folder: `cloudhal mcp` serves the project it runs in. + +`--mode edits` lets the agent change content on its own while publishing, rollback and deletes wait for you. The other [modes](/docs/agents#modes) range from read-only (`plan`) to `auto`. + +## 5. Ask for something + +Try: + +> Write a launch post about our new product, show me a preview, then publish it. + +The agent creates a draft, calls `preview_page` and gives you a preview link. Every change it makes is a snapshot. When it calls `publish`, your client asks you to approve it, with a summary of what changes. Nothing goes live until you accept. + +Watch it happen in Studio: **Activity** shows every call, **Snapshots** shows the history, and any snapshot can be diffed or rolled back. + +## 6. Deploy to Cloudflare + +```sh +npx wrangler login +npm run deploy +``` + +`cloudhal deploy` creates two Workers (the site, and the admin Worker with Studio, the API and the remote MCP endpoint), a D1 database and an R2 bucket, pushes your local site, and prints a one-time link to create the deployment's Owner account. To use your own domain: + +```sh +npx cloudhal domain set news.example.com +npm run deploy +``` + +The zone must be in the same Cloudflare account. No Node project? [Deploy with one click](/docs/guides/deploy-button) from the template instead. + +## 7. Connect agents to the deployment + +On a deployment, agents use the remote MCP endpoint at `https:///mcp` with a key you create in Studio under **Agents**. Each key has a mode and scopes. See [remote agents](/docs/agents#remote-agents-on-a-deployment). + +## Next + +- [How agents work with Cloudhal](/docs/agents): modes, approvals, skills and the tool list. +- [CLI reference](/docs/reference/cli). +- Guides: [search](/docs/guides/search), [schedules](/docs/guides/schedules), [comments](/docs/guides/comments), [several sites](/docs/guides/multi-site). diff --git a/packages/cli/src/project-setup.ts b/packages/cli/src/project-setup.ts index c31ef35f..58d9e23c 100644 --- a/packages/cli/src/project-setup.ts +++ b/packages/cli/src/project-setup.ts @@ -11,12 +11,21 @@ */ export const BUILD_DEPENDENCIES = ["better-sqlite3", "esbuild", "workerd"] as const; +/** + * Cloudhal's own packages, exempt from pnpm's `minimumReleaseAge`: without this, every pnpm + * install refuses a Cloudhal release for its first day, including the upgrade a release asks for. + */ +export const RELEASE_AGE_EXEMPT = ["@cloudhal/*", "create-cloudhal"] as const; + /** pnpm settings for a project outside a workspace: settings only, no `packages`. */ export function pnpmWorkspaceYaml(): string { return [ "# pnpm 12 runs install scripts only for packages allowed here.", "allowBuilds:", ...BUILD_DEPENDENCIES.map((name) => ` ${name}: true`), + "# Cloudhal releases install on the day they ship.", + "minimumReleaseAgeExclude:", + ...RELEASE_AGE_EXEMPT.map((name) => ` - '${name}'`), "", ].join("\n"); } diff --git a/packages/cli/test/project-setup.test.ts b/packages/cli/test/project-setup.test.ts index 912bd784..c660b78c 100644 --- a/packages/cli/test/project-setup.test.ts +++ b/packages/cli/test/project-setup.test.ts @@ -42,7 +42,7 @@ describe("install settings", () => { const workspace = await readFile(path.join(ROOT, "pnpm-workspace.yaml"), "utf8"); for (const name of BUILD_DEPENDENCIES) expect(workspace).toContain(`\n ${name}: true`); expect(pnpmWorkspaceYaml()).toBe( - "# pnpm 12 runs install scripts only for packages allowed here.\nallowBuilds:\n better-sqlite3: true\n esbuild: true\n workerd: true\n", + "# pnpm 12 runs install scripts only for packages allowed here.\nallowBuilds:\n better-sqlite3: true\n esbuild: true\n workerd: true\n# Cloudhal releases install on the day they ship.\nminimumReleaseAgeExclude:\n - '@cloudhal/*'\n - 'create-cloudhal'\n", ); const pkg = JSON.parse(await readFile(path.join(STARTER, "package.json"), "utf8")); expect(pkg.trustedDependencies).toEqual([...BUILD_DEPENDENCIES]);