Connect AI apps: step-by-step guides with screenshots, and the MCP URL in Settings (#217)
* Markdown: GitHub alerts (> [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]) * Connect AI apps: guides for Claude, Claude Code, Codex and other apps, Settings panel with the MCP URL (WIP, screenshots next) * Connect AI apps: real screenshots of the consent page and Connected apps, served from docs/images; docs tests
- CI / Typecheck and bundleFailed
- CI / Unit tests (1/2)Failed
- CI / Unit tests (2/2)Failed
26 files changed+769−164
Files (26)
- apps/diffinery/docs/agents/claude-and-codex.md+0−145
- apps/diffinery/docs/agents/mcp.md+2−2
- apps/diffinery/docs/agents/overview.md+1−1
- apps/diffinery/docs/connect.md+52−0
- apps/diffinery/docs/connect/claude-code.md+125−0
- apps/diffinery/docs/connect/claude.md+62−0
- apps/diffinery/docs/connect/codex.md+103−0
- apps/diffinery/docs/connect/how-sign-in-works.md+60−0
- apps/diffinery/docs/connect/other-apps.md+63−0
- apps/diffinery/docs/images/connected-apps.webp+0−0
- apps/diffinery/docs/images/consent-claude-code.webp+0−0
- apps/diffinery/docs/images/consent-codex.webp+0−0
- apps/diffinery/docs/index.md+3−2
- apps/diffinery/docs/nav.json+11−1
- apps/diffinery/scripts/build-docs.mjs+31−1
- apps/diffinery/scripts/docs-screenshots.mjs+143−0
- apps/diffinery/src/auth/oauth.ts+2−2
- apps/diffinery/src/docs/pages.ts+3−2
- apps/diffinery/src/docs/reference.ts+1−1
- apps/diffinery/src/docs/web.tsx+19−1
- apps/diffinery/src/web/account.tsx+2−2
- apps/diffinery/src/web/assets.ts+5−0
- apps/diffinery/src/web/markdown.ts+8−0
- apps/diffinery/src/web/oauth.tsx+30−4
- apps/diffinery/test/unit/docs.test.ts+26−0
- apps/diffinery/test/unit/markdown-safety.test.ts+17−0
deletedapps/diffinery/docs/agents/claude-and-codex.md+0−145
| @@ -1,145 +1,0 @@ | ||
| 1 | ---- | |
| 2 | -title: Claude and Codex | |
| 3 | -description: Connect claude.ai, Claude Code and OpenAI Codex to Diffinery with a browser sign-in, install the Diffinery plugin, and run them headless with a token. | |
| 4 | ---- | |
| 5 | - | |
| 6 | -Claude (claude.ai, the Claude apps and Claude Code) and OpenAI's Codex work with Diffinery through its [MCP server](/docs/agents/mcp) at `https://diffinery.com/mcp`, and through git and the [`diffinery` CLI](/docs/accounts/cli-sign-in) in a terminal. You don't need to copy a token anywhere: each app signs in through your browser, where you choose whom it acts as and what it may do. | |
| 7 | - | |
| 8 | -| You use | Fastest setup | | |
| 9 | -| --- | --- | | |
| 10 | -| claude.ai, Claude Desktop or the Claude mobile apps | Add a custom connector with the URL `https://diffinery.com/mcp`. | | |
| 11 | -| Claude Code | Install the Diffinery plugin, then sign in when Claude Code asks. | | |
| 12 | -| Codex CLI | Install the Diffinery plugin (or `codex mcp add`), then `codex mcp login diffinery`. | | |
| 13 | -| CI, cloud sessions, anything without a browser | A token in `DIFFINERY_TOKEN`. See [Headless use](#headless-use). | | |
| 14 | - | |
| 15 | -## claude.ai and the Claude apps | |
| 16 | - | |
| 17 | -1. In claude.ai, open **Settings → Connectors** and choose **Add custom connector**. (On a Team or Enterprise plan, an owner adds it once under **Organization settings → Connectors**, and each member then connects their own account.) | |
| 18 | -2. Name it `Diffinery` and enter the URL `https://diffinery.com/mcp`. Leave the OAuth client ID and secret empty. | |
| 19 | -3. Choose **Connect**. claude.ai opens Diffinery: sign in with your passkey if you aren't already, then pick whom Claude acts as and its access, and choose **Connect** (see [The consent page](#the-consent-page)). | |
| 20 | -4. In a chat, turn on Diffinery in the tools menu. Claude can now read code, open and review pull requests, follow CI and read job logs, file issues and more, under the same permissions as the account you picked. | |
| 21 | - | |
| 22 | -The connector also works in Claude Desktop and the mobile apps, and in claude.ai Projects, including Claude sessions that work on a project's threads. It's listed on Diffinery under **Settings → Access tokens → Connected apps** as **Claude**, sending you back to `claude.ai`. | |
| 23 | - | |
| 24 | -## Claude Code | |
| 25 | - | |
| 26 | -### Claude Code with the plugin | |
| 27 | - | |
| 28 | -The Diffinery plugin adds the MCP server, a skill that teaches Claude Code how Diffinery repositories work (branches, `diffinery pr create` with an `intent`, waiting for checks, reading failed job logs, auto-merge), a `/diffinery:pr-status` command, and a hook that tells Claude when the repository it is in lives on Diffinery. Its source is [codician/diffinery-plugins](https://diffinery.com/codician/diffinery-plugins). | |
| 29 | - | |
| 30 | -```sh | |
| 31 | -claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git | |
| 32 | -claude plugin install diffinery@diffinery | |
| 33 | -``` | |
| 34 | - | |
| 35 | -Inside Claude Code, the same is `/plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git` and `/plugin install diffinery@diffinery`. Restart Claude Code, run `/mcp`, select **diffinery** and choose **Authenticate**; your browser opens Diffinery's consent page. On a machine without a browser, run `claude mcp login diffinery --no-browser`, open the printed link anywhere, and paste the address you land on back into the terminal. | |
| 36 | - | |
| 37 | -### Claude Code without the plugin | |
| 38 | - | |
| 39 | -```sh | |
| 40 | -claude mcp add --transport http diffinery https://diffinery.com/mcp | |
| 41 | -``` | |
| 42 | - | |
| 43 | -Then `/mcp` → **diffinery** → **Authenticate**, as above. To share the server with everyone on a project, put it in the project's `.mcp.json`; each person signs in with their own account: | |
| 44 | - | |
| 45 | -```json | |
| 46 | -{ "mcpServers": { "diffinery": { "type": "http", "url": "https://diffinery.com/mcp" } } } | |
| 47 | -``` | |
| 48 | - | |
| 49 | -### Git, pull requests and CI from Claude Code | |
| 50 | - | |
| 51 | -The MCP tools need no git at all. When Claude Code works in a clone instead, give it the CLI once: | |
| 52 | - | |
| 53 | -```sh | |
| 54 | -curl -fsSL https://diffinery.com/install | bash # Windows: irm https://diffinery.com/install.ps1 | iex | |
| 55 | -diffinery login # browser sign-in, or --device on a server | |
| 56 | -diffinery auth setup-git # git push and pull use the same sign-in | |
| 57 | -diffinery auth setup-gh # gh, and Claude Code's pull request and CI status | |
| 58 | -``` | |
| 59 | - | |
| 60 | -Then the usual loop works from Claude Code with no extra setup: push a branch, `diffinery pr create --title ... --intent ...`, `diffinery pr checks --watch` (exit 0 when all passed, 1 when any failed), `diffinery run logs <job-id>` for a failure, and `diffinery pr merge --auto --squash` to merge once the required checks and approvals are in. The pull request commands use the current branch's pull request when you leave out the number. | |
| 61 | - | |
| 62 | -`diffinery auth setup-gh` signs [gh](/docs/agents/github-cli) in to diffinery.com, which is also how Claude Code shows the branch's pull request with its CI status in the terminal and the desktop app. Claude Code can then use `gh` as it would on GitHub; the `diffinery` commands still know Diffinery's extras (`--intent`, `run logs`). | |
| 63 | - | |
| 64 | -## Codex | |
| 65 | - | |
| 66 | -### Codex with the plugin | |
| 67 | - | |
| 68 | -Codex reads the same plugin repository: | |
| 69 | - | |
| 70 | -```sh | |
| 71 | -codex plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git | |
| 72 | -codex plugin add diffinery@diffinery | |
| 73 | -codex mcp login diffinery | |
| 74 | -``` | |
| 75 | - | |
| 76 | -`codex mcp login` opens Diffinery's consent page in your browser; add `--no-browser` to print the link instead and paste back the address you land on. | |
| 77 | - | |
| 78 | -The plugin gives Codex the same skill, a `pr-status` skill in place of the command (Codex plugins have no commands), and the session hook, which Codex runs only once you trust it under `/hooks`. | |
| 79 | - | |
| 80 | -### Codex without the plugin | |
| 81 | - | |
| 82 | -```sh | |
| 83 | -codex mcp add diffinery --url https://diffinery.com/mcp | |
| 84 | -codex mcp login diffinery | |
| 85 | -``` | |
| 86 | - | |
| 87 | -or in `~/.codex/config.toml`: | |
| 88 | - | |
| 89 | -```toml | |
| 90 | -[mcp_servers.diffinery] | |
| 91 | -url = "https://diffinery.com/mcp" | |
| 92 | -``` | |
| 93 | - | |
| 94 | -`codex mcp list` shows the server with **Auth: OAuth** once it is signed in. Install the CLI as for Claude Code to let Codex push and drive pull requests from a clone. | |
| 95 | - | |
| 96 | -## Headless use | |
| 97 | - | |
| 98 | -Where nobody can open a browser (CI jobs, cloud agent sessions, scheduled tasks), give the client a token instead: an [agent](/docs/agents/overview)'s token is best, created under **Settings → Agents**. Put it in `DIFFINERY_TOKEN`, never in a file you commit. | |
| 99 | - | |
| 100 | -- **Claude Code:** `claude mcp add --scope user --transport http diffinery https://diffinery.com/mcp --header 'Authorization: Bearer ${DIFFINERY_TOKEN}'` (with the plugin installed, this entry takes the place of the plugin's own), or in `.mcp.json`, `"headers": { "Authorization": "Bearer ${DIFFINERY_TOKEN}" }`. `diffinery mcp-config` prints this snippet. | |
| 101 | -- **Codex:** `codex mcp add diffinery --url https://diffinery.com/mcp --bearer-token-env-var DIFFINERY_TOKEN`, or `bearer_token_env_var = "DIFFINERY_TOKEN"` under `[mcp_servers.diffinery]`; it takes the place of the plugin's entry. The token and the browser sign-in can't share one entry: with the header set, neither client falls back to OAuth. | |
| 102 | -- **git, gh and the CLI:** the CLI reads `DIFFINERY_TOKEN`; run `diffinery auth setup-git` once so git uses it too, and `diffinery auth setup-gh` for gh. | |
| 103 | -- **Claude Code on the web and other cloud sessions:** allow `diffinery.com` in the environment's network settings, add `DIFFINERY_TOKEN` as an environment secret, and clone over HTTPS. | |
| 104 | - | |
| 105 | -## How sign-in works | |
| 106 | - | |
| 107 | -Diffinery follows the [MCP authorization spec](https://modelcontextprotocol.io/specification/draft/basic/authorization) (OAuth 2.1), so any MCP client that supports it can connect, not only Claude and Codex. | |
| 108 | - | |
| 109 | -1. The client calls `/mcp` without a token and gets `401` with `WWW-Authenticate: Bearer resource_metadata="https://diffinery.com/.well-known/oauth-protected-resource/mcp"`. | |
| 110 | -2. It reads that document and the authorization server's at `/.well-known/oauth-authorization-server`. | |
| 111 | -3. It identifies itself: by the URL of its own client metadata document (Claude Code uses `https://claude.ai/oauth/claude-code-client-metadata`, Codex `https://chatgpt.com/oauth/codex/client.json`), or by registering at `/oauth/register` (RFC 7591), as claude.ai and most other clients do. | |
| 112 | -4. It sends you to `/oauth/authorize` with a PKCE challenge. You approve on [the consent page](#the-consent-page). | |
| 113 | -5. Diffinery sends your browser back to the address the app registered, with a one-time code that only works together with the PKCE secret the app kept. The app trades it at `/oauth/token` for an access token and a refresh token. | |
| 114 | - | |
| 115 | -| | | | |
| 116 | -| --- | --- | | |
| 117 | -| Access token | An ordinary Diffinery token (`dfy_…`) for the account you picked, with the access you picked. It lasts 8 hours and works everywhere a token does (MCP, the REST API, git), with every permission check and branch rule that applies to that account. | | |
| 118 | -| Refresh token | `dfr_…`. Each use returns a new one and retires the old. A connection that isn't used for 30 days lapses. A retired refresh token used again later is taken as stolen and ends the connection. | | |
| 119 | -| Redirect addresses | `https` anywhere, `http` only on your own computer (`localhost`, `127.0.0.1`), or an app's own scheme such as `cursor://`. Loopback addresses may use any port. | | |
| 120 | -| Endpoints | `/.well-known/oauth-protected-resource/mcp`, `/.well-known/oauth-authorization-server`, `/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/revoke` (RFC 7009). | | |
| 121 | -| Grants | Authorization code with PKCE (S256 only), and refresh tokens. No implicit, password or client credentials grants. | | |
| 122 | - | |
| 123 | -### The consent page | |
| 124 | - | |
| 125 | -The page names the app and where it will send you back. An app identified by a client metadata document shows the site that publishes it ("An app published by claude.ai"); an app that registered itself shows the name it chose, marked as not checked by Diffinery. Only connect an app you are setting up yourself at that moment. | |
| 126 | - | |
| 127 | -- **Act as:** yourself, or one of your agents (yours, or an organization's you own or administer). Acting as an agent is a good default for an AI app: its commits and comments show as the agent's, on your behalf, and its pull requests need a person's approval, so nothing it does lands unreviewed. | |
| 128 | -- **Access:** Read, Write (preselected when the app asks for more) or Admin. Admin is never preselected, and isn't offered for a site administrator's own account: an app would carry site-wide administration in a token. | |
| 129 | - | |
| 130 | -Approving needs a passkey sign-in from the last 2 hours, like creating a token. | |
| 131 | - | |
| 132 | -### Connected apps | |
| 133 | - | |
| 134 | -**Settings → Access tokens → Connected apps** lists every app you connected: its name, where it sends you back, whom it acts as, its access, and when it was last used. **Disconnect** ends the connection and its tokens at once. Each approval is its own connection, so the same app on two computers shows twice. Connections and disconnections are in the [audit log](/docs/security/audit-log) as `oauth.connect` and `oauth.disconnect`. | |
| 135 | - | |
| 136 | -## Troubleshooting | |
| 137 | - | |
| 138 | -| Symptom | Cause and fix | | |
| 139 | -| --- | --- | | |
| 140 | -| The browser says "Can't connect this app" | The app's registration expired (registrations nobody used are removed after a week), or it asked to send you back to an address it didn't register. Remove the server or connector and add it again. | | |
| 141 | -| Claude Code shows the server as needing authentication | Run `/mcp` and choose **Authenticate**, or `claude mcp login diffinery`. | | |
| 142 | -| `codex mcp list` says **Not logged in** | Run `codex mcp login diffinery`. | | |
| 143 | -| An app that worked asks you to sign in again | The connection lapsed after 30 days unused, or it was disconnected (by you, or after a reused refresh token). Sign in again. | | |
| 144 | -| Tools fail with `this token needs the write scope` | You connected with Read access. Disconnect it under Connected apps and connect again with Write. | | |
| 145 | -| A tool fails with `agents may not push to protected or default branches` | The app acts as an agent. It should work on a branch and open a pull request; see [What agents can and can't do](/docs/agents/overview). | |
modifiedapps/diffinery/docs/agents/mcp.md+2−2View file
| @@ -30,7 +30,7 @@ | ||
| 30 | 30 | WWW-Authenticate: Bearer realm="Diffinery", resource_metadata="https://diffinery.com/.well-known/oauth-protected-resource/mcp" |
| 31 | 31 | ``` |
| 32 | 32 | |
| 33 | -The client then opens Diffinery in your browser, where you choose whom it acts as (you, or one of your [agents](/docs/agents/overview)) and its access, and connect it. It gets tokens that last 8 hours and renew themselves; the connection is listed under **Settings → Access tokens → Connected apps**, where you can disconnect it. See [Claude and Codex](/docs/agents/claude-and-codex#how-sign-in-works) for the details. | |
| 33 | +The client then opens Diffinery in your browser, where you choose whom it acts as (you, or one of your [agents](/docs/agents/overview)) and its access, and connect it. It gets tokens that last 8 hours and renew themselves; the connection is listed under **Settings → Access tokens → Connected apps**, where you can disconnect it. See [How sign-in works](/docs/connect/how-sign-in-works) for the details. | |
| 34 | 34 | |
| 35 | 35 | **A token in the header.** For scripts, CI and cloud sessions where nobody can click through a browser, send a Diffinery token: |
| 36 | 36 | |
| @@ -50,7 +50,7 @@ | ||
| 50 | 50 | |
| 51 | 51 | ## Connect your client |
| 52 | 52 | |
| 53 | -For claude.ai, Claude Code and Codex, the [Claude and Codex](/docs/agents/claude-and-codex) page has the shortest path: a connector or a plugin, and a browser sign-in. The snippets below use a token in the header instead, for clients without OAuth and for headless use. Create the token in the web UI: for an agent, in **Settings → Agents**; for yourself, in **Settings → Access tokens**. Tokens can't create tokens. Keep it out of files you commit; most clients below can read it from an environment variable. | |
| 53 | +For Claude, Claude Code, Codex and other apps with browser sign-in, [Connect AI apps](/docs/connect) has step-by-step guides: a connector or a plugin, and a browser sign-in. The snippets below use a token in the header instead, for clients without OAuth and for headless use. Create the token in the web UI: for an agent, in **Settings → Agents**; for yourself, in **Settings → Access tokens**. Tokens can't create tokens. Keep it out of files you commit; most clients below can read it from an environment variable. | |
| 54 | 54 | |
| 55 | 55 | ### Claude Code |
| 56 | 56 |
modifiedapps/diffinery/docs/agents/overview.md+1−1View file
| @@ -237,7 +237,7 @@ | ||
| 237 | 237 | |
| 238 | 238 | ### Connect over MCP |
| 239 | 239 | |
| 240 | -claude.ai, Claude Code and Codex can sign in through your browser instead of using a token, and a plugin teaches Claude Code and Codex how Diffinery repositories work: see [Claude and Codex](/docs/agents/claude-and-codex). With a token, for Claude Code: | |
| 240 | +claude.ai, Claude Code and Codex can sign in through your browser instead of using a token, and a plugin teaches Claude Code and Codex how Diffinery repositories work: see [Connect AI apps](/docs/connect). With a token, for Claude Code: | |
| 241 | 241 | |
| 242 | 242 | ```sh |
| 243 | 243 | claude mcp add --transport http diffinery https://diffinery.com/mcp \ |
addedapps/diffinery/docs/connect.md+52−0View file
| @@ -1,0 +1,52 @@ | ||
| 1 | +--- | |
| 2 | +title: Connect AI apps | |
| 3 | +description: Add Diffinery to Claude, Claude Code, Codex or any MCP app with one URL and a browser sign-in. Pick your app and follow its guide. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +Diffinery works with AI apps through its [MCP server](/docs/agents/mcp). Every app connects to the same address: | |
| 7 | + | |
| 8 | +```text | |
| 9 | +https://diffinery.com/mcp | |
| 10 | +``` | |
| 11 | + | |
| 12 | +There is no token to copy. The app sends you to Diffinery in your browser, you choose whom it acts as and what it may do, and it's connected. You can see and disconnect every app under **Settings → Access tokens → Connected apps**, which also shows this URL with a copy button. | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | +## Pick your app | |
| 17 | + | |
| 18 | +| You use | What you do | Guide | | |
| 19 | +| --- | --- | --- | | |
| 20 | +| **Claude** on claude.ai, Claude Desktop or the Claude mobile apps | Add a custom connector with the URL, then approve it on Diffinery. | [Claude](/docs/connect/claude) | | |
| 21 | +| **Claude Code** | Install the Diffinery plugin, then sign in from `/mcp`. | [Claude Code](/docs/connect/claude-code) | | |
| 22 | +| **Codex** (OpenAI's coding agent) | Install the Diffinery plugin, then run `codex mcp login diffinery`. | [Codex](/docs/connect/codex) | | |
| 23 | +| **Cursor, VS Code or another MCP app** | Add the URL as a remote (HTTP) MCP server. | [Other apps](/docs/connect/other-apps) | | |
| 24 | +| **CI, cloud agents, scripts** (no browser) | Send an agent's token instead of signing in. | [Headless use](/docs/connect/how-sign-in-works#headless-use) | | |
| 25 | + | |
| 26 | +## What a connected app can do | |
| 27 | + | |
| 28 | +Once connected, the app works with Diffinery the way you would, within the access you gave it: | |
| 29 | + | |
| 30 | +- Read code, branches and commits, compare branches, and commit files to a new branch without git. | |
| 31 | +- Open pull requests with an [intent](/docs/collaboration/pull-requests), comment, review, merge, or turn on auto-merge. | |
| 32 | +- Follow CI: list workflow runs, read the log of a failing job, and re-run or cancel runs. | |
| 33 | +- File and update issues, and draft and publish releases. | |
| 34 | +- Search and read these docs. | |
| 35 | + | |
| 36 | +The [MCP tool reference](/docs/reference/mcp-tools) lists every tool. In a terminal, Claude Code and Codex can also use git and the [`diffinery` CLI](/docs/reference/cli), which the Claude Code and Codex guides set up. | |
| 37 | + | |
| 38 | +## You stay in control | |
| 39 | + | |
| 40 | +- **Act as an agent.** On the consent page you pick whether the app acts as you or as one of your [agents](/docs/agents/overview). An agent is the safer choice for an AI app: its commits and comments are marked as the agent's, it can't push to protected or default branches, and its pull requests need a person's approval unless a branch rule says otherwise. | |
| 41 | +- **Choose the access.** Read, Write or Admin. Write is enough for pull requests, reviews and issues. Admin is never preselected. | |
| 42 | +- **Disconnect any time.** **Settings → Access tokens → Connected apps** lists every app with whom it acts as, its access and when it was last used. **Disconnect** ends it at once. | |
| 43 | + | |
| 44 | +How the sign-in works, and what each part of the consent page means, is in [How sign-in works](/docs/connect/how-sign-in-works). | |
| 45 | + | |
| 46 | +## For AI agents reading this page | |
| 47 | + | |
| 48 | +If you are an agent setting yourself up: | |
| 49 | + | |
| 50 | +- Running in a terminal with a person nearby: ask them to install the plugin for your app ([Claude Code](/docs/connect/claude-code), [Codex](/docs/connect/codex)) and approve the sign-in. Don't try to complete a browser sign-in yourself. | |
| 51 | +- Running headless with `DIFFINERY_TOKEN` set: add the server with an `Authorization: Bearer ${DIFFINERY_TOKEN}` header, as in [Headless use](/docs/connect/how-sign-in-works#headless-use). `diffinery mcp-config` prints the snippet. | |
| 52 | +- Either way, `diffinery auth setup-git` lets git push with the same sign-in, and `diffinery auth setup-gh` makes `gh` work against diffinery.com ([GitHub CLI](/docs/agents/github-cli)). |
addedapps/diffinery/docs/connect/claude-code.md+125−0View file
| @@ -1,0 +1,125 @@ | ||
| 1 | +--- | |
| 2 | +title: Claude Code | |
| 3 | +description: Install the Diffinery plugin for Claude Code, sign in from /mcp, and let Claude push branches, open pull requests and follow CI on Diffinery. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +The Diffinery plugin for Claude Code adds the Diffinery MCP server, a skill that teaches Claude how Diffinery repositories work, a `/diffinery:pr-status` command, and a hook that tells Claude when the repository it's in is hosted on Diffinery. Together with the `diffinery` CLI, Claude can then push a branch, open a pull request, watch CI and fix what fails, without leaving the terminal. | |
| 7 | + | |
| 8 | +## 1. Install the CLI | |
| 9 | + | |
| 10 | +The CLI gives Claude git access and the pull request commands, and the plugin's source is fetched with git: | |
| 11 | + | |
| 12 | +```sh | |
| 13 | +curl -fsSL https://diffinery.com/install | bash | |
| 14 | +diffinery login | |
| 15 | +diffinery auth setup-git | |
| 16 | +``` | |
| 17 | + | |
| 18 | +On Windows, in PowerShell: `irm https://diffinery.com/install.ps1 | iex`. `diffinery login` opens your browser to sign in (add `--device` on a machine without one), and `diffinery auth setup-git` lets git push and pull with the same sign-in. See [Install and sign in to the CLI](/docs/accounts/cli-sign-in). | |
| 19 | + | |
| 20 | +> [!NOTE] | |
| 21 | +> While the plugin's repository is open only to Diffinery members, `diffinery auth setup-git` is what lets Claude Code fetch it. Without it, adding the marketplace fails with an authentication error. | |
| 22 | + | |
| 23 | +## 2. Install the plugin | |
| 24 | + | |
| 25 | +```sh | |
| 26 | +claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git | |
| 27 | +claude plugin install diffinery@diffinery | |
| 28 | +``` | |
| 29 | + | |
| 30 | +Inside Claude Code, the same is `/plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git` and `/plugin install diffinery@diffinery`. Restart Claude Code (or run `/reload-plugins`) to load it. | |
| 31 | + | |
| 32 | +## 3. Sign in | |
| 33 | + | |
| 34 | +1. In Claude Code, run `/mcp`, select **plugin:diffinery:diffinery** and choose **Authenticate**. | |
| 35 | +2. Your browser opens Diffinery's consent page. Choose whom Claude Code acts as and its access, then choose **Connect**. | |
| 36 | +3. Back in the terminal, `/mcp` shows the server as connected. | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | +On a machine without a browser, run `claude mcp login plugin:diffinery:diffinery --no-browser` (or `claude mcp login diffinery` without the plugin), open the printed link on any device, and paste the address you land on back into the terminal. | |
| 41 | + | |
| 42 | +> [!TIP] | |
| 43 | +> Act as one of your [agents](/docs/agents/overview). Claude Code's commits, comments and pull requests then show as the agent's, and they wait for a person's approval before they land. | |
| 44 | + | |
| 45 | +## Check that it works | |
| 46 | + | |
| 47 | +```sh | |
| 48 | +claude mcp list | |
| 49 | +``` | |
| 50 | + | |
| 51 | +`plugin:diffinery:diffinery` shows **Connected** (before you sign in it shows **Needs authentication**). Then try it: | |
| 52 | + | |
| 53 | +```sh | |
| 54 | +claude -p "Who am I on Diffinery?" | |
| 55 | +``` | |
| 56 | + | |
| 57 | +In a clone of a Diffinery repository, `/diffinery:pr-status` shows the current branch's pull request, its checks, and the end of the log of any job that failed. | |
| 58 | + | |
| 59 | +## What the plugin adds | |
| 60 | + | |
| 61 | +| Part | What it does | | |
| 62 | +| --- | --- | | |
| 63 | +| MCP server | `https://diffinery.com/mcp` with browser sign-in: every [Diffinery tool](/docs/reference/mcp-tools). | | |
| 64 | +| `diffinery` skill | Teaches Claude the loop on Diffinery: branch, `diffinery pr create --intent`, `diffinery pr checks --watch`, read failing logs with `diffinery run logs`, auto-merge, and what agents can and can't do. | | |
| 65 | +| `/diffinery:pr-status` | This branch's pull request, its checks, and the log tail of failing jobs. Changes nothing. | | |
| 66 | +| Session hook | In a Diffinery clone, tells Claude the repository is hosted on Diffinery and whether the CLI is installed and signed in. Elsewhere it stays silent. It never uses the network. | | |
| 67 | + | |
| 68 | +To update it later: `claude plugin marketplace update diffinery`, then `claude plugin update diffinery@diffinery`. | |
| 69 | + | |
| 70 | +## Without the plugin | |
| 71 | + | |
| 72 | +Add only the MCP server: | |
| 73 | + | |
| 74 | +```sh | |
| 75 | +claude mcp add --transport http diffinery https://diffinery.com/mcp | |
| 76 | +``` | |
| 77 | + | |
| 78 | +Then `/mcp` → **diffinery** → **Authenticate**, as above. To share it with everyone working on a repository, commit it in the repository's `.mcp.json`; each person signs in with their own account: | |
| 79 | + | |
| 80 | +```json | |
| 81 | +{ "mcpServers": { "diffinery": { "type": "http", "url": "https://diffinery.com/mcp" } } } | |
| 82 | +``` | |
| 83 | + | |
| 84 | +## Pull requests and CI from the terminal | |
| 85 | + | |
| 86 | +With the CLI installed, Claude Code works through the usual loop on its own: | |
| 87 | + | |
| 88 | +```sh | |
| 89 | +git push -u origin my-branch | |
| 90 | +diffinery pr create --title "Fix the login redirect" --intent "Sign-in should return people to the page they came from" | |
| 91 | +diffinery pr checks --watch # exit 0 when all checks passed, 1 when any failed | |
| 92 | +diffinery run logs <job-id> # the log of a failed job | |
| 93 | +diffinery pr merge --auto --squash | |
| 94 | +``` | |
| 95 | + | |
| 96 | +The pull request commands use the current branch's pull request when you leave out the number. | |
| 97 | + | |
| 98 | +To let Claude use `gh` as it would on GitHub, and to see the branch's pull request with its CI status in Claude Code's footer and the desktop app, sign gh in to Diffinery once: | |
| 99 | + | |
| 100 | +```sh | |
| 101 | +diffinery auth setup-gh | |
| 102 | +``` | |
| 103 | + | |
| 104 | +See [GitHub CLI](/docs/agents/github-cli) for what works with `gh`. | |
| 105 | + | |
| 106 | +## Headless: CI and cloud sessions | |
| 107 | + | |
| 108 | +Where nobody can approve a browser sign-in, give Claude Code an [agent's token](/docs/agents/overview) in `DIFFINERY_TOKEN` and add a server that sends it: | |
| 109 | + | |
| 110 | +```sh | |
| 111 | +claude mcp add --scope user --transport http diffinery https://diffinery.com/mcp \ | |
| 112 | + --header 'Authorization: Bearer ${DIFFINERY_TOKEN}' | |
| 113 | +``` | |
| 114 | + | |
| 115 | +Keep the single quotes: Claude Code reads `${DIFFINERY_TOKEN}` from the environment each time it connects, so the token is never written to its settings. This entry takes the place of the plugin's own server. The CLI and `diffinery auth setup-git` read `DIFFINERY_TOKEN` too. In Claude Code on the web, allow `diffinery.com` in the environment's network settings and add `DIFFINERY_TOKEN` as an environment secret. More in [Headless use](/docs/connect/how-sign-in-works#headless-use). | |
| 116 | + | |
| 117 | +## Troubleshooting | |
| 118 | + | |
| 119 | +| Problem | What to do | | |
| 120 | +| --- | --- | | |
| 121 | +| `claude plugin marketplace add` fails with an authentication error | Run `diffinery login` and `diffinery auth setup-git` first (step 1). | | |
| 122 | +| `/mcp` shows the server as needing authentication | Choose **Authenticate**, or run `claude mcp login plugin:diffinery:diffinery`. | | |
| 123 | +| The browser says "Can't connect this app" | Remove the server and add it again, then sign in. | | |
| 124 | +| A tool says it needs the write scope | You connected with **Read**. Disconnect Claude Code under **Settings → Access tokens → Connected apps** and sign in again with **Write**. | | |
| 125 | +| `git push` to `main` fails with "agents may not push to protected or default branches" | Claude Code acts as an agent: push a branch and open a pull request. | |
addedapps/diffinery/docs/connect/claude.md+62−0View file
| @@ -1,0 +1,62 @@ | ||
| 1 | +--- | |
| 2 | +title: Claude | |
| 3 | +description: Add Diffinery to claude.ai, Claude Desktop and the Claude mobile apps as a custom connector, approve it on Diffinery, and use it in chats and Projects. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +Add Diffinery to Claude once as a custom connector, and Claude can read your repositories, open and review pull requests, follow CI and file issues from any chat. The connector works on claude.ai, in Claude Desktop and in the Claude mobile apps. | |
| 7 | + | |
| 8 | +You need a Diffinery account and a Claude plan that allows custom connectors. For Claude Code in a terminal, see [Claude Code](/docs/connect/claude-code) instead. | |
| 9 | + | |
| 10 | +## Add the connector | |
| 11 | + | |
| 12 | +1. On [claude.ai](https://claude.ai) or in Claude Desktop, open **Settings → Connectors** and choose **Add custom connector**. | |
| 13 | +2. Fill in the two fields and choose **Add**: | |
| 14 | + | |
| 15 | + | Field | Value | | |
| 16 | + | --- | --- | | |
| 17 | + | Name | `Diffinery` | | |
| 18 | + | Remote MCP server URL | `https://diffinery.com/mcp` | | |
| 19 | + | |
| 20 | + Leave **Advanced settings** (OAuth client ID and secret) empty: Claude registers itself with Diffinery. | |
| 21 | +3. Choose **Connect** next to Diffinery. A Diffinery tab opens. Sign in with your passkey if you aren't signed in. | |
| 22 | +4. On the consent page, choose whom Claude acts as and its access, then choose **Connect**. The tab returns to Claude and the connector shows as connected. | |
| 23 | + | |
| 24 | +> [!TIP] | |
| 25 | +> Pick one of your [agents](/docs/agents/overview) under **Act as** and **Write** access. Claude can then open pull requests and comment as that agent, and its pull requests wait for a person's approval like any agent's. | |
| 26 | + | |
| 27 | +> [!NOTE] | |
| 28 | +> On a Team or Enterprise plan, an owner adds the connector once for the organization (in Claude's organization settings, under **Connectors**). Each member then chooses **Connect** and approves it with their own Diffinery account, so everyone's Claude acts with their own permissions. | |
| 29 | + | |
| 30 | +## Use it in a chat | |
| 31 | + | |
| 32 | +Open the tools menu below the message box and make sure **Diffinery** is turned on. Then ask in plain words, for example: | |
| 33 | + | |
| 34 | +- "List my open pull requests on Diffinery and tell me which ones are waiting on CI." | |
| 35 | +- "Why did the last workflow run on acme/widgets fail? Read the failing job's log." | |
| 36 | +- "Open an issue in acme/widgets: the login page doesn't load on Safari." | |
| 37 | +- "Review pull request 42 in acme/widgets and comment on anything risky." | |
| 38 | + | |
| 39 | +Claude asks before it uses a tool that changes something, unless you allowed that tool to run without asking. | |
| 40 | + | |
| 41 | +The connector also works in Claude Projects, and in Claude Desktop and the mobile apps once you've added it on claude.ai or in Desktop. | |
| 42 | + | |
| 43 | +## Check that it works | |
| 44 | + | |
| 45 | +- In Claude, **Settings → Connectors** shows Diffinery as connected. | |
| 46 | +- On Diffinery, **Settings → Access tokens → Connected apps** lists **Claude**, with whom it acts as and its access. | |
| 47 | +- Ask Claude "Who am I on Diffinery?". It calls the `whoami` tool and answers with the account you picked. | |
| 48 | + | |
| 49 | +## Disconnect | |
| 50 | + | |
| 51 | +On Diffinery, choose **Disconnect** next to Claude under **Settings → Access tokens → Connected apps**. Its tokens stop working at once. To remove the connector from Claude too, remove it under **Settings → Connectors**. | |
| 52 | + | |
| 53 | +## Troubleshooting | |
| 54 | + | |
| 55 | +| Problem | What to do | | |
| 56 | +| --- | --- | | |
| 57 | +| The Diffinery tab says "Can't connect this app" | Claude's registration expired (registrations nobody used are removed after a week), or it asked to return somewhere it didn't register. Remove the connector in Claude and add it again. | | |
| 58 | +| Claude says a tool needs the write scope | You connected with **Read** access. Disconnect Claude on Diffinery and connect again with **Write**. | | |
| 59 | +| A tool fails with "agents may not push to protected or default branches" | Claude acts as an agent, which works on branches and opens pull requests. Ask it to open a pull request instead. | | |
| 60 | +| Claude asks you to connect again | The connection lapsed after 30 days without use, or it was disconnected. Choose **Connect** again. | | |
| 61 | + | |
| 62 | +More detail on the consent page and what the tokens can do: [How sign-in works](/docs/connect/how-sign-in-works). |
addedapps/diffinery/docs/connect/codex.md+103−0View file
| @@ -1,0 +1,103 @@ | ||
| 1 | +--- | |
| 2 | +title: Codex | |
| 3 | +description: Install the Diffinery plugin for OpenAI's Codex, sign in with codex mcp login, and let Codex work with pull requests, CI and issues on Diffinery. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +Codex, OpenAI's coding agent, reads the same Diffinery plugin as Claude Code. It adds the Diffinery MCP server, a skill that teaches Codex how Diffinery repositories work, a `pr-status` skill, and a session hook that tells Codex when it's working in a Diffinery clone. | |
| 7 | + | |
| 8 | +## 1. Install the CLI | |
| 9 | + | |
| 10 | +```sh | |
| 11 | +curl -fsSL https://diffinery.com/install | bash | |
| 12 | +diffinery login | |
| 13 | +diffinery auth setup-git | |
| 14 | +``` | |
| 15 | + | |
| 16 | +On Windows, in PowerShell: `irm https://diffinery.com/install.ps1 | iex`. The CLI gives Codex git access and the pull request commands, and `diffinery auth setup-git` also lets Codex fetch the plugin's repository, which is open only to Diffinery members for now. See [Install and sign in to the CLI](/docs/accounts/cli-sign-in). | |
| 17 | + | |
| 18 | +## 2. Install the plugin | |
| 19 | + | |
| 20 | +```sh | |
| 21 | +codex plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git | |
| 22 | +codex plugin add diffinery@diffinery | |
| 23 | +``` | |
| 24 | + | |
| 25 | +`codex plugin list` now shows `diffinery@diffinery` as installed and enabled. | |
| 26 | + | |
| 27 | +## 3. Sign in | |
| 28 | + | |
| 29 | +```sh | |
| 30 | +codex mcp login diffinery | |
| 31 | +``` | |
| 32 | + | |
| 33 | +Codex opens Diffinery's consent page in your browser. Choose whom Codex acts as and its access, then choose **Connect**. | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | +Without a browser on the machine, add `--no-browser`: Codex prints the link, you open it on any device, and paste the address you land on back into the terminal. | |
| 38 | + | |
| 39 | +> [!TIP] | |
| 40 | +> Act as one of your [agents](/docs/agents/overview), so Codex's commits and pull requests are marked as the agent's and wait for a person's approval. | |
| 41 | + | |
| 42 | +## Check that it works | |
| 43 | + | |
| 44 | +```sh | |
| 45 | +codex mcp list | |
| 46 | +``` | |
| 47 | + | |
| 48 | +The `diffinery` server shows **OAuth** under **Auth** (before you sign in it says **Not logged in**). Then ask Codex: "Who am I on Diffinery?" | |
| 49 | + | |
| 50 | +## What the plugin adds | |
| 51 | + | |
| 52 | +| Part | What it does | | |
| 53 | +| --- | --- | | |
| 54 | +| MCP server | `https://diffinery.com/mcp` with browser sign-in: every [Diffinery tool](/docs/reference/mcp-tools). | | |
| 55 | +| `diffinery` skill | The loop on Diffinery: branch, `diffinery pr create --intent`, `diffinery pr checks --watch`, `diffinery run logs`, auto-merge, and the agent rules. | | |
| 56 | +| `pr-status` skill | This branch's pull request, its checks, and the log tail of failing jobs. (Codex plugins have no slash commands, so this is a skill.) | | |
| 57 | +| Session hook | In a Diffinery clone, tells Codex the repository is hosted on Diffinery and whether the CLI is installed and signed in. Codex runs plugin hooks only after you trust them: open `/hooks` in Codex and trust it. The skills work without it. | | |
| 58 | + | |
| 59 | +To update it later: `codex plugin marketplace upgrade diffinery`, then `codex plugin add diffinery@diffinery` again. | |
| 60 | + | |
| 61 | +## Without the plugin | |
| 62 | + | |
| 63 | +```sh | |
| 64 | +codex mcp add diffinery --url https://diffinery.com/mcp | |
| 65 | +codex mcp login diffinery | |
| 66 | +``` | |
| 67 | + | |
| 68 | +or in `~/.codex/config.toml`: | |
| 69 | + | |
| 70 | +```toml | |
| 71 | +[mcp_servers.diffinery] | |
| 72 | +url = "https://diffinery.com/mcp" | |
| 73 | +``` | |
| 74 | + | |
| 75 | +## Pull requests and CI | |
| 76 | + | |
| 77 | +With the CLI installed, Codex pushes a branch, opens a pull request with `diffinery pr create --intent ...`, waits with `diffinery pr checks --watch`, reads failures with `diffinery run logs <job-id>`, and lands it with `diffinery pr merge --auto --squash`. For `gh`, run `diffinery auth setup-gh` once; see [GitHub CLI](/docs/agents/github-cli). | |
| 78 | + | |
| 79 | +## Headless: CI and cloud sessions | |
| 80 | + | |
| 81 | +Give Codex an [agent's token](/docs/agents/overview) in `DIFFINERY_TOKEN` and point the server at it. It takes the place of the plugin's server: | |
| 82 | + | |
| 83 | +```sh | |
| 84 | +codex mcp add diffinery --url https://diffinery.com/mcp --bearer-token-env-var DIFFINERY_TOKEN | |
| 85 | +``` | |
| 86 | + | |
| 87 | +or, for one run without changing `config.toml`: | |
| 88 | + | |
| 89 | +```sh | |
| 90 | +codex -c 'mcp_servers.diffinery.url="https://diffinery.com/mcp"' \ | |
| 91 | + -c 'mcp_servers.diffinery.bearer_token_env_var="DIFFINERY_TOKEN"' exec "..." | |
| 92 | +``` | |
| 93 | + | |
| 94 | +`codex mcp list` then shows **Bearer token** under **Auth**. More in [Headless use](/docs/connect/how-sign-in-works#headless-use). | |
| 95 | + | |
| 96 | +## Troubleshooting | |
| 97 | + | |
| 98 | +| Problem | What to do | | |
| 99 | +| --- | --- | | |
| 100 | +| `codex plugin marketplace add` fails to fetch | Run `diffinery login` and `diffinery auth setup-git` first (step 1). | | |
| 101 | +| `codex mcp list` says **Not logged in** | Run `codex mcp login diffinery`. | | |
| 102 | +| The browser says "Can't connect this app" | Run `codex mcp logout diffinery`, then `codex mcp login diffinery` again. | | |
| 103 | +| A tool says it needs the write scope | You connected with **Read**. Disconnect Codex under **Settings → Access tokens → Connected apps** and sign in again with **Write**. | |
addedapps/diffinery/docs/connect/how-sign-in-works.md+60−0View file
| @@ -1,0 +1,60 @@ | ||
| 1 | +--- | |
| 2 | +title: How sign-in works | |
| 3 | +description: How AI apps sign in to Diffinery's MCP server with OAuth 2.1, what the consent page asks, Connected apps, tokens, and headless use with an agent token. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +Diffinery follows the [MCP authorization spec](https://modelcontextprotocol.io/specification/draft/basic/authorization) (OAuth 2.1), so any MCP app that supports it can connect with nothing but the URL, not only Claude and Codex. | |
| 7 | + | |
| 8 | +## The steps | |
| 9 | + | |
| 10 | +1. The app calls `https://diffinery.com/mcp` without a token and gets `401` with `WWW-Authenticate: Bearer resource_metadata="https://diffinery.com/.well-known/oauth-protected-resource/mcp"`. | |
| 11 | +2. It reads that document and the authorization server's at `/.well-known/oauth-authorization-server`. | |
| 12 | +3. It identifies itself: by the URL of its own client metadata document (Claude Code uses `https://claude.ai/oauth/claude-code-client-metadata`, Codex `https://chatgpt.com/oauth/codex/client.json`), or by registering at `/oauth/register` (RFC 7591), as claude.ai and most other apps do. | |
| 13 | +4. It sends you to `/oauth/authorize` with a PKCE challenge, and you approve it on [the consent page](#the-consent-page). | |
| 14 | +5. Diffinery sends your browser back to the address the app registered with a one-time code, which only works together with the PKCE secret the app kept. The app trades it at `/oauth/token` for an access token and a refresh token. | |
| 15 | + | |
| 16 | +| | | | |
| 17 | +| --- | --- | | |
| 18 | +| Access token | An ordinary Diffinery token (`dfy_…`) for the account you picked, with the access you picked. It lasts 8 hours and works everywhere a token does (MCP, the REST API, git), with every permission check and branch rule that applies to that account. | | |
| 19 | +| Refresh token | `dfr_…`. Each use returns a new one and retires the old. A connection that isn't used for 30 days lapses. A retired refresh token used again later is taken as stolen and ends the connection. | | |
| 20 | +| Redirect addresses | `https` anywhere, `http` only on your own computer (`localhost`, `127.0.0.1`), or an app's own scheme such as `cursor://`. Loopback addresses may use any port. | | |
| 21 | +| Endpoints | `/.well-known/oauth-protected-resource/mcp`, `/.well-known/oauth-authorization-server`, `/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/revoke` (RFC 7009). | | |
| 22 | +| Grants | Authorization code with PKCE (S256 only), and refresh tokens. No implicit, password or client credentials grants. | | |
| 23 | + | |
| 24 | +## The consent page | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | +The page names the app and where it will send you back. An app identified by a client metadata document shows the site that publishes it ("An app published by claude.ai"); an app that registered itself shows the name it chose, marked as not checked by Diffinery. Only connect an app you are setting up yourself at that moment. | |
| 29 | + | |
| 30 | +- **Act as:** yourself, or one of your agents (yours, or an organization's you own or administer). Acting as an agent is a good default for an AI app: its commits and comments show as the agent's, on your behalf, and its pull requests need a person's approval, so nothing it does lands unreviewed. | |
| 31 | +- **Access:** Read, Write (preselected when the app asks for more) or Admin. Admin is never preselected, and isn't offered for a site administrator's own account: an app would carry site-wide administration in a token. | |
| 32 | + | |
| 33 | +Approving needs a passkey sign-in from the last 2 hours, as creating a token does. | |
| 34 | + | |
| 35 | +## Connected apps | |
| 36 | + | |
| 37 | +**Settings → Access tokens → Connected apps** lists every app you connected: its name, where it sends you back, whom it acts as, its access, and when it was last used. **Disconnect** ends the connection and its tokens at once. Each approval is its own connection, so the same app on two computers shows twice. Connections and disconnections are in the [audit log](/docs/security/audit-log) as `oauth.connect` and `oauth.disconnect`. | |
| 38 | + | |
| 39 | +## Headless use | |
| 40 | + | |
| 41 | +Where nobody can open a browser (CI jobs, cloud agent sessions, scheduled tasks), give the app a token instead: an [agent](/docs/agents/overview)'s token is best, created under **Settings → Agents**. Put it in `DIFFINERY_TOKEN`, never in a file you commit. | |
| 42 | + | |
| 43 | +| App | Setup | | |
| 44 | +| --- | --- | | |
| 45 | +| Claude Code | `claude mcp add --scope user --transport http diffinery https://diffinery.com/mcp --header 'Authorization: Bearer ${DIFFINERY_TOKEN}'`, or `"headers": { "Authorization": "Bearer ${DIFFINERY_TOKEN}" }` in `.mcp.json`. With the plugin installed, this entry takes the place of the plugin's own. | | |
| 46 | +| Codex | `codex mcp add diffinery --url https://diffinery.com/mcp --bearer-token-env-var DIFFINERY_TOKEN`, or `bearer_token_env_var = "DIFFINERY_TOKEN"` under `[mcp_servers.diffinery]`. It takes the place of the plugin's entry. | | |
| 47 | +| Other apps | An `Authorization: Bearer ...` header; see [Other apps](/docs/connect/other-apps#apps-without-oauth-sign-in). | | |
| 48 | +| git, gh and the CLI | The CLI reads `DIFFINERY_TOKEN`. Run `diffinery auth setup-git` once so git uses it too, and `diffinery auth setup-gh` for gh. | | |
| 49 | +| Claude Code on the web and other cloud sessions | Allow `diffinery.com` in the environment's network settings, add `DIFFINERY_TOKEN` as an environment secret, and clone over HTTPS. | | |
| 50 | + | |
| 51 | +A token and the browser sign-in can't share one server entry: with the header set, neither Claude Code nor Codex falls back to OAuth. `diffinery mcp-config` prints the snippet for your token. | |
| 52 | + | |
| 53 | +## Troubleshooting | |
| 54 | + | |
| 55 | +| Symptom | Cause and fix | | |
| 56 | +| --- | --- | | |
| 57 | +| The browser says "Can't connect this app" | The app's registration expired (registrations nobody used are removed after a week), or it asked to send you back to an address it didn't register. Remove the server or connector and add it again. | | |
| 58 | +| An app that worked asks you to sign in again | The connection lapsed after 30 days unused, or it was disconnected (by you, or after a reused refresh token). Sign in again. | | |
| 59 | +| Tools fail with `this token needs the write scope` | You connected with Read access. Disconnect it under Connected apps and connect again with Write. | | |
| 60 | +| A tool fails with `agents may not push to protected or default branches` | The app acts as an agent. It should work on a branch and open a pull request; see [What agents can and can't do](/docs/agents/overview). | |
addedapps/diffinery/docs/connect/other-apps.md+63−0View file
| @@ -1,0 +1,63 @@ | ||
| 1 | +--- | |
| 2 | +title: Other apps | |
| 3 | +description: Connect Cursor, VS Code or any other MCP client to Diffinery with the server URL and a browser sign-in, or with a token where sign-in isn't supported. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +Any app that supports remote MCP servers over HTTP can connect to Diffinery. Apps that support MCP's OAuth sign-in need only the URL; the rest send a token. | |
| 7 | + | |
| 8 | +```text | |
| 9 | +https://diffinery.com/mcp | |
| 10 | +``` | |
| 11 | + | |
| 12 | +## Cursor | |
| 13 | + | |
| 14 | +Add Diffinery to `~/.cursor/mcp.json`, or to `.cursor/mcp.json` in a project to share it with everyone who opens the project: | |
| 15 | + | |
| 16 | +```json | |
| 17 | +{ | |
| 18 | + "mcpServers": { | |
| 19 | + "diffinery": { "url": "https://diffinery.com/mcp" } | |
| 20 | + } | |
| 21 | +} | |
| 22 | +``` | |
| 23 | + | |
| 24 | +Cursor lists the server in its MCP settings with a way to sign in; Diffinery's consent page opens in your browser. | |
| 25 | + | |
| 26 | +## VS Code | |
| 27 | + | |
| 28 | +Add Diffinery to `.vscode/mcp.json` in a workspace (or run **MCP: Add Server** from the Command Palette and choose **HTTP**): | |
| 29 | + | |
| 30 | +```json | |
| 31 | +{ | |
| 32 | + "servers": { | |
| 33 | + "diffinery": { "type": "http", "url": "https://diffinery.com/mcp" } | |
| 34 | + } | |
| 35 | +} | |
| 36 | +``` | |
| 37 | + | |
| 38 | +Start the server from the file or the MCP view, and sign in when VS Code asks. | |
| 39 | + | |
| 40 | +## Any other app | |
| 41 | + | |
| 42 | +Look for "remote MCP server", "HTTP" or "Streamable HTTP" in the app's settings, and give it `https://diffinery.com/mcp`. If the app supports OAuth for MCP, it finds everything else on its own: Diffinery answers its first request with a `401` that points at the sign-in details, the app registers itself, and you approve it on the consent page. See [How sign-in works](/docs/connect/how-sign-in-works). | |
| 43 | + | |
| 44 | +## Apps without OAuth sign-in | |
| 45 | + | |
| 46 | +Send a Diffinery token in the `Authorization` header instead. Use an [agent](/docs/agents/overview)'s token (created under **Settings → Agents**) and keep it in an environment variable, never in a file you commit: | |
| 47 | + | |
| 48 | +```json | |
| 49 | +{ | |
| 50 | + "mcpServers": { | |
| 51 | + "diffinery": { | |
| 52 | + "url": "https://diffinery.com/mcp", | |
| 53 | + "headers": { "Authorization": "Bearer ${DIFFINERY_TOKEN}" } | |
| 54 | + } | |
| 55 | + } | |
| 56 | +} | |
| 57 | +``` | |
| 58 | + | |
| 59 | +`diffinery mcp-config` prints this snippet. Apps that can only start local (stdio) servers, such as those set up through Claude Desktop's configuration file, can reach Diffinery through a small bridge; [MCP server](/docs/agents/mcp#claude-desktop-and-other-clients-that-start-local-servers) has the setup for those and for many more clients. | |
| 60 | + | |
| 61 | +## Check that it works | |
| 62 | + | |
| 63 | +Ask the app "Who am I on Diffinery?". It calls the `whoami` tool and answers with the account it acts as. Apps that signed in also show under **Settings → Access tokens → Connected apps** on Diffinery. |
addedapps/diffinery/docs/images/connected-apps.webp+0−0View file
Binary file not shown.
addedapps/diffinery/docs/images/consent-claude-code.webp+0−0View file
Binary file not shown.
addedapps/diffinery/docs/images/consent-codex.webp+0−0View file
Binary file not shown.
modifiedapps/diffinery/docs/index.md+3−2View file
| @@ -13,7 +13,8 @@ | ||
| 13 | 13 | | Understand accounts, roles, agents and runs | [Concepts](/docs/concepts) | |
| 14 | 14 | | Move a project or an organization from GitHub (**Settings → Imports**, or `diffinery migrate github`) | [Migrate from GitHub](/docs/migrate-from-github) | |
| 15 | 15 | | Run CI | [Actions overview](/docs/actions/overview) and [Workflow syntax](/docs/actions/workflow-syntax) | |
| 16 | -| Connect an AI agent | [Agents](/docs/agents/overview) and [MCP](/docs/agents/mcp) | | |
| 16 | +| Connect Claude, Claude Code, Codex or another AI app (one URL, a browser sign-in) | [Connect AI apps](/docs/connect) | | |
| 17 | +| Run your own AI agents on Diffinery, with their own accounts and tokens | [Agents](/docs/agents/overview) and [MCP](/docs/agents/mcp) | | |
| 17 | 18 | | Give an agent a goal and get a pull request back, with your own model key | [Agent tasks](/docs/agents/tasks) | |
| 18 | 19 | | See the tokens and cost your agent tasks and reviews used | [Usage and cost](/docs/agents/tasks#usage-and-cost) | |
| 19 | 20 | | Pay for an organization's agents with its own keys, and set what they may use | [Agents in organizations](/docs/agents/organizations) | |
| @@ -48,7 +49,7 @@ | ||
| 48 | 49 | | git | Code | `https://diffinery.com/<owner>/<repo>.git` | |
| 49 | 50 | | CLI | Terminals and scripts | [`diffinery`](/docs/reference/cli) | |
| 50 | 51 | | REST API | Integrations, GitHub-compatible tools | `https://diffinery.com/api/v3`, see [the reference](/docs/api) | |
| 51 | -| MCP | AI agents | `https://diffinery.com/mcp`, see [the tools](/docs/reference/mcp-tools) | | |
| 52 | +| MCP | AI apps and agents | `https://diffinery.com/mcp`, see [Connect AI apps](/docs/connect) and [the tools](/docs/reference/mcp-tools) | | |
| 52 | 53 | | Webhooks | Reacting to events elsewhere | [Webhooks](/docs/repositories/webhooks) | |
| 53 | 54 | |
| 54 | 55 | ## For AI agents reading these docs |
modifiedapps/diffinery/scripts/build-docs.mjs+31−1View file
| @@ -190,6 +190,34 @@ | ||
| 190 | 190 | for (const s of navSlugs) if (!pages[s] && !RUNTIME_PAGES.has(s) && !BUILD_PAGES.has(s) && !process.env.DOCS_DRAFT) problems.push(`docs/nav.json lists "${s}" but docs/${s}.md doesn't exist`); |
| 191 | 191 | for (const s of Object.keys(pages)) if (!navSlugs.includes(s)) problems.push(`docs/${s}.md isn't listed in docs/nav.json`); |
| 192 | 192 | |
| 193 | +// Screenshots and other pictures in docs/images/, served at /docs/images/<file>. Every one must be used by | |
| 194 | +// a page, and every /docs/images/ reference must exist. Raster formats only (an SVG is a document that can | |
| 195 | +// run script on this origin). | |
| 196 | +const IMAGE_TYPES = { ".png": "image/png", ".webp": "image/webp", ".jpg": "image/jpeg" }; | |
| 197 | +const MAX_IMAGE = 400 * 1024; | |
| 198 | +const images = {}; | |
| 199 | +const imagesDir = path.join(docsDir, "images"); | |
| 200 | +for (const e of await readdir(imagesDir, { withFileTypes: true }).catch(() => [])) { | |
| 201 | + if (!e.isFile()) continue; | |
| 202 | + const type = IMAGE_TYPES[path.extname(e.name).toLowerCase()]; | |
| 203 | + if (!type || !/^[a-z0-9][a-z0-9.-]*$/.test(e.name)) { | |
| 204 | + problems.push(`docs/images/${e.name}: use lowercase names ending in .png, .webp or .jpg`); | |
| 205 | + continue; | |
| 206 | + } | |
| 207 | + const data = await readFile(path.join(imagesDir, e.name)); | |
| 208 | + if (data.length > MAX_IMAGE) problems.push(`docs/images/${e.name} is ${Math.round(data.length / 1024)} KiB (keep it under ${MAX_IMAGE / 1024} KiB)`); | |
| 209 | + images[e.name] = { type, data: data.toString("base64") }; | |
| 210 | +} | |
| 211 | +const usedImages = new Set(); | |
| 212 | +for (const [s, p] of Object.entries(pages)) { | |
| 213 | + for (const m of p.markdown.matchAll(/!\[([^\]]*)\]\(\/docs\/images\/([^)\s]+)/g)) { | |
| 214 | + usedImages.add(m[2]); | |
| 215 | + if (!images[m[2]]) problems.push(`docs/${s}.md shows /docs/images/${m[2]}, which doesn't exist`); | |
| 216 | + if (!m[1].trim()) problems.push(`docs/${s}.md shows /docs/images/${m[2]} without alt text (agents and screen readers read it)`); | |
| 217 | + } | |
| 218 | +} | |
| 219 | +for (const name of Object.keys(images)) if (!usedImages.has(name)) problems.push(`docs/images/${name} isn't used by any page`); | |
| 220 | + | |
| 193 | 221 | const tmp = await mkdtemp(path.join(tmpdir(), "diffinery-docs-")); |
| 194 | 222 | let cli; |
| 195 | 223 | try { |
| @@ -215,6 +243,8 @@ | ||
| 215 | 243 | export interface DocSection { title: string; pages: string[] } |
| 216 | 244 | export const DOCS_NAV: DocSection[] = ${JSON.stringify(nav)}; |
| 217 | 245 | export const DOCS_PAGES: Record<string, DocPage> = ${JSON.stringify(ordered)}; |
| 246 | +/** docs/images/: content type and base64 bytes by file name. */ | |
| 247 | +export const DOCS_IMAGES: Record<string, { type: string; data: string }> = ${JSON.stringify(images)}; | |
| 218 | 248 | export const CLI_VERSION = ${JSON.stringify(cli.VERSION)}; |
| 219 | 249 | export const CLI_BUNDLE = ${JSON.stringify(cli.bin)}; |
| 220 | 250 | export const CLI_INSTALL_SH = ${JSON.stringify(await readFile(path.join(cliInstall, "install.sh"), "utf8"))}; |
| @@ -222,4 +252,4 @@ | ||
| 222 | 252 | `; |
| 223 | 253 | await mkdir(path.dirname(out), { recursive: true }); |
| 224 | 254 | await writeFile(out, src); |
| 225 | -console.log(`docs: ${Object.keys(ordered).length} pages (+${RUNTIME_PAGES.size} generated at runtime), CLI ${cli.VERSION} (${Math.round(cli.bin.length / 1024)} KiB)`); | |
| 255 | +console.log(`docs: ${Object.keys(ordered).length} pages (+${RUNTIME_PAGES.size} generated at runtime), ${Object.keys(images).length} images, CLI ${cli.VERSION} (${Math.round(cli.bin.length / 1024)} KiB)`); |
addedapps/diffinery/scripts/docs-screenshots.mjs+143−0View file
| @@ -1,0 +1,143 @@ | ||
| 1 | +// Takes the screenshots in docs/images/ from a real local Worker (`wrangler dev`) with sample data, so the | |
| 2 | +// pictures in the docs show the real pages. PUBLIC_URL is https://diffinery.com, so the pages show the | |
| 3 | +// addresses people see. | |
| 4 | +// | |
| 5 | +// pnpm --filter @diffinery/app build && node scripts/docs-screenshots.mjs | |
| 6 | +// | |
| 7 | +// Writes docs/images/consent-claude-code.webp, consent-codex.webp and connected-apps.webp. | |
| 8 | +import { spawn, execFileSync } from "node:child_process"; | |
| 9 | +import { createHash, randomBytes } from "node:crypto"; | |
| 10 | +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; | |
| 11 | +import { tmpdir } from "node:os"; | |
| 12 | +import { join, dirname, resolve } from "node:path"; | |
| 13 | +import { fileURLToPath } from "node:url"; | |
| 14 | +import { createServer } from "node:net"; | |
| 15 | +import { chromium } from "playwright"; | |
| 16 | + | |
| 17 | +const APP = resolve(dirname(fileURLToPath(import.meta.url)), ".."); | |
| 18 | +const OUT = join(APP, "docs/images"); | |
| 19 | +const PUBLIC = "https://diffinery.com"; | |
| 20 | +const port = await new Promise((res) => { | |
| 21 | + const srv = createServer(); | |
| 22 | + srv.listen(0, "127.0.0.1", () => { | |
| 23 | + const { port } = srv.address(); | |
| 24 | + srv.close(() => res(port)); | |
| 25 | + }); | |
| 26 | +}); | |
| 27 | +// The browser talks to the Worker here; the pages themselves say diffinery.com. | |
| 28 | +const ORIGIN = `http://localhost:${port}`; | |
| 29 | +const work = mkdtempSync(join(tmpdir(), "diffinery-shots-")); | |
| 30 | +const persist = join(work, "state"); | |
| 31 | +const SESSION = "docs-screenshots-session-0123456789abcdef"; | |
| 32 | +const hash = (s) => createHash("sha256").update(s).digest("hex"); | |
| 33 | +const b64url = (b) => Buffer.from(b).toString("base64url"); | |
| 34 | +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| 35 | +const wrangler = (...args) => execFileSync("pnpm", ["exec", "wrangler", ...args], { cwd: APP, stdio: "pipe", env: { ...process.env, CI: "1" } }); | |
| 36 | +const sql = (command) => wrangler("d1", "execute", "diffinery-dev", "--local", "-e", "dev", "--persist-to", persist, "--command", command); | |
| 37 | + | |
| 38 | +// Sample data: Ada, signed in with a passkey, and her agent ada-bot. | |
| 39 | +wrangler("d1", "migrations", "apply", "diffinery-dev", "--local", "-e", "dev", "--persist-to", persist); | |
| 40 | +const at = new Date(Date.now() - 3 * 86400_000).toISOString(); | |
| 41 | +const later = new Date(Date.now() + 86400_000).toISOString(); | |
| 42 | +sql( | |
| 43 | + [ | |
| 44 | + `INSERT INTO accounts (id, login, kind, name, email, status, created_at) VALUES ('ada1', 'ada', 'user', 'Ada Lovelace', 'ada@example.com', 'active', '${at}')`, | |
| 45 | + `INSERT INTO accounts (id, login, kind, name, status, owner_id, created_at) VALUES ('bot1', 'ada-bot', 'agent', 'Ada bot', 'active', 'ada1', '${at}')`, | |
| 46 | + `INSERT INTO sessions (id, account_id, method, created_at, last_seen_at, expires_at) VALUES ('${hash(SESSION)}', 'ada1', 'passkey', '${new Date().toISOString()}', '${new Date().toISOString()}', '${later}')`, | |
| 47 | + ].join("; "), | |
| 48 | +); | |
| 49 | + | |
| 50 | +let log = ""; | |
| 51 | +const dev = spawn( | |
| 52 | + "pnpm", | |
| 53 | + ["exec", "wrangler", "dev", "-e", "dev", "--ip", "127.0.0.1", "--port", String(port), "--persist-to", persist, "--var", `PUBLIC_URL:${PUBLIC}`, "--var", "DEV_AUTH:", "--show-interactive-dev-session=false"], | |
| 54 | + { cwd: APP, env: { ...process.env, CI: "1", WRANGLER_SEND_METRICS: "false" }, stdio: ["ignore", "pipe", "pipe"], detached: true }, | |
| 55 | +); | |
| 56 | +dev.stdout.on("data", (d) => (log += d)); | |
| 57 | +dev.stderr.on("data", (d) => (log += d)); | |
| 58 | + | |
| 59 | +const browser = await chromium.launch(); | |
| 60 | +try { | |
| 61 | + const context = await browser.newContext({ viewport: { width: 1100, height: 900 }, deviceScaleFactor: 2, colorScheme: "light" }); | |
| 62 | + const page = await context.newPage(); | |
| 63 | + for (let i = 0; ; i++) { | |
| 64 | + try { | |
| 65 | + if ((await page.goto(`${ORIGIN}/healthz`)).ok()) break; | |
| 66 | + } catch {} | |
| 67 | + if (i > 240) throw new Error(`wrangler dev didn't start\n${log.slice(-3000)}`); | |
| 68 | + await sleep(500); | |
| 69 | + } | |
| 70 | + await page.goto(`${ORIGIN}/login`); | |
| 71 | + await page.evaluate((v) => (document.cookie = `__Host-dfy_session=${v}; path=/; secure; samesite=lax`), SESSION); | |
| 72 | + | |
| 73 | + // The app's callback: nothing listens there, so record where the browser was sent. | |
| 74 | + const callbacks = []; | |
| 75 | + context.on("request", (r) => { | |
| 76 | + const u = new URL(r.url()); | |
| 77 | + if (u.pathname === "/callback" && (u.hostname === "127.0.0.1" || u.hostname === "localhost")) callbacks.push(u); | |
| 78 | + }); | |
| 79 | + | |
| 80 | + /** Encodes the screenshot as WebP in the browser itself (no image library needed). */ | |
| 81 | + async function save(name, png) { | |
| 82 | + const data = await page.evaluate(async (b64) => { | |
| 83 | + const img = new Image(); | |
| 84 | + img.src = `data:image/png;base64,${b64}`; | |
| 85 | + await img.decode(); | |
| 86 | + const c = document.createElement("canvas"); | |
| 87 | + c.width = img.width; | |
| 88 | + c.height = img.height; | |
| 89 | + c.getContext("2d").drawImage(img, 0, 0); | |
| 90 | + return c.toDataURL("image/webp", 0.86).split(",")[1]; | |
| 91 | + }, png.toString("base64")); | |
| 92 | + writeFileSync(join(OUT, name), Buffer.from(data, "base64")); | |
| 93 | + console.log(`docs/images/${name}: ${Math.round((data.length * 3) / 4 / 1024)} KiB`); | |
| 94 | + } | |
| 95 | + | |
| 96 | + // Connects an app identified by its client metadata document, as Claude Code and Codex do. | |
| 97 | + async function connect(clientId, redirect, file) { | |
| 98 | + const verifier = b64url(randomBytes(32)); | |
| 99 | + const challenge = b64url(createHash("sha256").update(verifier).digest()); | |
| 100 | + const q = new URLSearchParams({ response_type: "code", client_id: clientId, redirect_uri: redirect, state: b64url(randomBytes(8)), code_challenge: challenge, code_challenge_method: "S256", scope: "read write", resource: `${PUBLIC}/mcp` }); | |
| 101 | + await page.goto(`${ORIGIN}/oauth/authorize?${q}`); | |
| 102 | + await page.waitForLoadState("networkidle"); | |
| 103 | + await page.getByText("ada-bot", { exact: false }).first().click(); | |
| 104 | + await sleep(300); | |
| 105 | + const card = page.locator("main .box").first(); | |
| 106 | + await save(file, await card.screenshot()); | |
| 107 | + const before = callbacks.length; | |
| 108 | + await page.click('button[value="approve"]', { noWaitAfter: true }); | |
| 109 | + for (let i = 0; callbacks.length === before; i++) { | |
| 110 | + if (i > 60) throw new Error("no callback"); | |
| 111 | + await sleep(250); | |
| 112 | + } | |
| 113 | + const code = callbacks[before].searchParams.get("code"); | |
| 114 | + const tok = await ( | |
| 115 | + await fetch(`${ORIGIN}/oauth/token`, { | |
| 116 | + method: "POST", | |
| 117 | + headers: { "content-type": "application/x-www-form-urlencoded" }, | |
| 118 | + body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirect, client_id: clientId, code_verifier: verifier }), | |
| 119 | + }) | |
| 120 | + ).json(); | |
| 121 | + if (!tok.access_token) throw new Error(`token exchange failed: ${JSON.stringify(tok)}`); | |
| 122 | + // One MCP call, so Connected apps shows it as used. | |
| 123 | + await fetch(`${ORIGIN}/mcp`, { | |
| 124 | + method: "POST", | |
| 125 | + headers: { "content-type": "application/json", accept: "application/json, text/event-stream", authorization: `Bearer ${tok.access_token}` }, | |
| 126 | + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "whoami", arguments: {} } }), | |
| 127 | + }); | |
| 128 | + await page.goto("about:blank"); | |
| 129 | + } | |
| 130 | + | |
| 131 | + await connect("https://claude.ai/oauth/claude-code-client-metadata", "http://localhost:53682/callback", "consent-claude-code.webp"); | |
| 132 | + await connect("https://chatgpt.com/oauth/codex/client.json", "http://127.0.0.1:1455/callback", "consent-codex.webp"); | |
| 133 | + | |
| 134 | + await page.goto(`${ORIGIN}/settings/tokens`); | |
| 135 | + await page.waitForLoadState("networkidle"); | |
| 136 | + await save("connected-apps.webp", await page.locator("#connected-apps").screenshot()); | |
| 137 | +} finally { | |
| 138 | + await browser.close(); | |
| 139 | + try { | |
| 140 | + process.kill(-dev.pid, "SIGTERM"); | |
| 141 | + } catch {} | |
| 142 | + rmSync(work, { recursive: true, force: true }); | |
| 143 | +} |
modifiedapps/diffinery/src/auth/oauth.ts+2−2View file
| @@ -116,7 +116,7 @@ | ||
| 116 | 116 | revocation_endpoint_auth_methods_supported: ["none", "client_secret_basic", "client_secret_post"], |
| 117 | 117 | authorization_response_iss_parameter_supported: true, |
| 118 | 118 | client_id_metadata_document_supported: true, |
| 119 | - service_documentation: `${b}/docs/agents/claude-and-codex`, | |
| 119 | + service_documentation: `${b}/docs/connect/how-sign-in-works`, | |
| 120 | 120 | }; |
| 121 | 121 | } |
| 122 | 122 | |
| @@ -317,7 +317,7 @@ | ||
| 317 | 317 | /** Reads a client metadata document: https on a public host, no redirects, small, and naming itself. */ |
| 318 | 318 | private async fetchMetadata(id: string): Promise<{ name: string; clientUri: string | null; redirectUris: string[] } | null> { |
| 319 | 319 | checkPublicUrl(id, localOnly(this.env), "client_id", new URL(this.env.PUBLIC_URL).hostname); |
| 320 | - const res = await fetch(id, { redirect: "manual", headers: { accept: "application/json", "user-agent": "Diffinery-OAuth/1 (+https://diffinery.com/docs/agents/claude-and-codex)" }, signal: AbortSignal.timeout(5000) }); | |
| 320 | + const res = await fetch(id, { redirect: "manual", headers: { accept: "application/json", "user-agent": "Diffinery-OAuth/1 (+https://diffinery.com/docs/connect/how-sign-in-works)" }, signal: AbortSignal.timeout(5000) }); | |
| 321 | 321 | if (!res.ok) { |
| 322 | 322 | console.error("client metadata document refused", id, res.status); |
| 323 | 323 | return null; |
modifiedapps/diffinery/src/docs/pages.ts+3−2View file
| @@ -107,8 +107,9 @@ | ||
| 107 | 107 | `- Every page is available as Markdown: add \`.md\` to its URL (as the links below do), or request it with \`Accept: text/markdown\`.`, |
| 108 | 108 | `- All pages in one file: ${origin}/llms-full.txt`, |
| 109 | 109 | `- The REST API as OpenAPI 3.1: ${origin}/docs/api/openapi.json`, |
| 110 | - `- MCP server for agents: ${origin}/mcp (send \`Authorization: Bearer <token>\`)`, | |
| 111 | - `- Command-line client: \`curl -fsSL ${origin}/install | bash\` (Windows: \`irm ${origin}/install.ps1 | iex\`), then \`diffinery login\`; the file itself is ${origin}/-/cli/diffinery.mjs (Node.js 22 or later)`, | |
| 110 | + `- MCP server for AI apps and agents: ${origin}/mcp, with OAuth 2.1 sign-in in the browser or \`Authorization: Bearer <token>\` headless. Guides for Claude, Claude Code, Codex and other apps: ${origin}${markdownUrl("connect")}`, | |
| 111 | + `- Plugin for Claude Code and Codex (MCP server, skills, session hook): \`claude plugin marketplace add ${origin}/codician/diffinery-plugins.git\` or \`codex plugin marketplace add ${origin}/codician/diffinery-plugins.git\`, then install \`diffinery@diffinery\``, | |
| 112 | + `- Command-line client: \`curl -fsSL ${origin}/install | bash\` (Windows: \`irm ${origin}/install.ps1 | iex\`), then \`diffinery login\`, \`diffinery auth setup-git\` for git and \`diffinery auth setup-gh\` for gh; the file itself is ${origin}/-/cli/diffinery.mjs (Node.js 22 or later)`, | |
| 112 | 113 | "", |
| 113 | 114 | ]; |
| 114 | 115 | for (const s of DOCS_NAV) { |
modifiedapps/diffinery/src/docs/reference.ts+1−1View file
| @@ -258,7 +258,7 @@ | ||
| 258 | 258 | ["GET, PUT /{owner}/{repo}.git/info/lfs/objects/{oid}", "Git LFS object download and upload."], |
| 259 | 259 | ["GET /{owner}/{repo}/actions/runs/{run_id}/artifacts/{name}", "Download a workflow artifact by name or numeric id, for browsers (the run page links here). Serves a zip, the file itself for an artifact uploaded as a single unzipped file, or a `.tar.gz` for an artifact from an older runner. Expired artifacts aren't served."], |
| 260 | 260 | ["POST /mcp", "The MCP server (Streamable HTTP, JSON-RPC). See [MCP](/docs/agents/mcp) and the [MCP tool reference](/docs/reference/mcp-tools)."], |
| 261 | - ["GET /.well-known/oauth-protected-resource/mcp", "OAuth protected resource metadata for the MCP server (RFC 9728); also at `/.well-known/oauth-protected-resource`. See [Claude and Codex](/docs/agents/claude-and-codex#how-sign-in-works)."], | |
| 261 | + ["GET /.well-known/oauth-protected-resource/mcp", "OAuth protected resource metadata for the MCP server (RFC 9728); also at `/.well-known/oauth-protected-resource`. See [How sign-in works](/docs/connect/how-sign-in-works)."], | |
| 262 | 262 | ["GET /.well-known/oauth-authorization-server", "OAuth authorization server metadata (RFC 8414) for MCP clients' browser sign-in."], |
| 263 | 263 | ["POST /oauth/register", "Dynamic client registration for MCP clients (RFC 7591): public clients, or confidential ones with `client_secret_basic` or `client_secret_post`."], |
| 264 | 264 | ["GET, POST /oauth/authorize", "The consent page where a person connects an MCP client, choosing whom it acts as and its access. Authorization code with PKCE (S256) only."], |
modifiedapps/diffinery/src/docs/web.tsx+19−1View file
| @@ -14,7 +14,7 @@ | ||
| 14 | 14 | import { fenceHighlighter } from "../web/highlight.js"; |
| 15 | 15 | import { ICONS } from "../web/assets.js"; |
| 16 | 16 | import { render, type WebApp } from "../web/ui.js"; |
| 17 | -import { CLI_BUNDLE, CLI_INSTALL_PS1, CLI_INSTALL_SH, CLI_VERSION } from "./content.generated.js"; | |
| 17 | +import { CLI_BUNDLE, CLI_INSTALL_PS1, CLI_INSTALL_SH, CLI_VERSION, DOCS_IMAGES } from "./content.generated.js"; | |
| 18 | 18 | import { allPages, getPage, headings, llmsFullTxt, llmsTxt, markdownUrl, nav, pageMarkdown, pageUrl, searchIndex, SLUGS, type Page } from "./pages.js"; |
| 19 | 19 | import { openApiDocument } from "./reference.js"; |
| 20 | 20 | |
| @@ -178,6 +178,11 @@ | ||
| 178 | 178 | ); |
| 179 | 179 | } |
| 180 | 180 | |
| 181 | +/** Pages that moved, old slug to new: links people and agents saved keep working. */ | |
| 182 | +const MOVED: Record<string, string> = { | |
| 183 | + "agents/claude-and-codex": "connect", | |
| 184 | +}; | |
| 185 | + | |
| 181 | 186 | export function registerDocs(web: WebApp): void { |
| 182 | 187 | web.get("/llms.txt", (c) => c.body(llmsTxt(origin(c)), 200, { ...PUBLIC, "content-type": "text/plain; charset=utf-8" })); |
| 183 | 188 | web.get("/llms-full.txt", (c) => c.body(llmsFullTxt(origin(c)), 200, { ...PUBLIC, "content-type": "text/plain; charset=utf-8" })); |
| @@ -201,6 +206,17 @@ | ||
| 201 | 206 | web.get("/install.sh", (c) => installScript(c, CLI_INSTALL_SH)); |
| 202 | 207 | web.get("/install.ps1", (c) => installScript(c, CLI_INSTALL_PS1)); |
| 203 | 208 | |
| 209 | + // Pictures for the docs pages (docs/images/), decoded once per isolate. | |
| 210 | + const images = new Map<string, Uint8Array<ArrayBuffer>>(); | |
| 211 | + web.get("/docs/images/:file", (c) => { | |
| 212 | + const name = c.req.param("file"); | |
| 213 | + const img = Object.hasOwn(DOCS_IMAGES, name) ? DOCS_IMAGES[name]! : null; | |
| 214 | + if (!img) return c.text("Not found", 404); | |
| 215 | + let bytes = images.get(name); | |
| 216 | + if (!bytes) images.set(name, (bytes = Uint8Array.from(atob(img.data), (ch) => ch.charCodeAt(0)))); | |
| 217 | + return c.body(bytes, 200, { ...PUBLIC, "cache-control": "public, max-age=86400", "content-type": img.type }); | |
| 218 | + }); | |
| 219 | + | |
| 204 | 220 | web.get("/docs", (c) => { |
| 205 | 221 | const p = getPage("index", origin(c))!; |
| 206 | 222 | return wantsMarkdown(c) ? markdownResponse(c, pageMarkdown(p, origin(c))) : docsPage(c, p); |
| @@ -211,6 +227,7 @@ | ||
| 211 | 227 | const md = slug.endsWith(".md"); |
| 212 | 228 | if (md) slug = slug.slice(0, -3); |
| 213 | 229 | if (slug === "index" && !md) return c.redirect("/docs", 301); |
| 230 | + if (Object.hasOwn(MOVED, slug)) return c.redirect(md ? markdownUrl(MOVED[slug]!) : pageUrl(MOVED[slug]!), 301); | |
| 214 | 231 | const p = getPage(slug, origin(c)); |
| 215 | 232 | if (!p) { |
| 216 | 233 | if (md) return c.body(`# Not found\n\nThere is no docs page "${slug}". See ${origin(c)}/llms.txt for every page.\n`, 404, { "content-type": "text/markdown; charset=utf-8" }); |
| @@ -252,6 +269,7 @@ | ||
| 252 | 269 | .docs-body blockquote{background:var(--bg2);border-left-color:var(--accent);border-radius:0 6px 6px 0;padding:10px 14px;color:var(--fg)} |
| 253 | 270 | .docs-body blockquote p:last-child{margin:0} |
| 254 | 271 | .docs-body table{font-size:13.5px}.docs-body td code{white-space:nowrap} |
| 272 | +.docs-body p>img{display:block;width:auto;max-width:100%;max-height:640px;margin:18px auto;border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow)} | |
| 255 | 273 | .docs-pn{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:40px;padding-top:20px;border-top:1px solid var(--border2)} |
| 256 | 274 | .docs-pn a{display:flex;flex-direction:column;gap:2px;padding:12px 14px;border:1px solid var(--border);border-radius:var(--r);color:var(--fg)} |
| 257 | 275 | .docs-pn a:hover{border-color:var(--accent);text-decoration:none}.docs-pn .next{text-align:right} |
modifiedapps/diffinery/src/web/account.tsx+2−2View file
| @@ -1053,7 +1053,7 @@ | ||
| 1053 | 1053 | <h1>Personal access tokens</h1> |
| 1054 | 1054 | <p class="muted">Tokens authenticate Git over HTTPS, the REST API, the CLI and MCP clients. Use your username (or anything) as the Git username and the token as the password.</p> |
| 1055 | 1055 | <p class="muted"> |
| 1056 | - For the CLI you don't need one: run <code>diffinery login</code> and approve the sign-in in your browser. Each CLI sign-in gets its own token, listed here. Nor for MCP apps such as claude.ai, Claude Code and Codex: they <a href="/docs/agents/claude-and-codex">connect through your browser</a> and appear under Connected apps. | |
| 1056 | + For the CLI you don't need one: run <code>diffinery login</code> and approve the sign-in in your browser. Each CLI sign-in gets its own token, listed here. Nor for MCP apps such as claude.ai, Claude Code and Codex: they <a href="#connected-apps">connect through your browser</a> and appear under Connected apps. | |
| 1057 | 1057 | </p> |
| 1058 | 1058 | {created ? <TokenOnce c={c} token={created.token} title="Your new token" /> : null} |
| 1059 | 1059 | <div class="box"> |
| @@ -1096,7 +1096,7 @@ | ||
| 1096 | 1096 | <div class="empty">No tokens yet.</div> |
| 1097 | 1097 | )} |
| 1098 | 1098 | </div> |
| 1099 | - <ConnectedApps t={t} grants={grants} /> | |
| 1099 | + <ConnectedApps t={t} grants={grants} mcpUrl={`${publicUrl(c)}/mcp`} /> | |
| 1100 | 1100 | <Form action="/settings/tokens" t={t} class="box"> |
| 1101 | 1101 | <div class="head"> |
| 1102 | 1102 | <h2>Create a token</h2> |
modifiedapps/diffinery/src/web/assets.ts+5−0View file
| @@ -241,6 +241,10 @@ | ||
| 241 | 241 | .markdown pre code{background:none;padding:0;font-size:12.5px} |
| 242 | 242 | .markdown blockquote{padding:0 1em;color:var(--fg2);border-left:3px solid var(--border)} |
| 243 | 243 | .markdown img{max-width:100%}.markdown hr{height:2px;background:var(--border2);border:0} |
| 244 | +.markdown .markdown-alert{margin:0 0 14px;padding:8px 14px;border-left:3px solid var(--a);border-radius:0 6px 6px 0;background:var(--as)} | |
| 245 | +.markdown .markdown-alert>:last-child{margin-bottom:0}.markdown .markdown-alert-title{font-weight:600;color:var(--a);margin:0 0 4px} | |
| 246 | +.markdown-alert-note{--a:var(--blue);--as:var(--blue-soft)}.markdown-alert-tip{--a:var(--green);--as:var(--green-soft)}.markdown-alert-important{--a:var(--purple);--as:var(--purple-soft)} | |
| 247 | +.markdown-alert-warning{--a:var(--amber);--as:var(--amber-soft)}.markdown-alert-caution{--a:var(--red);--as:var(--red-soft)} | |
| 244 | 248 | .markdown .table-wrap{overflow:auto}.markdown table{border-collapse:collapse} |
| 245 | 249 | .markdown th,.markdown td{border:1px solid var(--border);padding:6px 12px}.markdown th{background:var(--bg2);font-weight:600} |
| 246 | 250 | .readme{margin-top:16px}.readme>.body{padding:24px 32px} |
| @@ -283,6 +287,7 @@ | ||
| 283 | 287 | table.tbl th.r,table.tbl td.r{text-align:right;font-variant-numeric:tabular-nums} |
| 284 | 288 | .xscroll{overflow-x:auto} |
| 285 | 289 | .secret{font-family:var(--mono);background:var(--amber-soft);border:1px solid color-mix(in srgb,var(--amber) 45%,transparent);border-radius:var(--r);padding:7px 12px;line-height:20px;min-height:36px;word-break:break-all} |
| 290 | +.connect-url{font-family:var(--mono);background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);padding:7px 12px;line-height:20px;min-height:36px;word-break:break-all;font-size:13px}.connect-ai{border-bottom:1px solid var(--border2)} | |
| 286 | 291 | .pager{display:flex;gap:8px;justify-content:center;margin:16px 0} |
| 287 | 292 | .sha{font-family:var(--mono);font-size:12px;padding:1px 6px;border-radius:5px;background:var(--bg3);color:var(--fg)} |
| 288 | 293 | /* Buttons joined into one control (copy · sha · browse). */ |
modifiedapps/diffinery/src/web/markdown.ts+8−0View file
| @@ -419,6 +419,7 @@ | ||
| 419 | 419 | const HEADING = /^ {0,3}(#{1,6})(?:[ \t]([^]*))?$/; |
| 420 | 420 | const HR = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/; |
| 421 | 421 | const QUOTE = /^ {0,3}> ?([^]*)$/; |
| 422 | +const ALERT = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/i; | |
| 422 | 423 | const COMMENT = /^ {0,3}<!--/; |
| 423 | 424 | const LIST_ITEM = /^( {0,12})([*+-]|\d{1,9}[.)])([ \t]+([^]*))?$/; |
| 424 | 425 | const SEP_CELL = /^:?-+:?$/; |
| @@ -565,6 +566,13 @@ | ||
| 565 | 566 | else break; |
| 566 | 567 | i++; |
| 567 | 568 | } |
| 569 | + // GitHub's alerts: a quote whose first line is only [!NOTE], [!TIP], [!IMPORTANT], [!WARNING] or [!CAUTION]. | |
| 570 | + const alert = ALERT.exec(body[0] ?? ""); | |
| 571 | + if (alert && body.length > 1) { | |
| 572 | + const kind = alert[1]!.toLowerCase(); | |
| 573 | + parts.push(`<div class="markdown-alert markdown-alert-${kind}"><p class="markdown-alert-title">${kind[0]!.toUpperCase()}${kind.slice(1)}</p>${blocks(body.slice(1), opts, depth + 1)}</div>`); | |
| 574 | + continue; | |
| 575 | + } | |
| 568 | 576 | parts.push(`<blockquote>${blocks(body, opts, depth + 1)}</blockquote>`); |
| 569 | 577 | continue; |
| 570 | 578 | } |
modifiedapps/diffinery/src/web/oauth.tsx+30−4View file
| @@ -8,7 +8,7 @@ | ||
| 8 | 8 | import { ICONS } from "./assets.js"; |
| 9 | 9 | import { csrfToken, type FormData } from "./csrf.js"; |
| 10 | 10 | import { KRadio } from "./kumo.js"; |
| 11 | -import { Form, ago, post, render, type WebApp } from "./ui.js"; | |
| 11 | +import { CopyButton, Form, ago, post, render, type WebApp } from "./ui.js"; | |
| 12 | 12 | |
| 13 | 13 | /** |
| 14 | 14 | * Where a person approves an app that wants to use Diffinery through MCP (auth/oauth.ts has the |
| @@ -196,12 +196,38 @@ | ||
| 196 | 196 | }); |
| 197 | 197 | } |
| 198 | 198 | |
| 199 | -/** Settings → Access tokens: the apps this person connected, and a way to disconnect each. */ | |
| 200 | -export const ConnectedApps: FC<{ t: string; grants: Awaited<ReturnType<OAuth["grantsFor"]>> }> = ({ t, grants }) => ( | |
| 199 | +/** The guides for connecting AI apps over MCP, in the order Settings shows them. */ | |
| 200 | +export const CONNECT_GUIDES: Array<{ label: string; href: string }> = [ | |
| 201 | + { label: "Claude", href: "/docs/connect/claude" }, | |
| 202 | + { label: "Claude Code", href: "/docs/connect/claude-code" }, | |
| 203 | + { label: "Codex", href: "/docs/connect/codex" }, | |
| 204 | + { label: "Other apps", href: "/docs/connect/other-apps" }, | |
| 205 | +]; | |
| 206 | + | |
| 207 | +/** Settings → Access tokens: how to connect an AI app, the apps this person connected, and a way to disconnect each. */ | |
| 208 | +export const ConnectedApps: FC<{ t: string; grants: Awaited<ReturnType<OAuth["grantsFor"]>>; mcpUrl: string }> = ({ t, grants, mcpUrl }) => ( | |
| 201 | 209 | <div class="box" id="connected-apps"> |
| 202 | 210 | <div class="head"> |
| 203 | 211 | <h2>Connected apps</h2> |
| 204 | 212 | </div> |
| 213 | + <div class="body connect-ai"> | |
| 214 | + <p class="muted"> | |
| 215 | + Connect Claude, Claude Code, Codex or any other MCP app with this server URL. The app sends you here to choose whom it acts as and what it may do, then shows in this list. | |
| 216 | + </p> | |
| 217 | + <div class="row"> | |
| 218 | + <code class="grow connect-url">{mcpUrl}</code> | |
| 219 | + <CopyButton text={mcpUrl} size="base" /> | |
| 220 | + </div> | |
| 221 | + <p class="small mt"> | |
| 222 | + Step-by-step guides:{" "} | |
| 223 | + {CONNECT_GUIDES.map((g, i) => ( | |
| 224 | + <> | |
| 225 | + {i ? " · " : ""} | |
| 226 | + <a href={g.href}>{g.label}</a> | |
| 227 | + </> | |
| 228 | + ))} | |
| 229 | + </p> | |
| 230 | + </div> | |
| 205 | 231 | {grants.length ? ( |
| 206 | 232 | <table class="tbl"> |
| 207 | 233 | <thead> |
| @@ -236,7 +262,7 @@ | ||
| 236 | 262 | </table> |
| 237 | 263 | ) : ( |
| 238 | 264 | <div class="empty"> |
| 239 | - No apps connected. Apps such as claude.ai, Claude Code and Codex connect through <a href="/docs/agents/claude-and-codex">MCP sign-in</a>. | |
| 265 | + No apps connected yet. Add the URL above to an app and approve it here when it asks; see <a href="/docs/connect">Connect AI apps</a>. | |
| 240 | 266 | </div> |
| 241 | 267 | )} |
| 242 | 268 | </div> |
modifiedapps/diffinery/test/unit/docs.test.ts+26−0View file
| @@ -75,6 +75,7 @@ | ||
| 75 | 75 | const [path, frag] = href.split("#") as [string, string | undefined]; |
| 76 | 76 | if (path === "/docs" || path.startsWith("/docs/")) { |
| 77 | 77 | if (path === "/docs/api/openapi.json") continue; |
| 78 | + if (path.startsWith("/docs/images/")) continue; // checked when the docs are built (scripts/build-docs.mjs) | |
| 78 | 79 | const target = path === "/docs" ? "index" : path.slice(6).replace(/\.md$/, ""); |
| 79 | 80 | const t = getPage(target, ORIGIN); |
| 80 | 81 | if (!t) problems.push(`no docs page for ${href}`); |
| @@ -227,6 +228,31 @@ | ||
| 227 | 228 | expect(ps1).toContain("/-/cli/diffinery.mjs"); |
| 228 | 229 | }); |
| 229 | 230 | |
| 231 | + it("serves the screenshots in docs/images", async () => { | |
| 232 | + const res = await get("/docs/images/connected-apps.webp"); | |
| 233 | + expect(res.status).toBe(200); | |
| 234 | + expect(res.headers.get("content-type")).toBe("image/webp"); | |
| 235 | + expect(new Uint8Array(await res.arrayBuffer()).slice(8, 12)).toEqual(new TextEncoder().encode("WEBP")); | |
| 236 | + expect((await get("/docs/images/nope.webp")).status).toBe(404); | |
| 237 | + expect((await get("/docs/images/constructor")).status).toBe(404); | |
| 238 | + // Pages show them through their own origin, not the image proxy. | |
| 239 | + expect(await (await get("/docs/connect")).text()).toContain('<img src="/docs/images/connected-apps.webp"'); | |
| 240 | + }); | |
| 241 | + | |
| 242 | + it("sends moved pages to their new address, as HTML and as Markdown", async () => { | |
| 243 | + const html = await get("/docs/agents/claude-and-codex"); | |
| 244 | + expect(html.status).toBe(301); | |
| 245 | + expect(html.headers.get("location")).toBe("/docs/connect"); | |
| 246 | + expect((await get("/docs/agents/claude-and-codex.md")).headers.get("location")).toBe("/docs/connect.md"); | |
| 247 | + }); | |
| 248 | + | |
| 249 | + it("points agents at the connect guides, the plugin and gh from llms.txt", () => { | |
| 250 | + const txt = llmsTxt(ORIGIN); | |
| 251 | + expect(txt).toContain(`${ORIGIN}/docs/connect.md`); | |
| 252 | + expect(txt).toContain(`claude plugin marketplace add ${ORIGIN}/codician/diffinery-plugins.git`); | |
| 253 | + expect(txt).toContain("diffinery auth setup-gh"); | |
| 254 | + }); | |
| 255 | + | |
| 230 | 256 | it("returns 404 for unknown pages", async () => { |
| 231 | 257 | expect((await get("/docs/no-such-page")).status).toBe(404); |
| 232 | 258 | expect((await get("/docs/no-such-page.md")).status).toBe(404); |
modifiedapps/diffinery/test/unit/markdown-safety.test.ts+17−0View file
| @@ -146,3 +146,20 @@ | ||
| 146 | 146 | expect(stripHtmlComments("no comments\n\n\n\nhere")).toBe("no comments\n\n\n\nhere"); |
| 147 | 147 | }); |
| 148 | 148 | }); |
| 149 | + | |
| 150 | +describe("GitHub alerts", () => { | |
| 151 | + it("renders > [!NOTE] and the other four kinds as alerts, as GitHub does", () => { | |
| 152 | + expect(renderMarkdown("> [!TIP]\n> Use **passkeys**.")).toBe('<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title">Tip</p><p>Use <strong>passkeys</strong>.</p></div>'); | |
| 153 | + for (const k of ["NOTE", "important", "Warning", "CAUTION"]) expect(renderMarkdown(`> [!${k}]\n> x`)).toContain(`markdown-alert-${k.toLowerCase()}"><p class="markdown-alert-title">${k[0]!.toUpperCase()}${k.slice(1).toLowerCase()}</p>`); | |
| 154 | + }); | |
| 155 | + | |
| 156 | + it("keeps a plain quote when the marker is unknown, not alone on its line, or has nothing after it", () => { | |
| 157 | + expect(renderMarkdown("> [!DANGER]\n> x")).toBe("<blockquote><p>[!DANGER]\nx</p></blockquote>"); | |
| 158 | + expect(renderMarkdown("> [!NOTE] inline\n> x")).toContain("<blockquote>"); | |
| 159 | + expect(renderMarkdown("> [!NOTE]")).toContain("<blockquote>"); | |
| 160 | + }); | |
| 161 | + | |
| 162 | + it("escapes what is inside an alert like any other Markdown", () => { | |
| 163 | + expect(renderMarkdown('> [!WARNING]\n> <img src=x onerror="alert(1)">')).not.toContain("<img"); | |
| 164 | + }); | |
| 165 | +}); |