Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/pnpm-release-age.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions apps/docs/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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" },
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/scripts/postbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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),
);
}
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/src/lib/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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];
}
2 changes: 2 additions & 0 deletions apps/docs/src/lib/redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
34 changes: 34 additions & 0 deletions apps/docs/src/lib/reference.ts
Original file line number Diff line number Diff line change
@@ -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 <command>`, `pnpm exec cloudhal <command>`, `yarn cloudhal <command>` or `bunx cloudhal <command>` ([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,
};
}
23 changes: 19 additions & 4 deletions apps/docs/src/lib/site-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,20 @@ 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
\`\`\`

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.
Expand All @@ -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
Expand All @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions apps/docs/test/repo-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
});
});
68 changes: 68 additions & 0 deletions docs/agents/README.md
Original file line number Diff line number Diff line change
@@ -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://<studio-host>/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: <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://<name>/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 <dir>`.

## 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://<studio-host>/mcp` with the header `x-api-key: <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).
Loading
Loading