Skip to content

feat: @cireilclaw/plugin-github — GitHub App integration plugin#19

Merged
lyssieth merged 17 commits into
mainfrom
feat/github-plugin
Jun 5, 2026
Merged

feat: @cireilclaw/plugin-github — GitHub App integration plugin#19
lyssieth merged 17 commits into
mainfrom
feat/github-plugin

Conversation

@lyssieth

@lyssieth lyssieth commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Adds @cireilclaw/plugin-github, a full-featured GitHub App integration plugin with 15 tools across 5 domains.

Tools:

Domain Tools
Issues github-create-issue, github-read-issue, github-list-issues, github-update-issue, github-search-issues
Comments github-add-issue-comment, github-list-issue-comments
Pull Requests github-read-pr, github-list-prs, github-list-pr-files
Repos github-list-repos, github-read-repo
Content github-read-file, github-list-contents, github-search-code

Auth: GitHub App JWT → installation token flow via node:crypto, cached for 1-hour expiry.

Other changes:

  • Patch bumps on @cireilclaw/plugin-brave-search (0.2.2 → 0.2.3), @cireilclaw/plugin-openweather (0.1.2 → 0.1.3), @cireilclaw/plugin-template (0.1.1 → 0.1.2)
  • Root publish:all script publishes all @cireilclaw/* packages, excluding runtime

Security Considerations — additions & clarifications

  • Private key input trimming

    • resolvePrivateKey trims the configured privateKey (keyOrPath.trim()) before detecting PEM. This handles accidental surrounding whitespace or pasted file-path artifacts and prevents false negatives when a PEM is supplied with extra whitespace.
  • Extra headers passthrough

    • gh and ghParse accept an extraHeaders?: Record<string, string> and merge/forward them into requests (headers spread with ...extraHeaders). Callers can supply custom Accept or other headers which will be forwarded to GitHub.
  • Raw fallback size cap

    • github-read-file enforces MAX_RAW_FALLBACK_BYTES = 10 * 1024 * 1024 (10 MB). If GitHub's reported data.size exceeds this limit the plugin throws a ToolError and does not download the raw blob.
  • JWT/token handling

    • JWT is built by base64url-encoding header/payload and signing with Node's crypto using createPrivateKey(pem) and sign("sha256", ...), producing an RS256 JWT. The installation token is fetched and cached in module scope with expiresAt from GitHub's expires_at and reused until 2 minutes before expiry.
  • URL handling & pagination

    • gh constructs request URLs with new URL(path, "https://api.github.com") so absolute next links returned by GitHub are supported. parseLinkNext extracts rel="next" links and ghPaginate follows them while throwing on non-OK responses.

Potential risks / reviewer attention items

  • File-read error exposure

    • resolvePrivateKey will read a file path and, on failure, throw a ToolError containing the underlying fs error message. Reviewers should confirm whether exposing underlying I/O error text is acceptable in this environment.
  • Forwarded headers surface

    • Because extraHeaders are merged directly into request headers, callers can override Accept/Authorization/User-Agent if they provide those keys. Ensure callers are trusted or validate/whitelist allowed headers if that’s a concern.
  • Minimal runtime shape validation

    • ghParse and other callers cast JSON responses to typed shapes with unsafe assertions. While many fields are checked at runtime where branching depends on them (e.g., data.type, data.size), other fields rely on assumptions; consider adding targeted runtime checks for fields that gate behavior.

Confirmed mitigations already present

  • Trimming of privateKey input before PEM detection
  • resolvePrivateKey supports PEM string or filesystem path with explicit error on read failure
  • JWT created with createPrivateKey + explicit signing digest for RS256
  • Installation token caching with a 2-minute expiry buffer
  • Raw content fallback guarded by GitHub-reported data.size and capped at 10 MB
  • gh/ghParse support extraHeaders forwarding and build URLs to handle absolute GitHub pagination links
  • loadConfig memoization and concurrency deduplication

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Linter diff in the way? Review this PR in Change Stack to focus on meaningful changes and expand context only when needed.

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 64fcdcf5-e0ed-4526-8d05-11304a859d5a

📥 Commits

Reviewing files that changed from the base of the PR and between 30ef33c and 242ee3e.

📒 Files selected for processing (3)
  • packages/plugin-github/src/api.ts
  • packages/plugin-github/src/auth.ts
  • packages/plugin-github/src/content.ts

📝 Walkthrough

Walkthrough

A new GitHub plugin package is introduced with REST API helpers, GitHub App authentication, typed API models, and tools for issues, pull requests, comments, repositories, and content. Workspace scripts and several package versions were bumped, and an example GitHub config TOML was added.

Changes

GitHub Plugin Implementation

Layer / File(s) Summary
Plugin setup, build config, and API types
packages/plugin-github/package.json, packages/plugin-github/tsconfig.json, packages/plugin-github/tsdown.config.ts, packages/plugin-github/src/types.ts, packages/plugin-github/src/api.ts
Package manifest, TypeScript/tsdown build configuration, and GitHub REST API type definitions establish the foundation. gh() and ghParse<TData>() provide low-level API request and response-parsing helpers.
Authentication and configuration loading
packages/plugin-github/src/config.ts, packages/plugin-github/src/auth.ts
Config interface and loadConfig() validate and cache GitHub App credentials (appId, installationId, privateKey) with concurrent load deduplication. getInstallationToken() generates RS256 JWTs and fetches cached installation access tokens with expiry buffering.
Issue management tools
packages/plugin-github/src/issues.ts
Five tools (github-create-issue, github-read-issue, github-list-issues, github-update-issue, github-search-issues) implement strict input schemas, call GitHub's issues API endpoints, and normalize responses with assignee/label mapping and result filtering.
Pull request tools
packages/plugin-github/src/pulls.ts
Three tools (github-read-pr, github-list-prs, github-list-pr-files) validate parameters, fetch PR details with optional filtering and sorting, and map file changes into normalized result shapes.
Comment and content tools
packages/plugin-github/src/comments.ts, packages/plugin-github/src/content.ts
Two comment tools add and list issue/PR comments; three content tools read files (with base64 decoding and type enforcement), list directory contents, and search code. All validate inputs and normalize API responses.
Repository tools
packages/plugin-github/src/repos.ts
Two tools list installation repositories and read individual repo metadata, transforming GitHub API responses into consistent repos and repository detail result shapes.
Plugin entry point
packages/plugin-github/src/index.ts
Composes all tool registries (issues, PRs, comments, repos, content) and exports them under a single "github" plugin via definePlugin.

Workspace Configuration and Release Updates

Layer / File(s) Summary
GitHub plugin configuration example
config/plugins/github.toml.example
Adds a TOML configuration template documenting required GitHub App credentials and example private key format for local setup.
Workspace publish script and version updates
package.json, packages/brave-search/package.json, packages/openweather/package.json, packages/template/package.json
Root publish:all script enables recursive publishing of all @cireilclaw/* packages via pnpm. Package versions are bumped: brave-search 0.2.20.2.3, openweather 0.1.20.1.3, and template 0.1.10.1.2.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: addition of a new GitHub App integration plugin for the @cireilclaw ecosystem.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/plugins/github.toml.example`:
- Around line 7-8: The template incorrectly says privateKey may be a file path
though the code treats it as PEM content; update the implementation so auth
accepts either a PEM string or a filesystem path: in the config parsing (the
privateKey setting) and in the auth flow where createPrivateKey(...) is called,
detect whether privateKey looks like a PEM (e.g., starts with "-----BEGIN") and
otherwise attempt to read the file at that path (synchronously or async) and use
its contents as the PEM before calling createPrivateKey; ensure errors are
handled with clear messages so a path that cannot be read fails fast.

In `@packages/plugin-github/src/auth.ts`:
- Around line 21-33: The JWT is being signed with crypto.sign(undefined, ...)
which is incorrect for RS256; update the call in the function that builds the
JWT (the code using header/payload, b64url, createPrivateKey and sign) to call
crypto.sign with the explicit digest algorithm 'sha256' as the first argument,
ensure you sign the UTF-8 bytes of signingInput using the key from
createPrivateKey(pem), and continue returning the token as
`${signingInput}.${signature.toString("base64url")}` (ensuring the signature is
produced by the 'sha256' digest to match the JWT header's RS256).

In `@packages/plugin-github/src/config.ts`:
- Around line 6-33: The installationId parsing in inner (function inner and the
ConfigSchema/installationId handling) currently uses Number.parseInt which
accepts partial numbers and can yield NaN; update the code that builds the
returned installationId so that when parsed.installationId is a string you first
validate it matches a full unsigned integer (e.g. /^\d+$/) or otherwise parse to
a number and verify Number.isInteger(value) && value > 0, and if validation
fails throw a ToolError indicating an invalid installationId in plugin config;
keep the existing numeric branch unchanged.

In `@packages/plugin-github/src/content.ts`:
- Around line 33-40: In github-read-file (in
packages/plugin-github/src/content.ts) guard before decoding: verify
data.encoding === "base64" and that data.content is non-null/ non-empty; if
those checks fail, throw a ToolError with a clear message (referencing the path)
or perform a second request to the GitHub raw endpoint using Accept:
application/vnd.github.v3.raw to obtain the file bytes, and only call
Buffer.from(data.content, "base64") when the checks pass; update any error
messages accordingly and reference the existing ToolError type and the
data.encoding/data.content fields.

In `@packages/plugin-github/src/pulls.ts`:
- Around line 132-146: The githubListPrFiles tool (defined as githubListPrFiles
and using ghParse) currently fetches the PR files endpoint only once and thus
truncates results for PRs with >30 files; update execute in githubListPrFiles to
paginate: call the /repos/{owner}/{repo}/pulls/{number}/files endpoint
repeatedly with page and per_page (use per_page=100) or follow the Link header
until no next page, accumulate GHPrFile[] results into a single items array, and
return the combined list; ensure parsing still uses listPrFilesSchema for inputs
and reuse ghParse for each request while handling response headers to detect
termination.

In `@packages/plugin-github/src/repos.ts`:
- Around line 11-24: The current execute function calls
ghParse<GHInstallationRepos> once for "/installation/repositories" and only maps
result.repositories, which misses paginated pages; update execute to perform
paginated fetching (follow GitHub's Link header or use page/per_page params) by
repeatedly calling ghParse for subsequent pages until no next page, accumulate
all repositories into a single array, then map that accumulated array to the
existing repo shape (referencing execute, ghParse, GHInstallationRepos, and the
"/installation/repositories" endpoint) and return { repos, success: true } after
all pages are fetched.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 70adcf54-0166-42f3-862a-0418ed23dddb

📥 Commits

Reviewing files that changed from the base of the PR and between cb32109 and 5acc060.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • config/plugins/github.toml.example
  • package.json
  • packages/brave-search/package.json
  • packages/openweather/package.json
  • packages/plugin-github/package.json
  • packages/plugin-github/src/api.ts
  • packages/plugin-github/src/auth.ts
  • packages/plugin-github/src/comments.ts
  • packages/plugin-github/src/config.ts
  • packages/plugin-github/src/content.ts
  • packages/plugin-github/src/index.ts
  • packages/plugin-github/src/issues.ts
  • packages/plugin-github/src/pulls.ts
  • packages/plugin-github/src/repos.ts
  • packages/plugin-github/src/types.ts
  • packages/plugin-github/tsconfig.json
  • packages/plugin-github/tsdown.config.ts
  • packages/template/package.json

Comment thread config/plugins/github.toml.example
Comment thread packages/plugin-github/src/auth.ts
Comment thread packages/plugin-github/src/config.ts
Comment thread packages/plugin-github/src/content.ts
Comment thread packages/plugin-github/src/pulls.ts
Comment thread packages/plugin-github/src/repos.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/plugin-github/src/api.ts (1)

33-53: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

ghParse doesn't support extraHeaders, limiting flexibility.

The gh function accepts extraHeaders to allow callers to override the Accept header (as used in content.ts for raw file fallback), but ghParse doesn't expose this parameter. This forces callers needing custom headers to use gh directly and handle response parsing manually.

Consider adding the parameter for consistency:

♻️ Suggested change
 async function ghParse<TData>(
   ctx: PluginToolContext,
   method: string,
   path: string,
   body?: unknown,
+  extraHeaders?: Record<string, string>,
 ): Promise<TData> {
-  const response = await gh(ctx, method, path, body);
+  const response = await gh(ctx, method, path, body, extraHeaders);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugin-github/src/api.ts` around lines 33 - 53, ghParse currently
lacks the optional extraHeaders parameter supported by gh, so callers cannot
override headers (e.g., Accept) without using gh directly; update the ghParse
function signature to accept extraHeaders?: Record<string, string> (or the same
type used by gh) and pass it through to the gh(ctx, method, path, body,
extraHeaders) call, keeping existing error handling and return behavior intact
(including the 204 empty object case) so callers like content.ts can supply
custom headers without reimplementing parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/plugin-github/src/auth.ts`:
- Around line 20-33: The PEM-detection in resolvePrivateKey is brittle because
it fails when the input has leading/trailing whitespace; update
resolvePrivateKey to trim the input first (e.g., const trimmed =
keyOrPath.trim()), check trimmed.startsWith("-----BEGIN "), and if it is a PEM
return the trimmed string; if not, attempt to readFileSync using the trimmed
value and preserve the existing ToolError behavior (including the captured error
message) so paths with accidental whitespace are handled correctly; keep
references to resolvePrivateKey and ToolError for locating the change.

In `@packages/plugin-github/src/content.ts`:
- Around line 39-64: Before calling gh for the raw fallback, enforce a byte-size
cap by comparing data.size to a defined MAX_RAW_FALLBACK_BYTES constant and
throw a ToolError if data.size exceeds that cap so you never fetch very large
files; after a successful rawResponse, keep returning size: data.size (not
rawContent.length) to preserve GitHub's byte-size semantics and avoid using
character length; update the logic around gh, rawResponse, rawContent and
ToolError to bail early on oversized data and to set the returned size from
data.size.

---

Outside diff comments:
In `@packages/plugin-github/src/api.ts`:
- Around line 33-53: ghParse currently lacks the optional extraHeaders parameter
supported by gh, so callers cannot override headers (e.g., Accept) without using
gh directly; update the ghParse function signature to accept extraHeaders?:
Record<string, string> (or the same type used by gh) and pass it through to the
gh(ctx, method, path, body, extraHeaders) call, keeping existing error handling
and return behavior intact (including the 204 empty object case) so callers like
content.ts can supply custom headers without reimplementing parsing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f7d76fcd-51b7-49c5-aba3-91ad2350601d

📥 Commits

Reviewing files that changed from the base of the PR and between 5acc060 and 30ef33c.

📒 Files selected for processing (6)
  • packages/plugin-github/src/api.ts
  • packages/plugin-github/src/auth.ts
  • packages/plugin-github/src/config.ts
  • packages/plugin-github/src/content.ts
  • packages/plugin-github/src/pulls.ts
  • packages/plugin-github/src/repos.ts

Comment thread packages/plugin-github/src/auth.ts
Comment thread packages/plugin-github/src/content.ts
lyssieth added 17 commits June 6, 2026 02:54
Add package.json, tsconfig.json, and tsdown.config.ts for new
@cireilclaw/plugin-github package. Follows same build setup as
existing plugins (tsdown ESM, workspace SDK dep).
- auth.ts: GitHub App JWT generation (RS256) + installation token cache
- api.ts: gh/ghParse HTTP helpers wrapping ctx.net.fetch
- config.ts: valibot schema for appId/privateKey/installationId
- types.ts: typed interfaces for GitHub REST API responses
- github-create-issue: file a new issue with labels/assignees
- github-read-issue: detailed issue info
- github-list-issues: filterable issue list (state, labels, assignee)
- github-update-issue: modify title, body, state, labels
- github-search-issues: full-text search across issues
- github-add-issue-comment: post a comment
- github-list-issue-comments: read existing comments
- github-read-pr / github-list-prs / github-list-pr-files
- github-list-repos / github-read-repo
- github-read-file / github-list-contents / github-search-code
Entry point registers all 15 tools under the 'github' plugin name.
Example config documents appId/privateKey/installationId fields.
@cireilclaw/plugin-brave-search 0.2.2 -> 0.2.3
@cireilclaw/plugin-openweather 0.1.2 -> 0.1.3
@cireilclaw/plugin-template  0.1.1 -> 0.1.2
Publishes all @cireilclaw/* packages to npm via pnpm publish -r,
filtering out the un-scoped cireilclaw-runtime.
…56 digest

- resolvePrivateKey: detect PEM content vs file path via -----BEGIN prefix
- sign with 'sha256' digest to match JWT RS256 algorithm header
parseInstallationId rejects non-numeric strings and NaN,
moved before inner to fix no-use-before-define.
…allback

- Verify data.encoding === 'base64' and content non-empty before decode
- Fall back to raw endpoint with Accept: application/vnd.github.v3.raw
- Add extraHeaders param to gh() for custom Accept headers
Fetches all pages of /pulls/{number}/files for PRs with >30 files.
Use shared parseLinkNext from api.ts, follow Link headers to
fetch all pages of /installation/repositories.
resolvePrivateKey trims input to handle accidental whitespace
around the PEM string or file path.
- MAX_RAW_FALLBACK_BYTES prevents fetching very large files
- Return data.size (GitHub byte count) instead of rawContent.length
ghParse now accepts optional extraHeaders and forwards them to
gh, so callers can supply custom Accept headers.
@lyssieth lyssieth force-pushed the feat/github-plugin branch from 951b600 to fab6fd6 Compare June 5, 2026 23:54
@lyssieth lyssieth merged commit 39d3e6e into main Jun 5, 2026
4 checks passed
@lyssieth lyssieth deleted the feat/github-plugin branch June 5, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant