Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 70 additions & 19 deletions .github/workflows/deploy-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
# cursor -> getsentry/plugin-cursor
# codex -> getsentry/plugin-codex
# grok -> getsentry/plugin-grok
# pi -> getsentry/plugin-pi
#
# Marketplaces consume `getsentry/plugin-<agent>` by git ref. Each job builds its
# agent's tree from this repo, then commits it onto the target repo's `main`,
# replacing the previous contents. The four jobs target four different repos,
# so they run in parallel without contention.
# Marketplaces consume `getsentry/plugin-<agent>` by git ref. Each build job
# validates its agent's tree without deployment credentials and uploads it as an
# artifact. A separate deploy job then mints a repository-scoped token and
# commits only that validated artifact onto the target repo's `main`. The five
# matrix entries target five different repos, so they run in parallel without
# contention.
#
# Cross-repo writes use a GitHub App token scoped per-job to a single plugin
# repo; the default GITHUB_TOKEN cannot push to other repositories. The app must
# be installed on the org with contents:write on the three plugin repos, its ID
# be installed on the org with contents:write on the five plugin repos, its ID
# stored as the PLUGIN_DEPLOY_APP_ID variable and its private key as the
# PLUGIN_DEPLOY_KEY secret. Each target repo must already exist with `main` as
# its default branch.
Expand All @@ -43,20 +46,75 @@ permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
agent: [claude, cursor, codex, grok, pi]
steps:
- name: Checkout source
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0

- name: Set up Node for Pi validation
if: matrix.agent == 'pi'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24

- name: Install Pi CLI
if: matrix.agent == 'pi'
Comment thread
sentry-warden[bot] marked this conversation as resolved.
run: npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.83.0

- name: Build plugin-${{ matrix.agent }}
env:
AGENT: ${{ matrix.agent }}
run: |
set -euo pipefail

DIST_DIR="$(mktemp -d)/dist"
"src/plugins/${AGENT}/build.sh" "$DIST_DIR"
"src/plugins/${AGENT}/validate.sh" "$DIST_DIR"

mkdir -p "artifacts/${AGENT}"
rsync -a --delete "$DIST_DIR/" "artifacts/${AGENT}/"

- name: Upload validated plugin-${{ matrix.agent }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: plugin-${{ matrix.agent }}-${{ github.sha }}
path: artifacts/${{ matrix.agent }}/
if-no-files-found: error
include-hidden-files: true
retention-days: 1

deploy:
needs: build
# A failed build omits only that agent's artifact. Run the deploy matrix for
# the remaining artifacts instead of treating the build matrix as one gate.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
agent: [claude, cursor, codex, grok]
agent: [claude, cursor, codex, grok, pi]
Comment thread
sergical marked this conversation as resolved.
concurrency:
group: deploy-plugin-${{ matrix.agent }}
cancel-in-progress: false
steps:
- name: Checkout source
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Download validated plugin-${{ matrix.agent }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: plugin-${{ matrix.agent }}-${{ github.sha }}
path: artifact

# Mint the write credential only after every third-party package and
# artifact action has finished executing in this job.
- name: Mint deploy token
id: token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
Expand All @@ -66,10 +124,7 @@ jobs:
owner: getsentry
repositories: plugin-${{ matrix.agent }}

- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0

- name: Build and deploy plugin-${{ matrix.agent }}
- name: Deploy plugin-${{ matrix.agent }}
env:
AGENT: ${{ matrix.agent }}
SRC_SHA: ${{ github.sha }}
Expand All @@ -85,14 +140,10 @@ jobs:
git -C "$WORKTREE" config user.name "github-actions[bot]"
git -C "$WORKTREE" config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# Rewrite the whole tree: clear tracked content (preserve the .git
# dir), then repopulate from source via the agent's build script.
# Rewrite the whole tree from the already validated artifact. No build
# tool or third-party package executes while GH_TOKEN is in scope.
git -C "$WORKTREE" rm -rfq --ignore-unmatch .
"src/plugins/${AGENT}/build.sh" "$WORKTREE"

# Validate the built tree against the agent's schema/validator before
# it can be deployed.
"src/plugins/${AGENT}/validate.sh" "$WORKTREE"
rsync -a --delete --exclude='.git/' artifact/ "$WORKTREE/"

# Commit only if something changed.
git -C "$WORKTREE" add -A
Expand Down
28 changes: 18 additions & 10 deletions .github/workflows/smoke-installer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# (--no-interactive) so it installs the Sentry plugin for every detected agent
# without a prompt, and verifies each plugin actually landed.
#
# Claude Code, Codex, and Grok ship cross-platform npm CLIs, so they get a real
# install against their default marketplaces. Cursor has no headless CLI to
# Claude Code, Codex, Grok, and Pi ship cross-platform npm CLIs, so they get a
# real install against their package sources. Cursor has no headless CLI to
# install in CI, but our Cursor install is only a `git clone` of the public
# plugin repo into ~/.cursor/plugins/local/sentry — so we put a `cursor` stub on
# PATH to satisfy detection and verify the real clone (this is what exercises
Expand Down Expand Up @@ -57,8 +57,8 @@ jobs:
- name: Build
run: pnpm --filter @sentry/ai build

- name: Install Claude Code, Codex, and Grok CLIs
run: npm install -g @anthropic-ai/claude-code @openai/codex @xai-official/grok
- name: Install Claude Code, Codex, Grok, and Pi CLIs
run: npm install -g @anthropic-ai/claude-code @openai/codex @xai-official/grok @earendil-works/pi-coding-agent

- name: Put a Cursor stub on PATH
run: |
Expand All @@ -82,18 +82,26 @@ jobs:
set -euo pipefail

echo "== claude =="
claude plugin list
claude plugin list | grep -iq sentry || { echo "claude: sentry plugin missing"; exit 1; }
claude_plugins=$(claude plugin list)
printf '%s\n' "$claude_plugins"
printf '%s\n' "$claude_plugins" | grep -iq sentry || { echo "claude: sentry plugin missing"; exit 1; }

echo "== codex =="
codex plugin list
codex plugin list | grep -iq sentry || { echo "codex: sentry plugin missing"; exit 1; }
codex_plugins=$(codex plugin list)
printf '%s\n' "$codex_plugins"
printf '%s\n' "$codex_plugins" | grep -iq sentry || { echo "codex: sentry plugin missing"; exit 1; }

echo "== grok =="
grok plugin list
grok plugin list | grep -iq sentry || { echo "grok: sentry plugin missing"; exit 1; }
grok_plugins=$(grok plugin list)
printf '%s\n' "$grok_plugins"
printf '%s\n' "$grok_plugins" | grep -iq sentry || { echo "grok: sentry plugin missing"; exit 1; }

echo "== cursor =="
node -e "const p=require('path').join(require('os').homedir(),'.cursor','plugins','local','sentry'); if(!require('fs').existsSync(p)){console.error('cursor: plugin dir missing at '+p);process.exit(1)} console.log(p)"

echo "== pi =="
pi_plugins=$(pi list --no-approve)
printf '%s\n' "$pi_plugins"
printf '%s\n' "$pi_plugins" | grep -iq 'getsentry/plugin-pi' || { echo "pi: sentry package missing"; exit 1; }

echo "All agents have the Sentry plugin."
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Agent Instructions

## Project Overview
Sentry plugin for AI coding assistants (Claude Code, Cursor, Codex, and Grok). Provides MCP server integration and skills.
Sentry plugin for AI coding assistants (Claude Code, Cursor, Codex, Grok, and Pi). Provides MCP server integration and skills.

## Commit Attribution
AI commits MUST include:
Expand All @@ -23,7 +23,9 @@ src/SKILL_TREE.md # Generated skill index (see Skill Tree Navigation below

Per-agent plugin metadata is generated by the `src/plugins/<agent>/build.sh`
scripts and published to each agent's distribution repository; it is not
committed here.
committed here. Pi is packaged as a native Pi package (`package.json` with a
`pi` manifest) and includes a small `pi-mcp-adapter` extension because Pi has
no built-in MCP client.

Skills use YAML frontmatter with `allowed-tools` — this is required by Cursor and harmless in Claude Code. Keep it in all skill files.

Expand Down Expand Up @@ -76,7 +78,7 @@ Skills use YAML frontmatter with `allowed-tools` — this is required by Cursor
| `sentry-sdk-skill-creator` | Create a complete SDK skill bundle for any new platform |

## MCP Server
Sentry MCP server configured at `https://mcp.sentry.dev/mcp`. The source of truth is `mcp.json`: Cursor consumes it as-is at the plugin root, while the Codex and Grok builds emit it as `.mcp.json` (Codex's validator requires the dotted name; Grok auto-discovers it). Claude declares the server inline in its `plugin.json` (`mcpServers`), so the Claude build ships no MCP file. The root `.mcp.json` is a backwards-compat copy (identical to `mcp.json`) retained for existing installs that consumed the plugin from this repo's root; keep the two in sync until the root compat surface is removed.
Sentry MCP server configured at `https://mcp.sentry.dev/mcp`. The source of truth is `mcp.json`: Cursor consumes it as-is at the plugin root, while the Codex and Grok builds emit it as `.mcp.json` (Codex's validator requires the dotted name; Grok auto-discovers it). Claude declares the server inline in its `plugin.json` (`mcpServers`), so the Claude build ships no MCP file. Pi declares the same server in `src/plugins/pi/extensions/sentry-mcp.ts`, which adapts it into native Pi tools. The root `.mcp.json` is a backwards-compat copy (identical to `mcp.json`) retained for existing installs that consumed the plugin from this repo's root; keep the two in sync until the root compat surface is removed.

## Key Conventions
- All setup skills must **detect platform/SDK before suggesting configuration** — never assume
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
> The skills here are built from this source into installable plugins for
> [Claude Code](https://github.com/getsentry/plugin-claude),
> [Cursor](https://github.com/getsentry/plugin-cursor),
> [Codex](https://github.com/getsentry/plugin-codex), and
> [Grok](https://github.com/getsentry/plugin-grok) — install one of those, not
> this repo. They're also served over HTTP at
> [Codex](https://github.com/getsentry/plugin-codex),
> [Grok](https://github.com/getsentry/plugin-grok), and
> [Pi](https://github.com/getsentry/plugin-pi) — install one of those, not this
> repo. They're also served over HTTP at
> [skills.sentry.dev](https://skills.sentry.dev) for agents to fetch directly.
> In the future we may also publish the skills as a generic, standalone skills
> repository.
Expand All @@ -16,7 +17,7 @@ Your AI coding assistant already knows how to write code. This plugin teaches it

Whether you're adding Sentry to a new project, debugging a spike in errors, or configuring alerts, just ask. The plugin gives your assistant the context it needs to do it right.

Supports [**Claude Code**](https://github.com/getsentry/plugin-claude), [**Cursor**](https://github.com/getsentry/plugin-cursor), [**Codex**](https://github.com/getsentry/plugin-codex), and [**Grok**](https://github.com/getsentry/plugin-grok).
Supports [**Claude Code**](https://github.com/getsentry/plugin-claude), [**Cursor**](https://github.com/getsentry/plugin-cursor), [**Codex**](https://github.com/getsentry/plugin-codex), [**Grok**](https://github.com/getsentry/plugin-grok), and [**Pi**](https://github.com/getsentry/plugin-pi).

## What You Can Do

Expand Down Expand Up @@ -65,6 +66,7 @@ exactly that agent's plugin:
| Cursor | [`getsentry/plugin-cursor`](https://github.com/getsentry/plugin-cursor) |
| Codex | [`getsentry/plugin-codex`](https://github.com/getsentry/plugin-codex) |
| Grok | [`getsentry/plugin-grok`](https://github.com/getsentry/plugin-grok) |
| Pi | [`getsentry/plugin-pi`](https://github.com/getsentry/plugin-pi) |

These repositories are generated; do not edit them. Each one's README has the
install instructions for that agent.
Expand All @@ -83,11 +85,11 @@ and swaps the skill tree's `disable-model-invocation` flags for Codex's
```bash
git clone https://github.com/getsentry/sentry-for-ai.git
cd sentry-for-ai
src/plugins/codex/build.sh /tmp/sentry-codex # or src/plugins/{claude,cursor,grok}
src/plugins/codex/build.sh /tmp/sentry-codex # or src/plugins/{claude,cursor,grok,pi}
```

To build any target locally, run `src/plugins/<agent>/build.sh <output-dir>`
(`claude`, `cursor`, `codex`, or `grok`).
(`claude`, `cursor`, `codex`, `grok`, or `pi`).

## Skills

Expand Down
3 changes: 2 additions & 1 deletion packages/installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Install the [Sentry plugin](https://github.com/getsentry/sentry-for-ai) into you

The plugin teaches your assistant Sentry — how to set it up in any project, how to find and fix production issues, and how to configure alerts, AI monitoring, and more. This package detects which assistants you have installed and wires the plugin into each one for you.

Supports **Claude Code**, **Codex**, **Cursor**, and **Grok**.
Supports **Claude Code**, **Codex**, **Cursor**, **Grok**, and **Pi**.

```bash
npx @sentry/ai install
Expand Down Expand Up @@ -39,6 +39,7 @@ For each detected assistant, the installer runs that tool's native plugin comman
| Codex | `codex plugin add sentry` from the Sentry plugin marketplace |
| Cursor | Clones [`getsentry/plugin-cursor`](https://github.com/getsentry/plugin-cursor) into `~/.cursor/plugins/local/sentry` |
| Grok | `grok plugin install getsentry/plugin-grok` |
| Pi | `pi install git:github.com/getsentry/plugin-pi` |

Each per-agent plugin is built and published from the [`sentry-for-ai`](https://github.com/getsentry/sentry-for-ai) repository, which is the source of truth for all skills.

Expand Down
86 changes: 86 additions & 0 deletions packages/installer/src/__tests__/harnesses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createClaude } from "../harnesses/claude";
import { createCodex } from "../harnesses/codex";
import { createCursor } from "../harnesses/cursor";
import { createGrok } from "../harnesses/grok";
import { createPi } from "../harnesses/pi";
import { fakeSystem } from "./fake-system";

const ok: ShellResult = { ok: true };
Expand Down Expand Up @@ -361,6 +362,91 @@ describe("grok harness", () => {
});
});

describe("pi harness", () => {
it("detects when the pi binary is on PATH", async () => {
const harness = createPi(fakeSystem({ run: () => ok }));
expect(await harness.detect()).toBe(true);
});

it("does not detect when which fails", async () => {
const harness = createPi(fakeSystem({ run: () => notFound }));
expect(await harness.detect()).toBe(false);
});

it("reports installed when pi list includes our git package", async () => {
const harness = createPi(
fakeSystem({
run: () => ({
ok: true,
stdout: "User packages:\n git:github.com/getsentry/plugin-pi",
}),
}),
);
expect(await harness.isInstalled()).toBe(true);
});

it("reports not installed when pi list lacks our package", async () => {
const harness = createPi(
fakeSystem({ run: () => ({ ok: true, stdout: "User packages:\n npm:other-package" }) }),
);
expect(await harness.isInstalled()).toBe(false);
});

it("reports not installed when pi list fails", async () => {
const harness = createPi(fakeSystem({ run: () => notFound }));
expect(await harness.isInstalled()).toBe(false);
});

it("installs from the Pi distribution repository without trusting project resources", async () => {
const system = fakeSystem({ run: () => ok });
const outcome = await createPi(system).install();

expect(outcome).toMatchObject({
kind: "done",
command: "pi install git:github.com/getsentry/plugin-pi --no-approve",
});
expect(system.run).toHaveBeenCalledWith(
"pi install git:github.com/getsentry/plugin-pi --no-approve",
);
});

it("updates through Pi's package manager without trusting project resources", async () => {
const system = fakeSystem({ run: () => ok });
const outcome = await createPi(system).update();

expect(outcome).toMatchObject({
kind: "done",
command: "pi update git:github.com/getsentry/plugin-pi --no-approve",
});
expect(system.run).toHaveBeenCalledWith(
"pi update git:github.com/getsentry/plugin-pi --no-approve",
);
});

it("removes through Pi's package manager without trusting project resources", async () => {
const system = fakeSystem({ run: () => ok });
const outcome = await createPi(system).remove();

expect(outcome).toMatchObject({
kind: "done",
command: "pi remove git:github.com/getsentry/plugin-pi --no-approve",
});
expect(system.run).toHaveBeenCalledWith(
"pi remove git:github.com/getsentry/plugin-pi --no-approve",
);
});

it("forwards the output sink to package commands", async () => {
const system = fakeSystem({ run: () => ok });
const sink = {} as NodeJS.WritableStream;
await createPi(system).install(sink);
expect(system.run).toHaveBeenCalledWith(
"pi install git:github.com/getsentry/plugin-pi --no-approve",
sink,
);
});
});

describe("cursor harness", () => {
it("detects when the cursor binary is on PATH", async () => {
const harness = createCursor(fakeSystem({ run: () => ok }));
Expand Down
5 changes: 3 additions & 2 deletions packages/installer/src/harnesses/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { claude, createClaude } from "./claude";
import { codex, createCodex } from "./codex";
import { cursor, createCursor } from "./cursor";
import { grok, createGrok } from "./grok";
import { pi, createPi } from "./pi";

export type { Harness, InstallOutcome } from "./types";
export { createClaude, createCodex, createCursor, createGrok };
export { createClaude, createCodex, createCursor, createGrok, createPi };

export const harnesses = [claude, codex, cursor, grok];
export const harnesses = [claude, codex, cursor, grok, pi];
Loading
Loading