Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

diffinery setup adds Claude Code's Diffinery plugins, cloud environments included

diffinery setup (and the Agent setup script, through diffinery setup
--repo) now downloads the diffinery and diffinery-ci plugins from
/-/claude/, checks their SHA-256 header, saves them in
~/.claude/diffinery-plugins and names them in CLAUDE_CODE_PLUGIN_DIRS
in the env block of ~/.claude/settings.json, which Claude Code reads at
the start of every session: terminal, desktop app, editors, and cloud
sessions and project threads whose environment runs the setup script.
Nothing else to paste or upload. It keeps the rest of the settings
file, leaves a file it can't parse alone, leaves plugins installed from
the marketplace to it, and skips with --no-claude-plugins or
DIFFINERY_NO_CLAUDE_PLUGINS=1.

Serves diffinery-ci 0.2.0 (no option to set; in cloud threads Claude
also hears when CI passes). The CI status in Claude page gives one
step-by-step path per surface; the Claude Code, cloud agents, CLI
sign-in, quickstart and GitHub CLI pages and llms.txt say the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEy3wQ4DbP2VQUmrcNhTKn
Claude authored signed1 parent 294f5facommit db64d539821489de9c55c19b92898f70e8bc02dbBrowse files
18 files changed+304−67
Files (18)
modifiedapps/cli/src/commands/agent-setup.ts+14−1View file
@@ -7,6 +7,7 @@
77 import { gitCapture, parseRemote } from "../git.js";
88 import { CliError } from "../io.js";
99 import { VERSION } from "../version.js";
10+import { setupClaudePlugins } from "./claude-plugins.js";
1011
1112 /**
1213 * `diffinery setup --repo OWNER/NAME`: sets up an agent environment (a Claude Code cloud environment,
@@ -120,6 +121,7 @@
120121 clone: boolean;
121122 cloneDir?: string;
122123 report: boolean;
124+ claudePlugins: boolean;
123125 }
124126
125127 export async function agentSetup(ctx: Ctx, o: AgentSetupOptions, wireGit: (ctx: Ctx) => Promise<void>): Promise<number> {
@@ -251,7 +253,18 @@
251253 }
252254 }
253255
254- // 7. The report, which is the Check on the repository's Agent setup page.
256+ // 7. Claude Code's Diffinery plugins, where Claude Code runs (a Claude Code cloud environment).
257+ if (reachable && o.claudePlugins) {
258+ try {
259+ // Claude Code cloud sessions start in /home/user (as for the clone above).
260+ const p = await setupClaudePlugins(env, url, isDir("/home/user"));
261+ if (p) line("Claude Code plugins", p.ok, p.message);
262+ } catch (e) {
263+ line("Claude Code plugins", false, firstLine((e as Error).message));
264+ }
265+ }
266+
267+ // 8. The report, which is the Check on the repository's Agent setup page.
255268 const ok = lines.every((l) => l.ok);
256269 if (o.report && token && reachable && login) {
257270 try {
modifiedapps/cli/src/commands/auth.ts+21−2View file
@@ -7,6 +7,7 @@
77 import { browserLikely, browserLogin, deviceLogin, revokeCliToken, type Scope } from "../login.js";
88 import { json } from "../output.js";
99 import { agentSetup, ghEnvToken, setupRepo } from "./agent-setup.js";
10+import { setupClaudePlugins } from "./claude-plugins.js";
1011
1112 interface User {
1213 login: string;
@@ -228,7 +229,11 @@
228229 summary: "Set up this computer in one go: sign in, then git and gh",
229230 description: `Signs in through your browser (or with a code, on a machine without one) unless the CLI is
230231 signed in already, then runs \`diffinery auth setup-git\` so git clone, fetch and push use the
231-sign-in, and \`diffinery auth setup-gh\` when the GitHub CLI (gh) is installed. Safe to run again.
232+sign-in, and \`diffinery auth setup-gh\` when the GitHub CLI (gh) is installed. Where Claude Code is
233+installed, it also adds Claude Code's Diffinery plugins (diffinery and diffinery-ci): they're saved in
234+~/.claude/diffinery-plugins and named in ~/.claude/settings.json, so every new Claude Code session loads
235+them. Skip that with --no-claude-plugins (or DIFFINERY_NO_CLAUDE_PLUGINS=1). Safe to run again; it
236+brings the plugins up to date.
232237
233238 With --repo OWNER/NAME it sets up an agent environment instead (a Claude Code cloud environment,
234239 Codex, a CI runner), with no browser and no prompt: git and gh use the token in DIFFINERY_TOKEN
@@ -245,6 +250,7 @@
245250 "no-clone": { type: "boolean", desc: "With --repo: don't clone the repository (also DIFFINERY_NO_CLONE=1)" },
246251 "clone-dir": { type: "string", desc: "With --repo: where the clone goes (default: DIFFINERY_CLONE_DIR, /home/user in a Claude Code cloud environment, else the current directory)", arg: "DIR" },
247252 "no-report": { type: "boolean", desc: "With --repo: don't report the result to the repository's Agent setup page" },
253+ "no-claude-plugins": { type: "boolean", desc: "Don't add Claude Code's Diffinery plugins (also DIFFINERY_NO_CLAUDE_PLUGINS=1)" },
248254 },
249255 maxArgs: 0,
250256 async run(ctx, o) {
@@ -260,7 +266,7 @@
260266 await writeConfig(ctx.io.env, { url });
261267 next = new Ctx(ctx.io, ctx.opts);
262268 }
263- return agentSetup(next, { repo: setupRepo(str(o, "repo")!), url, clone: o["no-clone"] !== true && !ctx.io.env.DIFFINERY_NO_CLONE, cloneDir: str(o, "clone-dir"), report: o["no-report"] !== true }, (c) => setupGit.run(new Ctx(c.io, {}), {}, []) as Promise<void>);
269+ return agentSetup(next, { repo: setupRepo(str(o, "repo")!), url, clone: o["no-clone"] !== true && !ctx.io.env.DIFFINERY_NO_CLONE, cloneDir: str(o, "clone-dir"), report: o["no-report"] !== true, claudePlugins: o["no-claude-plugins"] !== true }, (c) => setupGit.run(new Ctx(c.io, {}), {}, []) as Promise<void>);
264270 }
265271 const cfg = await ctx.config();
266272 const url = str(o, "url") ? normalizeUrl(str(o, "url")!) : cfg.url;
@@ -285,6 +291,19 @@
285291 ctx.err(`${ctx.c.yellow("warning:")} ${e.message}`);
286292 failed = true;
287293 }
294+ if (o["no-claude-plugins"] !== true) {
295+ try {
296+ const p = await setupClaudePlugins(ctx.io.env, url);
297+ if (p?.ok) ctx.out(`${ctx.c.green("✓")} Claude Code plugins: ${p.message}`);
298+ else if (p) {
299+ ctx.err(`${ctx.c.yellow("warning:")} Claude Code plugins: ${p.message}`);
300+ failed = true;
301+ }
302+ } catch (e) {
303+ ctx.err(`${ctx.c.yellow("warning:")} Claude Code plugins: ${(e as Error).message}`);
304+ failed = true;
305+ }
306+ }
288307 ctx.out("");
289308 ctx.out(`Connect Claude Code, Codex, Cursor and other AI apps: ${url}/docs/connect`);
290309 return failed ? 1 : 0;
addedapps/cli/src/commands/claude-plugins.ts+115−0View file
@@ -1,0 +1,115 @@
1+import { createHash } from "node:crypto";
2+import { existsSync, statSync } from "node:fs";
3+import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4+import { homedir } from "node:os";
5+import path from "node:path";
6+import { VERSION } from "../version.js";
7+
8+/**
9+ * Claude Code's Diffinery plugins, set up by `diffinery setup` with nothing else to do: the `diffinery`
10+ * plugin (skill, /diffinery:pr-status, MCP server) and `diffinery-ci` (live CI band and pane, and a note
11+ * to Claude when CI on a commit it pushed settles). The server serves them at <server>/-/claude/<name>.zip;
12+ * they're saved in Claude Code's settings directory and named in the `env` block of its user settings as
13+ * CLAUDE_CODE_PLUGIN_DIRS, which Claude Code reads at the start of every session: the terminal, the
14+ * desktop app, VS Code, and cloud sessions and project threads, whose setup script runs this. A cloud
15+ * environment keeps them in its snapshot. Running it again brings them up to date.
16+ */
17+
18+export const CLAUDE_PLUGINS = ["diffinery", "diffinery-ci"] as const;
19+
20+/** Where Claude Code keeps its user settings: CLAUDE_CONFIG_DIR, else ~/.claude. */
21+export function claudeDir(env: NodeJS.ProcessEnv): string {
22+ return env.CLAUDE_CONFIG_DIR && path.isAbsolute(env.CLAUDE_CONFIG_DIR) ? env.CLAUDE_CONFIG_DIR : path.join(env.HOME || homedir(), ".claude");
23+}
24+
25+/** Whether Claude Code is on this machine: its settings directory exists, or `claude` is on PATH. */
26+export function hasClaude(env: NodeJS.ProcessEnv): boolean {
27+ if (isDir(claudeDir(env))) return true;
28+ return (env.PATH ?? "").split(path.delimiter).some((d) => d && existsSync(path.join(d, "claude")));
29+}
30+
31+function isDir(p: string): boolean {
32+ try {
33+ return statSync(p).isDirectory();
34+ } catch {
35+ return false;
36+ }
37+}
38+
39+/** CLAUDE_CODE_PLUGIN_DIRS with ours in place of any earlier copy of them, others kept in their order. */
40+export function withOurs(current: string | undefined, ours: string[]): string {
41+ const names = new Set(ours.map((p) => path.basename(p)));
42+ const kept = (current ?? "")
43+ .split(path.delimiter)
44+ .map((p) => p.trim())
45+ .filter((p) => p && !(path.basename(path.dirname(p)) === "diffinery-plugins" && names.has(path.basename(p))) && !ours.includes(p));
46+ return [...kept, ...ours].join(path.delimiter);
47+}
48+
49+export interface PluginsResult {
50+ ok: boolean;
51+ message: string;
52+}
53+
54+/**
55+ * Downloads the plugins and names them in Claude Code's user settings. Returns the check line's
56+ * outcome, or null when there's nothing to do here (no Claude Code, or DIFFINERY_NO_CLAUDE_PLUGINS).
57+ * `claudeCloud`: a Claude Code cloud environment, whose setup script may run before Claude Code has
58+ * made its settings directory.
59+ */
60+export async function setupClaudePlugins(env: NodeJS.ProcessEnv, url: string, claudeCloud = false): Promise<PluginsResult | null> {
61+ if (env.DIFFINERY_NO_CLAUDE_PLUGINS) return null;
62+ if (!claudeCloud && !hasClaude(env)) return null;
63+ const dir = claudeDir(env);
64+ const settingsFile = path.join(dir, "settings.json");
65+
66+ // Read the settings first: a file we can't read as JSON is left as it is.
67+ let settings: Record<string, unknown> = {};
68+ try {
69+ const text = await readFile(settingsFile, "utf8");
70+ const parsed: unknown = text.trim() ? JSON.parse(text) : {};
71+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not an object");
72+ settings = parsed as Record<string, unknown>;
73+ } catch (e) {
74+ if ((e as NodeJS.ErrnoException).code !== "ENOENT") return { ok: false, message: `${settingsFile} isn't valid JSON, so it was left alone: fix it, then run this again` };
75+ }
76+
77+ // A plugin already installed from the Diffinery marketplace (`claude plugin install`) stays as it is.
78+ const enabled = settings.enabledPlugins && typeof settings.enabledPlugins === "object" ? Object.keys(settings.enabledPlugins as object) : [];
79+ const wanted = CLAUDE_PLUGINS.filter((n) => !enabled.some((k) => k.startsWith(`${n}@`) && (settings.enabledPlugins as Record<string, unknown>)[k] !== false));
80+ const fromMarketplace = CLAUDE_PLUGINS.filter((n) => !wanted.includes(n));
81+
82+ const target = path.join(dir, "diffinery-plugins");
83+ await mkdir(target, { recursive: true });
84+ const got: string[] = [];
85+ const files: string[] = [];
86+ for (const name of wanted) {
87+ let res: Response;
88+ try {
89+ res = await fetch(`${url}/-/claude/${name}.zip`, { headers: { "user-agent": `diffinery-cli/${VERSION}` } });
90+ } catch (e) {
91+ return { ok: false, message: `couldn't download the ${name} plugin: ${(e as Error).message}` };
92+ }
93+ if (!res.ok) return { ok: false, message: `couldn't download the ${name} plugin: ${url} answered ${res.status}` };
94+ const body = Buffer.from(await res.arrayBuffer());
95+ const sha = res.headers.get("x-diffinery-plugin-sha256");
96+ if (!sha || createHash("sha256").update(body).digest("hex") !== sha) return { ok: false, message: `the ${name} plugin download didn't match its checksum: run this again` };
97+ const file = path.join(target, `${name}.zip`);
98+ await writeFile(`${file}.tmp`, body);
99+ await rename(`${file}.tmp`, file);
100+ files.push(file);
101+ got.push(`${name} ${res.headers.get("x-diffinery-plugin-version") ?? ""}`.trim());
102+ }
103+
104+ if (files.length) {
105+ const block = settings.env && typeof settings.env === "object" && !Array.isArray(settings.env) ? (settings.env as Record<string, unknown>) : {};
106+ const current = typeof block.CLAUDE_CODE_PLUGIN_DIRS === "string" ? block.CLAUDE_CODE_PLUGIN_DIRS : undefined;
107+ const next = { ...settings, env: { ...block, CLAUDE_CODE_PLUGIN_DIRS: withOurs(current, files) } };
108+ await writeFile(`${settingsFile}.tmp`, `${JSON.stringify(next, null, 2)}\n`);
109+ await rename(`${settingsFile}.tmp`, settingsFile);
110+ }
111+ const parts = [];
112+ if (got.length) parts.push(`${got.join(" and ")} load in new Claude Code sessions (named in ${settingsFile})`);
113+ if (fromMarketplace.length) parts.push(`${fromMarketplace.join(" and ")} already installed from the Diffinery marketplace`);
114+ return { ok: true, message: parts.join("; ") };
115+}
modifiedapps/cli/test/helpers.ts+2−0View file
@@ -129,6 +129,8 @@
129129 GIT_AUTHOR_EMAIL: "test@example.com",
130130 GIT_COMMITTER_NAME: "Test",
131131 GIT_COMMITTER_EMAIL: "test@example.com",
132+ // This machine's Claude Code stays out of it; the tests that want it turn it back on.
133+ DIFFINERY_NO_CLAUDE_PLUGINS: "1",
132134 ...extra,
133135 };
134136 }
modifiedapps/cli/test/setup.test.ts+74−1View file
@@ -1,5 +1,6 @@
1+import { createHash } from "node:crypto";
12 import { execFileSync } from "node:child_process";
2-import { chmodSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
3+import { chmodSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
34 import path from "node:path";
45 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
56 import { configPath, writeConfig } from "../src/config.js";
@@ -199,3 +200,75 @@
199200 expect(r.stderr).toContain("--repo expects OWNER/NAME");
200201 });
201202 });
203+
204+describe("Claude Code plugins in diffinery setup", () => {
205+ let srv: FakeServer;
206+ const zips = { diffinery: "PK\u0003\u0004 diffinery plugin", "diffinery-ci": "PK\u0003\u0004 diffinery-ci plugin" } as const;
207+ const sha = (s: string) => createHash("sha256").update(s).digest("hex");
208+ beforeEach(async () => {
209+ srv = await fakeServer();
210+ srv.on("GET", "/api/v3/user", { body: { login: "alice", type: "User" } });
211+ for (const [name, body] of Object.entries(zips))
212+ srv.on("GET", `/-/claude/${name}.zip`, { body, headers: { "content-type": "application/zip", "x-diffinery-plugin-version": name === "diffinery" ? "0.1.1" : "0.2.0", "x-diffinery-plugin-sha256": sha(body) } });
213+ });
214+ afterEach(() => srv.close());
215+
216+ /** A machine with Claude Code: its settings directory, with `settings` in it when given. */
217+ const withClaude = async (settings?: unknown) => {
218+ const env = isolatedEnv({ DIFFINERY_NO_CLAUDE_PLUGINS: "" });
219+ await writeConfig(env, { url: srv.url, token: "dfy_good" });
220+ mkdirSync(path.join(env.HOME!, ".claude"));
221+ if (settings !== undefined) writeFileSync(path.join(env.HOME!, ".claude", "settings.json"), typeof settings === "string" ? settings : JSON.stringify(settings));
222+ return env;
223+ };
224+ const settingsOf = (env: NodeJS.ProcessEnv) => JSON.parse(readFileSync(path.join(env.HOME!, ".claude", "settings.json"), "utf8"));
225+ const dirsOf = (env: NodeJS.ProcessEnv) => (settingsOf(env).env.CLAUDE_CODE_PLUGIN_DIRS as string).split(path.delimiter);
226+
227+ it("saves both plugins and names them in Claude Code's settings, keeping what was there, and runs again cleanly", async () => {
228+ const env = await withClaude({ model: "opus", env: { FOO: "1", CLAUDE_CODE_PLUGIN_DIRS: "/opt/mine" } });
229+ for (let i = 0; i < 2; i++) {
230+ const r = await run(["setup"], { env, openUrl: async () => expect.fail("opened a browser") });
231+ expect(r.code).toBe(0);
232+ expect(r.stdout).toContain("✓ Claude Code plugins: diffinery 0.1.1 and diffinery-ci 0.2.0 load in new Claude Code sessions");
233+ }
234+ const dir = path.join(env.HOME!, ".claude", "diffinery-plugins");
235+ expect(dirsOf(env)).toEqual(["/opt/mine", path.join(dir, "diffinery.zip"), path.join(dir, "diffinery-ci.zip")]);
236+ expect(settingsOf(env)).toMatchObject({ model: "opus", env: { FOO: "1" } });
237+ expect(readFileSync(path.join(dir, "diffinery-ci.zip"), "utf8")).toBe(zips["diffinery-ci"]);
238+ });
239+
240+ it("does nothing without Claude Code, with --no-claude-plugins, or for a plugin installed from the marketplace", async () => {
241+ // No Claude Code: no ~/.claude, and a PATH with git alone.
242+ const bin = tempDir("diffinery-no-claude-");
243+ symlinkSync(execFileSync("sh", ["-c", "command -v git"]).toString().trim(), path.join(bin, "git"));
244+ const none = isolatedEnv({ DIFFINERY_NO_CLAUDE_PLUGINS: "", PATH: bin });
245+ await writeConfig(none, { url: srv.url, token: "dfy_good" });
246+ expect((await run(["setup"], { env: none })).stdout).not.toContain("Claude Code plugins");
247+ expect(existsSync(path.join(none.HOME!, ".claude"))).toBe(false);
248+
249+ const skip = await withClaude();
250+ expect((await run(["setup", "--no-claude-plugins"], { env: skip })).stdout).not.toContain("Claude Code plugins");
251+ expect(existsSync(path.join(skip.HOME!, ".claude", "settings.json"))).toBe(false);
252+
253+ const market = await withClaude({ enabledPlugins: { "diffinery-ci@diffinery": true } });
254+ const r = await run(["setup"], { env: market });
255+ expect(r.stdout).toContain("diffinery 0.1.1 load in new Claude Code sessions");
256+ expect(r.stdout).toContain("diffinery-ci already installed from the Diffinery marketplace");
257+ expect(dirsOf(market)).toEqual([path.join(market.HOME!, ".claude", "diffinery-plugins", "diffinery.zip")]);
258+ });
259+
260+ it("leaves settings it can't read alone, and refuses a download that doesn't match its checksum", async () => {
261+ const broken = await withClaude("{ not json");
262+ const r = await run(["setup"], { env: broken });
263+ expect(r.code).toBe(1);
264+ expect(r.stderr).toContain("isn't valid JSON, so it was left alone");
265+ expect(readFileSync(path.join(broken.HOME!, ".claude", "settings.json"), "utf8")).toBe("{ not json");
266+
267+ srv.on("GET", "/-/claude/diffinery-ci.zip", { body: "tampered", headers: { "content-type": "application/zip", "x-diffinery-plugin-sha256": sha("other") } });
268+ const bad = await withClaude();
269+ const r2 = await run(["setup"], { env: bad });
270+ expect(r2.code).toBe(1);
271+ expect(r2.stderr).toContain("didn't match its checksum");
272+ expect(existsSync(path.join(bad.HOME!, ".claude", "settings.json"))).toBe(false);
273+ });
274+});
modifiedapps/diffinery/claude-plugins/README.md+6−4View file
@@ -1,10 +1,12 @@
1-# Claude Code plugins, as served for upload
1+# Claude Code plugins, as served for diffinery setup
22
33 Copies of the plugins from codician/diffinery-plugins, packed by that repository's
44 `scripts/pack.py` (reproducible: the same sources give the same bytes). Diffinery serves them at
5-`/-/claude/<name>.zip` (and `/-/claude/<name>-<version>.zip`) so people can upload them to
6-claude.ai under Customize → Plugins → Add → Upload plugin, which can't fetch the private plugin
7-repository. See docs/connect/ci-status-in-claude.md.
5+`/-/claude/<name>.zip` (and `/-/claude/<name>-<version>.zip`): `diffinery setup` downloads them
6+(apps/cli/src/commands/claude-plugins.ts, also from Agent setup's script), checks the
7+x-diffinery-plugin-sha256 header and names them in Claude Code's settings, since neither it nor
8+claude.ai's Upload plugin can fetch the private plugin repository. See
9+docs/connect/ci-status-in-claude.md.
810
911 To update one: change it in codician/diffinery-plugins, bump its version, run
1012 `python3 scripts/pack.py <name>` there, replace the zip here (one file per plugin,
deletedapps/diffinery/claude-plugins/diffinery-ci-0.1.0.zip+0−0
Binary file not shown.
addedapps/diffinery/claude-plugins/diffinery-ci-0.2.0.zip+0−0View file
Binary file not shown.
modifiedapps/diffinery/docs/accounts/cli-sign-in.md+1−1View file
@@ -12,7 +12,7 @@
1212 diffinery setup
1313 ```
1414
15-`diffinery setup` signs you in through your browser, lets git use the sign-in, and signs in the GitHub CLI (`gh`) when you have it. The rest of this page explains each step.
15+`diffinery setup` signs you in through your browser, lets git use the sign-in, signs in the GitHub CLI (`gh`) when you have it, and adds Diffinery's plugins to Claude Code when you have it ([CI status in Claude](/docs/connect/ci-status-in-claude)). The rest of this page explains each step.
1616
1717 ## Install
1818
modifiedapps/diffinery/docs/agents/github-cli.md+1−1View file
@@ -60,7 +60,7 @@
6060
6161 There's nothing to set up beyond signing gh in. Claude Code treats any host other than github.com as GitHub Enterprise: it reads the remote of the folder it's in, gets the token from `GH_ENTERPRISE_TOKEN` (when `GH_HOST` names the host) or from `gh auth token --hostname diffinery.com`, and asks Diffinery's REST and GraphQL APIs for the pull request, its review decision and its CI counts. In the desktop app, the folder has to be one you've trusted.
6262
63-Sessions in Claude Code on the web (claude.ai/code) and project threads run in Anthropic's cloud and get their pull request cards from Anthropic's GitHub integration, which doesn't reach Diffinery. Those sessions can still use gh and the [MCP server](/docs/agents/mcp) to open pull requests and follow CI, and the `diffinery-ci` plugin tells Claude when CI on its pushed commit settles. For a live view of every check in the terminal, the desktop app and VS Code, see [CI status in Claude](/docs/connect/ci-status-in-claude).
63+Sessions in Claude Code on the web (claude.ai/code) and project threads run in Anthropic's cloud and get their pull request cards from Anthropic's GitHub integration, which doesn't reach Diffinery. Those sessions can still use gh and the [MCP server](/docs/agents/mcp) to open pull requests and follow CI, and the `diffinery-ci` plugin, which the repository's [Agent setup](/docs/connect/cloud-agents) script adds, tells Claude when CI on its pushed commit passes or fails. For a live view of every check in the terminal, the desktop app and VS Code, see [CI status in Claude](/docs/connect/ci-status-in-claude).
6464
6565 ## Troubleshooting
6666
modifiedapps/diffinery/docs/connect/ci-status-in-claude.md+49−34View file
@@ -3,68 +3,81 @@
33 description: See a Diffinery pull request and its CI checks live in Claude Code, and have Claude told when CI on its pushed commit passes or fails, cloud sessions included.
44 ---
55
6-Claude Code shows the pull request of the branch you're on, with its CI. On Diffinery you get that in three layers:
6+Claude Code shows the pull request of the branch you're on, with its CI. On Diffinery, the `diffinery-ci` plugin adds a live view of the checks, and tells Claude when CI on a commit it pushed passes or fails, so it can fix what broke. `diffinery setup` installs it for you, with the `diffinery` plugin, in every place Claude Code runs:
77
88 | Where you run Claude Code | What you see | Set up |
99 | --- | --- | --- |
10-| Terminal, the desktop app's Code tab, VS Code | Claude Code's own pull request badge, and with the `diffinery-ci` plugin a live band of the PR's checks, a checks pane and a status line entry | [Sign gh in](#1-sign-gh-in-to-diffinery), [install the plugin](#2-install-the-diffinery-ci-plugin) |
11-| Cloud sessions on claude.ai/code and project threads | No badge or card (see [what Claude's cloud doesn't show](#what-claudes-cloud-doesnt-show)); with the plugin, Claude is told when CI on a commit it pushed settles, and `/diffinery-ci` answers in text | [Upload the plugin](#in-claudeai-cloud-sessions-and-projects) |
10+| Terminal, the desktop app's Code tab, VS Code, JetBrains | Claude Code's own pull request badge, a live band of the PR's checks above the prompt, a checks pane and a status line entry. Claude is told when CI on its pushed commit fails. | [Two commands, once per computer](#on-your-computer) |
11+| Cloud sessions on claude.ai/code, and project threads | Claude is told when CI on its pushed commit passes or fails, and says so in the thread; `/diffinery-ci` answers in text. No floating card (see [what Claude's cloud doesn't show](#what-claudes-cloud-doesnt-show)). | [The repository's Agent setup](#in-cloud-sessions-and-project-threads), which you need there anyway |
1212 | Anywhere | Claude can follow CI itself with `diffinery pr checks --watch`, `gh pr checks` or the MCP tools | [Claude Code](/docs/connect/claude-code) |
1313
14-## 1. Sign gh in to Diffinery
14+## On your computer
1515
16-Claude Code finds a branch's pull request through gh's sign-in and treats any host other than github.com as GitHub Enterprise, which Diffinery answers. With the [Diffinery CLI](/docs/accounts/cli-sign-in):
16+1. Install the CLI and run its setup, in a terminal:
1717
18-```sh
19-diffinery auth setup-gh
20-```
18+ ```sh
19+ curl -fsSL https://diffinery.com/install | bash
20+ diffinery setup
21+ ```
2122
22-The terminal footer then shows `PR #12` for the branch's open pull request, and the desktop app's session chip shows the pull request and its CI. Details in [GitHub CLI and Claude Code](/docs/agents/github-cli#claude-codes-pull-request-badge).
23+ On Windows, in PowerShell: `irm https://diffinery.com/install.ps1 | iex`, then `diffinery setup`.
2324
24-## 2. Install the diffinery-ci plugin
25+2. `diffinery setup` opens your browser: choose **Authorize**. It then sets up git and `gh`, and adds both Claude Code plugins. The last lines read:
2526
26-`diffinery-ci` is a Claude Code [mod](https://code.claude.com/docs/en/plugins/mods/overview): a plugin whose code runs inside Claude Code. It needs Claude Code 2.1.287 or later in the terminal (2.1.286 in the desktop app).
27+ ```text
28+ ✓ git will use diffinery for https://diffinery.com
29+ ✓ Claude Code plugins: diffinery 0.1.1 and diffinery-ci 0.2.0 load in new Claude Code sessions (named in ~/.claude/settings.json)
30+ ```
2731
28-```sh
29-claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git
30-claude plugin install diffinery-ci@diffinery
31-```
32+3. Start a new Claude Code session in a clone of a Diffinery repository (or run `/reload-plugins` in an open one). On a branch with a pull request, the band appears above the prompt.
3233
33-Inside Claude Code the same is `/plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git`, then `/plugin install diffinery-ci@diffinery`, then `/reload-plugins`. While the plugin repository is open only to Diffinery members, git fetches it with your sign-in: run `diffinery setup` (or `diffinery auth setup-git`) first. In the desktop app, use **+** next to the prompt → **Plugins** → **Add marketplace** with the same URL.
34+That's all. `gh` signed in to Diffinery also gives Claude Code's own footer badge (`PR #12`) and the desktop app's session chip; see [GitHub CLI and Claude Code](/docs/agents/github-cli#claude-codes-pull-request-badge). Run `diffinery setup` again at any time to bring the plugins up to date.
3435
35-The plugin reads your token from `DIFFINERY_TOKEN`, from `diffinery login`, or from gh signed in to Diffinery, and sends it only to Diffinery.
36+> [!NOTE]
37+> `diffinery setup` saves the plugins in `~/.claude/diffinery-plugins/` and names them in `CLAUDE_CODE_PLUGIN_DIRS`, in the `env` block of `~/.claude/settings.json`, which Claude Code reads at the start of every session in the terminal, the desktop app and the editors. It keeps everything else in that file. To skip it, run `diffinery setup --no-claude-plugins`. A plugin you already installed from the Diffinery marketplace (`claude plugin install diffinery-ci@diffinery`) is left as it is.
3638
37-### What it shows
39+## In cloud sessions and project threads
3840
41+A Claude Code cloud environment gets the plugins from the setup script of the repository's **Settings → Agent setup**, the same three boxes that give it git, gh and the CLI. Follow [Cloud agent environments](/docs/connect/cloud-agents#set-up-a-claude-code-cloud-environment). In short:
42+
43+1. On Diffinery, open the repository's **Settings → Agent setup** and choose **Generate agent token**.
44+2. At [claude.ai/code](https://claude.ai/code), edit the cloud environment (the one your sessions or your project's threads use) from the environment selector under the message box.
45+3. **Network access**: **Custom**, with `diffinery.com` under **Allowed domains**.
46+4. **Setup script**: box 3, `curl -fsSL https://diffinery.com/<owner>/<repo>/agent-setup.sh | bash`.
47+5. **Environment variables**: box 4, as it is.
48+6. Save and start a session. The setup log ends with `✓ Claude Code plugins: … load in new Claude Code sessions`, and the Agent setup page's **Check** turns green.
49+
50+From then on, when Claude pushes the branch or opens its pull request, it hears how CI went once the checks finish, and tells you in the thread.
51+
52+> [!NOTE]
53+> Claude Code keeps a snapshot of the environment after its setup script runs, for about seven days or until you change the script or the allowed domains. An environment set up before the plugins were added gets them when the snapshot is next made: to get them now, change the setup script (adding a blank line is enough) and save.
54+
55+You don't need to upload anything on claude.ai, and the cloud environment needs no other variable.
56+
57+## What the plugin shows
58+
59+`diffinery-ci` is a Claude Code [mod](https://code.claude.com/docs/en/plugins/mods/overview): a plugin whose code runs inside Claude Code. It needs Claude Code 2.1.287 or later in the terminal (2.1.286 in the desktop app). It reads your token from `DIFFINERY_TOKEN`, from `diffinery login`, or from gh signed in to Diffinery, and sends it only to Diffinery.
60+
61+
3962 - **A band above the prompt** for the current branch's pull request: its number and title, the checks (`✗ 1 failed · ● 3 running · ✓ 6`), and the review and merge state (approved, changes requested, merge conflict, auto-merge on). **Checks** opens the pane, **Open** opens the pull request, **Hide** folds the band away until you run `/diffinery-ci`.
4063 - **`/diffinery-ci`** opens a pane with every check, how long it took and a link to its log, plus **Refresh**, **Fix CI** (asks Claude to read the failing logs and fix what's clear) and a switch for the note below.
4164 - **The status line** reads `PR #12 ✓ 10/10`, `PR #12 ● 4/10` or `PR #12 ✗ 1 failed`, and a toast says when CI passes or fails.
4265
4366 It looks again every 15 seconds while checks run, every minute otherwise, and right after Claude pushes, switches branch or runs a `pr` command. In a folder with several clones it follows the one Claude last worked in.
4467
45-### Claude hears when CI settles
68+## Claude hears when CI settles
4669
47-When Claude pushes the branch or opens its pull request in the session, the plugin follows that exact commit. Once its checks finish, it sends Claude a note: by default only when something failed, naming each failed check with its log link and asking Claude to fix what's clear and say so when the failure isn't caused by the branch. This is what GitHub repositories get from Anthropic's GitHub App as auto-fix.
70+When Claude pushes the branch or opens its pull request in the session, the plugin follows that exact commit. Once its checks finish, it sends Claude a note. When something failed, the note names each failed check with its log link and asks Claude to fix what's clear and say so when the failure isn't caused by the branch. This is what GitHub repositories get from Anthropic's GitHub App as auto-fix.
4871
72+Where the band shows (terminal, desktop app, VS Code), Claude hears about failures only, since the band already shows a pass. Where nothing draws (cloud sessions, project threads, `claude -p`), Claude hears about passes too and says so in one line, so you see the result in the transcript either way.
73+
4974 | To | Do |
5075 | --- | --- |
5176 | Stop the notes for this session | `/diffinery-ci off` (`on` turns them back on), or the switch in the pane |
52-| Also get a note when CI passes, or never | Set the plugin's **wake** option to `all` or `off` (`/config`, or under the plugin in `/plugin`) |
77+| Choose which results Claude hears | `/diffinery-ci notes auto` (the default), `failures`, `all` or `off`. It's kept for later sessions; `/diffinery-ci notes` says the current choice. |
5378
5479 The note comes from the plugin, and Claude reads it as one: the transcript names `diffinery-ci` as its sender.
5580
56-### In claude.ai, cloud sessions and projects
57-
58-Claude Code cloud sessions and project threads load the plugins on your claude.ai account. claude.ai can't read the plugin repository, so upload the plugin as a file:
59-
60-1. Download [diffinery-ci.zip](https://diffinery.com/-/claude/diffinery-ci.zip). The [diffinery plugin](https://diffinery.com/-/claude/diffinery.zip) (skill, `/diffinery:pr-status`, session hook, MCP server) is there too.
61-2. On claude.ai, open **Customize → Plugins → Add → Upload plugin** and pick the zip.
62-3. For a project, also add it under **Project settings → Plugins**. New sessions and new threads load it when they start.
63-
64-The session needs a token: give the cloud environment `DIFFINERY_TOKEN` (an [agent's token](/docs/agents/overview), or the repository's [Agent setup](/docs/connect/cloud-agents) token) and allow `diffinery.com` in its network settings.
65-
66-Cloud sessions run a mod's code but don't draw what it draws, so there the plugin works through the note to Claude and the text answer of `/diffinery-ci`. Notes need the session to be running when CI finishes: they don't wake a session whose machine has stopped.
67-
6881 ## What Claude's cloud doesn't show
6982
7083 The floating pull request and CI cards in claude.ai/code cloud sessions and in projects come from Anthropic's GitHub integration, the Claude GitHub App, and GitHub's own webhooks. As of October 2026:
@@ -83,4 +96,6 @@
8396 | No band, although the branch has a pull request | The folder's remote isn't on diffinery.com (`git remote -v`), the band is hidden (run `/diffinery-ci`), or mods are off: `/plugin` lists the mods a session loaded. |
8497 | `/diffinery-ci` says there's no pull request | The current branch has none yet. Open one with `diffinery pr create` or `gh pr create`. |
8598 | Claude wasn't told when CI failed | Notes follow commits the session itself pushed, and stop after `/diffinery-ci off`. A push from outside the session shows in the band only. |
86-| Upload plugin refuses the file | Upload the `.zip` as downloaded, without unpacking or renaming it. |
99+| `diffinery setup` says `~/.claude/settings.json isn't valid JSON` | Claude Code's settings file has a typo. Fix it (or move it away), then run `diffinery setup` again. |
100+| A cloud session has no `/diffinery-ci` command | Look for the `Claude Code plugins` line at the end of the environment's setup log. No line: the snapshot is older than the plugins, so change the setup script and save (see the note in [the cloud steps](#in-cloud-sessions-and-project-threads)). A `✗` line: it says what failed, usually `diffinery.com` missing from the allowed domains. |
101+| You'd rather install from the marketplace | Members of the plugin repository can run `claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git`, then `claude plugin install diffinery-ci@diffinery`. `diffinery setup` then leaves that plugin to the marketplace. |
modifiedapps/diffinery/docs/connect/claude-code.md+8−13View file
@@ -14,20 +14,14 @@
1414 diffinery setup
1515 ```
1616
17-On Windows, in PowerShell: `irm https://diffinery.com/install.ps1 | iex`, then `diffinery setup`. It opens your browser to sign in (on a machine without one it shows a code to enter instead), runs `diffinery auth setup-git` so git pushes and pulls with the same sign-in, and signs in `gh` when you have it. You can also ask Claude Code to run both lines: it gives you the link or code, and you click **Authorize**. See [Install and sign in to the CLI](/docs/accounts/cli-sign-in).
17+On Windows, in PowerShell: `irm https://diffinery.com/install.ps1 | iex`, then `diffinery setup`. It opens your browser to sign in (on a machine without one it shows a code to enter instead), runs `diffinery auth setup-git` so git pushes and pulls with the same sign-in, signs in `gh` when you have it, and adds the Diffinery plugin and the [`diffinery-ci` plugin](/docs/connect/ci-status-in-claude) to Claude Code. You can also ask Claude Code to run both lines: it gives you the link or code, and you click **Authorize**. See [Install and sign in to the CLI](/docs/accounts/cli-sign-in).
1818
19-> [!NOTE]
20-> 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.
19+## 2. The plugin
2120
22-## 2. Install the plugin
21+`diffinery setup` installed it: its last lines include `✓ Claude Code plugins: diffinery … and diffinery-ci … load in new Claude Code sessions`. Start a new Claude Code session (or run `/reload-plugins` in an open one), and `/plugin` lists `diffinery` and `diffinery-ci`. Run `diffinery setup` again to bring them up to date.
2322
24-```sh
25-claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git
26-claude plugin install diffinery@diffinery
27-```
23+Members of the plugin repository can install from its marketplace instead, which `/plugin` then keeps up to date: `claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git`, then `claude plugin install diffinery@diffinery`.
2824
29-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.
30-
3125 ## 3. Sign in
3226
3327 1. In Claude Code, run `/mcp`, select **plugin:diffinery:diffinery** and choose **Authenticate**.
@@ -102,7 +96,7 @@
10296
10397 See [GitHub CLI](/docs/agents/github-cli) for what works with `gh`.
10498
105-For a live band of the pull request's checks above the prompt, and a note to Claude when CI on its pushed commit passes or fails, add the `diffinery-ci` plugin: `claude plugin install diffinery-ci@diffinery`. See [CI status in Claude](/docs/connect/ci-status-in-claude).
99+The `diffinery-ci` plugin, which `diffinery setup` added too, shows a live band of the pull request's checks above the prompt, and tells Claude when CI on its pushed commit fails. See [CI status in Claude](/docs/connect/ci-status-in-claude).
106100
107101 ## Headless: CI and cloud sessions
108102
@@ -113,13 +107,14 @@
113107 --header 'Authorization: Bearer ${DIFFINERY_TOKEN}'
114108 ```
115109
116-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).
110+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. For Claude Code on the web, set up the environment from the repository's [Agent setup](/docs/connect/cloud-agents), which also brings the CLI and both plugins. More in [Headless use](/docs/connect/how-sign-in-works#headless-use).
117111
118112 ## Troubleshooting
119113
120114 | Problem | What to do |
121115 | --- | --- |
122-| `claude plugin marketplace add` fails with an authentication error | Run `diffinery setup` first (step 1). |
116+| `/plugin` doesn't list `diffinery` | Run `diffinery setup` (step 1) and start a new session. Its `Claude Code plugins` line says what went wrong, if anything. |
117+| `claude plugin marketplace add` fails with an authentication error | The plugin repository is open only to its members. Use `diffinery setup` instead (step 1). |
123118 | `/mcp` shows the server as needing authentication | Choose **Authenticate**, or run `claude mcp login plugin:diffinery:diffinery`. |
124119 | The browser says "Can't connect this app" | Remove the server and add it again, then sign in. |
125120 | 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**. |
modifiedapps/diffinery/docs/connect/cloud-agents.md+2−1View file
@@ -41,7 +41,7 @@
4141
4242 7. Save, and start a session in the environment. Back on the Agent setup page, the **Check** turns green within seconds of the setup script finishing (the page updates by itself).
4343
44-In the session, the repository is cloned at `/home/user/<repo>`, and these all work with no prompt and no path prefix:
44+In the session, the repository is cloned at `/home/user/<repo>`, Claude Code has the Diffinery plugins (so Claude hears when CI on its pushed commit passes or fails), and these all work with no prompt and no path prefix:
4545
4646 ```sh
4747 diffinery --version
@@ -76,6 +76,7 @@
7676 - makes git use the token for diffinery.com (`diffinery auth setup-git`): git asks the CLI, which reads `DIFFINERY_TOKEN` from the environment each time. The token is never written to disk, so no snapshot or file holds it.
7777 - leaves `gh` to `GH_ENTERPRISE_TOKEN` and `GH_HOST` from the environment. Without them it signs `gh` in with `DIFFINERY_TOKEN` instead (`gh` then keeps it in its own config).
7878 - clones the repository unless it's there already, or fetches it when it is. It goes in `/home/user/<repo>` in a Claude Code cloud environment and in the current directory elsewhere; set `DIFFINERY_CLONE_DIR` to choose, or `DIFFINERY_NO_CLONE=1` to skip it.
79+ - adds Claude Code's Diffinery plugins, `diffinery` and `diffinery-ci`, so sessions in the environment load them: Claude then hears when CI on a commit it pushed passes or fails ([CI status in Claude](/docs/connect/ci-status-in-claude)). Set `DIFFINERY_NO_CLAUDE_PLUGINS=1` to skip it.
7980 - checks each part and prints one line per check, saying what to change when one fails.
8081 - reports the result to the repository's **Check**.
8182
modifiedapps/diffinery/docs/quickstart.md+1−1View file
@@ -48,7 +48,7 @@
4848 diffinery whoami # prints diffy
4949 ```
5050
51-`diffinery setup` runs `diffinery login`, then `diffinery auth setup-git`, so `git clone`, `fetch` and `push` to diffinery.com use the sign-in, and `diffinery auth setup-gh` when `gh` is installed. You can run those three yourself instead. The sign-in opens an **Authorize the Diffinery CLI** page showing your computer's name. Check it, keep **Write** access for this guide, and click **Authorize**. The terminal prints `✓ Logged in to https://diffinery.com as diffy`. No token to copy: the CLI gets its own, valid for 90 days and listed under **Settings → Access tokens**, and `diffinery logout` revokes it. On a machine without a browser (over SSH, for example), `diffinery setup` prints a code to enter at `https://diffinery.com/login/device` from any device instead.
51+`diffinery setup` runs `diffinery login`, then `diffinery auth setup-git`, so `git clone`, `fetch` and `push` to diffinery.com use the sign-in, and `diffinery auth setup-gh` when `gh` is installed. With Claude Code installed, it also adds Diffinery's plugins to it ([CI status in Claude](/docs/connect/ci-status-in-claude)). You can run those steps yourself instead. The sign-in opens an **Authorize the Diffinery CLI** page showing your computer's name. Check it, keep **Write** access for this guide, and click **Authorize**. The terminal prints `✓ Logged in to https://diffinery.com as diffy`. No token to copy: the CLI gets its own, valid for 90 days and listed under **Settings → Access tokens**, and `diffinery logout` revokes it. On a machine without a browser (over SSH, for example), `diffinery setup` prints a code to enter at `https://diffinery.com/login/device` from any device instead.
5252
5353 Working in VS Code, Cursor or Windsurf? Install the editor extension with `curl -fsSL https://diffinery.com/install-editor | bash`, then use **Git: Clone → Clone from Diffinery**. See [Clone in VS Code, Cursor and other editors](/docs/repositories/clone-in-your-editor). In IntelliJ IDEA, PyCharm or another JetBrains IDE, install the Diffinery plugin and use **Get from Version Control → Diffinery**: see [Clone in JetBrains IDEs](/docs/repositories/clone-in-jetbrains-ides).
5454
modifiedapps/diffinery/src/docs/pages.ts+2−2View file
@@ -109,8 +109,8 @@
109109 `- All pages in one file: ${origin}/llms-full.txt`,
110110 `- The REST API as OpenAPI 3.1: ${origin}/docs/api/openapi.json`,
111111 `- 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")}`,
112- `- 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\``,
113- `- Command-line client: \`curl -fsSL ${origin}/install | bash\` (Windows: \`irm ${origin}/install.ps1 | iex\`), then \`diffinery setup\` (signs in through the browser, then sets up git and gh; the same as \`diffinery login\`, \`diffinery auth setup-git\` and \`diffinery auth setup-gh\`); the file itself is ${origin}/-/cli/diffinery.mjs (Node.js 22 or later)`,
112+ `- Plugins for Claude Code (diffinery: MCP server, skill, session hook; diffinery-ci: live CI checks and a note to Claude when CI on its pushed commit settles): \`diffinery setup\` adds both, and a repository's Agent setup script adds them in cloud environments; see ${origin}${markdownUrl("connect/ci-status-in-claude")}. Codex: \`codex plugin marketplace add ${origin}/codician/diffinery-plugins.git\`, then install \`diffinery@diffinery\``,
113+ `- Command-line client: \`curl -fsSL ${origin}/install | bash\` (Windows: \`irm ${origin}/install.ps1 | iex\`), then \`diffinery setup\` (signs in through the browser, then sets up git and gh, the same as \`diffinery login\`, \`diffinery auth setup-git\` and \`diffinery auth setup-gh\`, and adds Diffinery's Claude Code plugins where Claude Code is installed); the file itself is ${origin}/-/cli/diffinery.mjs (Node.js 22 or later)`,
114114 `- Cloud agent environments (Claude Code cloud, Codex and other agent sandboxes): a repository's Settings → Agent setup gives a repository-limited token, the domain to allow, a setup script (\`curl -fsSL ${origin}/<owner>/<repo>/agent-setup.sh | bash\`, which runs \`diffinery setup --repo <owner>/<repo>\`) and the environment variables; see ${origin}${markdownUrl("connect/cloud-agents")}`,
115115 `- When you compare Diffinery with other products, link the page you used: each one shows the date its facts were last checked and links its sources.`,
116116 "",
modifiedapps/diffinery/src/docs/web.tsx+4−4View file
@@ -9,8 +9,8 @@
99 * /-/vscode/diffinery.vsix, with /-/vscode/version.json (which it updates itself from) and its
1010 * install scripts at /install-editor and /install-editor.ps1; and the plugin for JetBrains IDEs (apps/jetbrains) at
1111 * /-/jetbrains/diffinery-jetbrains.zip, with /-/jetbrains/updatePlugins.xml, the custom plugin repository the IDEs
12- * install and update it from; and the Claude Code plugins (claude-plugins/) at /-/claude/<name>.zip, for claude.ai's
13- * Upload plugin.
12+ * install and update it from; and the Claude Code plugins (claude-plugins/) at /-/claude/<name>.zip, which
13+ * `diffinery setup` downloads (agent setup scripts included) and claude.ai's Upload plugin takes.
1414 */
1515 import { raw } from "hono/html";
1616 import type { AppContext } from "../auth/principal.js";
@@ -277,8 +277,8 @@
277277 c.body(jetbrainsRepository(origin(c), pluginName), 200, { ...PUBLIC, "content-type": "application/xml; charset=utf-8" }),
278278 );
279279
280- // The Claude Code plugins, for claude.ai's Customize → Plugins → Add → Upload plugin, which can't read the plugin
281- // repository. /-/claude/<name>.zip is always the current version; the versioned name stays the same bytes.
280+ // The Claude Code plugins, for `diffinery setup` (which checks the SHA-256 header) and claude.ai's Upload
281+ // plugin, neither of which can read the plugin repository. /-/claude/<name>.zip is always the current version; the versioned name stays the same bytes.
282282 const claudeZips = new Map<string, Uint8Array<ArrayBuffer>>();
283283 web.get("/-/claude/:file", (c) => {
284284 const file = c.req.param("file");
modifiedapps/diffinery/src/web/agent-setup.tsx+1−1View file
@@ -223,7 +223,7 @@
223223 <div class="body">
224224 <Paste value={script} />
225225 <p class="small muted mt" style="margin-bottom:0">
226- Installs the <code>diffinery</code> CLI on PATH, makes git and gh use the token from the environment (it never writes the token to disk or prints it), clones {slug} unless it's there (into <code>/home/user/{repo.name}</code> in Claude Code; set <code>DIFFINERY_NO_CLONE=1</code> to skip), then checks that
226+ Installs the <code>diffinery</code> CLI on PATH, makes git and gh use the token from the environment (it never writes the token to disk or prints it), clones {slug} unless it's there (into <code>/home/user/{repo.name}</code> in Claude Code; set <code>DIFFINERY_NO_CLONE=1</code> to skip), adds Claude Code's Diffinery plugins so Claude hears when CI on its pushed commit passes or fails (<a href="/docs/connect/ci-status-in-claude">CI status in Claude</a>), then checks that
227227 everything works and reports to the Check below. Safe to run every session, and quick once the CLI is installed. <a href={`/${slug}/agent-setup.sh`}>Read the script</a>.
228228 </p>
229229 </div>
modifiedapps/diffinery/test/unit/docs.test.ts+3−1View file
@@ -290,7 +290,9 @@
290290 it("points agents at the connect guides, the plugin and gh from llms.txt", () => {
291291 const txt = llmsTxt(ORIGIN);
292292 expect(txt).toContain(`${ORIGIN}/docs/connect.md`);
293- expect(txt).toContain(`claude plugin marketplace add ${ORIGIN}/codician/diffinery-plugins.git`);
293+ expect(txt).toContain(`codex plugin marketplace add ${ORIGIN}/codician/diffinery-plugins.git`);
294+ expect(txt).toContain(`\`diffinery setup\` adds both`);
295+ expect(txt).toContain(`${ORIGIN}/docs/connect/ci-status-in-claude.md`);
294296 expect(txt).toContain("diffinery auth setup-gh");
295297 });
296298