diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..a7169f3 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,37 @@ +{ + "name": "codeant-cli", + "owner": { + "name": "CodeAnt AI", + "email": "support@codeant.ai" + }, + "metadata": { + "description": "CodeAnt AI CLI and MCP server — org-wide secret triage, cross-repo SAST/SCA findings, on-demand scans, and local PR review inside Claude.", + "version": "0.4.14" + }, + "plugins": [ + { + "name": "codeant", + "source": "./.", + "description": "Drive CodeAnt AI from inside Claude Code via the CodeAnt MCP server. Read-only by default; write tools (trigger scan, resolve PR thread) gated behind CODEANT_READ_ONLY=0.", + "version": "0.4.14", + "author": { + "name": "CodeAnt AI", + "email": "support@codeant.ai" + }, + "homepage": "https://codeant.ai", + "repository": "https://github.com/CodeAnt-AI/codeant-cli", + "license": "MIT", + "keywords": [ + "code-review", + "pull-requests", + "security", + "secrets", + "sast", + "sca", + "static-analysis", + "mcp" + ], + "category": "code-quality" + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..d473ad6 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "name": "codeant", + "version": "0.4.14", + "description": "CodeAnt AI MCP server inside Claude Code — org-wide secret triage, cross-repo SAST/SCA findings, on-demand scans, and local PR review.", + "author": { + "name": "CodeAnt AI", + "email": "support@codeant.ai" + }, + "homepage": "https://codeant.ai", + "documentation": "https://docs.codeant.ai/cli/claude-code-plugin", + "repository": "https://github.com/CodeAnt-AI/codeant-cli", + "license": "MIT", + "keywords": [ + "code-review", + "pull-requests", + "security", + "secrets", + "sast", + "sca", + "static-analysis", + "mcp" + ], + "prerequisites": { + "commands": ["codeant"] + } +} diff --git a/.github/workflows/publish-mcpb.yml b/.github/workflows/publish-mcpb.yml new file mode 100644 index 0000000..a7986a3 --- /dev/null +++ b/.github/workflows/publish-mcpb.yml @@ -0,0 +1,46 @@ +name: Publish MCPB Bundle + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + publish-mcpb: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - id: version + run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT" + + - run: npm ci + + - run: npm run mcpb:build + + - uses: actions/upload-artifact@v4 + with: + name: codeant-mcpb-v${{ steps.version.outputs.version }} + path: dist/codeant.mcpb + if-no-files-found: error + + - uses: softprops/action-gh-release@v2 + with: + tag_name: mcpb-v${{ steps.version.outputs.version }} + name: CodeAnt MCPB v${{ steps.version.outputs.version }} + files: dist/codeant.mcpb + fail_on_unmatched_files: true + body: | + CodeAnt MCPB bundle v${{ steps.version.outputs.version }} + + Commit: ${{ github.sha }} + Message: ${{ github.event.head_commit.message }} + + Install instructions: https://github.com/CodeAnt-AI/codeant-cli/blob/main/mcp.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fd2ef82..1590cb5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,25 +1,55 @@ -name: Publish Package +name: Build CLI Release on: push: branches: - main + workflow_dispatch: jobs: - publish: + build-cli: runs-on: ubuntu-latest permissions: - contents: read + contents: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - registry-url: 'https://registry.npmjs.org' + + - id: version + run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT" - run: npm ci - - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: npm pack + # produces codeant-cli-.tgz in the working directory + + - uses: actions/upload-artifact@v4 + with: + name: codeant-cli-v${{ steps.version.outputs.version }} + path: codeant-cli-${{ steps.version.outputs.version }}.tgz + if-no-files-found: error + + - uses: softprops/action-gh-release@v2 + with: + tag_name: cli-v${{ steps.version.outputs.version }} + name: CodeAnt CLI v${{ steps.version.outputs.version }} + files: codeant-cli-${{ steps.version.outputs.version }}.tgz + fail_on_unmatched_files: true + body: | + CodeAnt CLI npm tarball v${{ steps.version.outputs.version }} + + Commit: ${{ github.sha }} + Message: ${{ github.event.head_commit.message }} + + ## Publish to npm + + Download the `.tgz` and run: + + ``` + npm publish codeant-cli-${{ steps.version.outputs.version }}.tgz --access public + ``` + + (Requires npm auth with publish rights on the `codeant-cli` package.) diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..556e968 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "codeant": { + "command": "codeant", + "args": ["mcp"], + "env": { + "CODEANT_READ_ONLY": "1" + } + } + } +} diff --git a/README.md b/README.md index 37b95e4..eea0af9 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,24 @@ node src/index.js secrets --last-commit node src/index.js secrets --all ``` +## MCP / Claude Connector + +This package also ships an MCP (Model Context Protocol) server that exposes CodeAnt's scan, review, and PR data as tools to Claude and other MCP clients. The same source tree is packaged as a Desktop Extension (`.mcpb`) for one-click install in Claude Desktop. + +See [mcp.md](mcp.md) for the tools listing, install paths (Claude Code CLI, Claude Desktop manual config, MCPB double-click), and bundling/submission instructions. + +## Privacy Policy + +Full policy: **https://codeant.ai/privacy** + +Summary of what this CLI / MCP server sends and stores: + +- **Data sent to CodeAnt servers.** Authentication tokens, repository metadata (org, repo, branch, PR identifiers), and — for local review and secrets scanning — the code snippets and diffs you explicitly ask CodeAnt to scan. Nothing is sent on its own; every call is in response to a command you run or a tool Claude invokes. +- **Where it is stored.** On CodeAnt's infrastructure (https://api.codeant.ai or your self-hosted instance). Locally, the auth token is cached in `~/.codeant/config.json` on your machine. +- **Third-party sharing.** None beyond CodeAnt's own infrastructure. CodeAnt does not sell or share your data with third parties for marketing. +- **Retention.** Scan findings and PR data are retained per the CodeAnt account's retention policy (see the privacy URL above). Local config persists until you run `codeant logout` or delete `~/.codeant/config.json`. +- **Contact.** support@codeant.ai + ## License MIT diff --git a/mcp.md b/mcp.md new file mode 100644 index 0000000..e559698 --- /dev/null +++ b/mcp.md @@ -0,0 +1,194 @@ +# CodeAnt MCP Server + +The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAnt's scan/review/PR data as tools Claude (and any other MCP client) can call directly. + +- **Entrypoint:** `codeant mcp` — stdio transport, single subcommand registered in [src/index.js](src/index.js). +- **Server:** [src/mcp/server.js](src/mcp/server.js) — uses `@modelcontextprotocol/sdk` and registers tools via `server.registerTool(...)`. +- **Bundle:** the [mcpb/](mcpb/) directory + [scripts/build-mcpb.mjs](scripts/build-mcpb.mjs) produce a `.mcpb` Desktop Extension bundle (`dist/codeant.mcpb`). + +## Tools registered + +| Name | Read/Write | What it does | +|------|------------|--------------| +| `codeant_scans_orgs` | read | List authenticated CodeAnt orgs. | +| `codeant_scans_repos` | read | List repos in an org. | +| `codeant_scans_history` | read | Recent scan runs for a repo. | +| `codeant_scans_get` | read | Severity/category summary for one scan. | +| `codeant_scans_results` | read | Full findings (SAST, SCA, secrets, IaC, …) for one scan. | +| `codeant_scans_dismissed` | read | Dismissed alerts for a repo. | +| `codeant_pr_list` | read | List PRs/MRs across GitHub, GitLab, Bitbucket, Azure DevOps. | +| `codeant_pr_get` | read | Detail for a PR/MR. | +| `codeant_pr_comments` | read | Comments on a PR, filtered. | +| `codeant_comments_search` | read | Free-text search across CodeAnt review comments. | +| `codeant_review_local` | read | Run a CodeAnt review on local working-copy changes. | +| `codeant_scans_start` | **write** | Trigger a new scan. Gated. | +| `codeant_pr_resolve` | **write** | Resolve a PR conversation thread. Gated. | + +Write tools are only registered when `CODEANT_READ_ONLY=0`. Default = read-only. + +Every tool carries MCP annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so the client can decide whether to auto-approve calls. + +## Configuration (env vars) + +| Var | Purpose | Default | +|-----|---------|---------| +| `CODEANT_API_TOKEN` | API token (CodeAnt account → settings → API). Required. | — | +| `CODEANT_API_URL` | API base URL. Override for self-hosted. | `https://api.codeant.ai` | +| `CODEANT_READ_ONLY` | `1` to hide write tools, `0` to expose them. | `1` | + +The CLI also reads these from `~/.codeant/config.json` when not set in env. For MCP usage prefer env — it keeps per-client config explicit. + +--- + +## Install paths + +### A — Claude Code CLI (terminal / VS Code extension) + +```bash +claude mcp add codeant -s user -e CODEANT_READ_ONLY=1 -- codeant mcp +``` + +`-s user` puts it in your user-scope config so it works in every project. Use `-s project` to scope it to one repo (writes a `.mcp.json` at the repo root). + +For a single project, you can instead drop this `.mcp.json` next to the project root: + +```json +{ + "mcpServers": { + "codeant": { + "command": "codeant", + "args": ["mcp"], + "env": { "CODEANT_READ_ONLY": "1" } + } + } +} +``` + +Verify with `/mcp` inside a Claude Code session. + +### B — Claude Desktop (manual config) + +Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the equivalent on Windows and add a `codeant` block: + +```json +{ + "mcpServers": { + "codeant": { + "command": "codeant", + "args": ["mcp"], + "env": { "CODEANT_READ_ONLY": "1" } + } + } +} +``` + +Requires the `codeant` CLI to be on the desktop app's PATH. If you hit "command not found" in `~/Library/Logs/Claude/mcp-server-codeant.log`, swap `"command": "codeant"` for `"command": "node"` with `"args": ["/codeant-cli/src/index.js", "mcp"]`. + +Quit Claude Desktop (Cmd-Q — not just close the window) and relaunch. The server appears under **Settings → Connectors**. + +### C — Claude Desktop (MCPB double-click) + +The MCPB bundle is the only path that does not assume the user has the CodeAnt CLI installed — it ships everything in the bundle. This is what we submit to the Anthropic directory. + +```bash +npm run mcpb:build +open -a Claude dist/codeant.mcpb +``` + +Claude pops an install dialog asking for the `user_config` fields: +- **CodeAnt API token** (sensitive, required) +- **API base URL** (defaults to `https://api.codeant.ai`) +- **Read-only mode** (defaults to on) + +After install, manage it in **Settings → Connectors**. + +If you also have a manual `claude_desktop_config.json` entry from path B, you'll see `codeant` listed twice — remove one to avoid duplicate tool listings. + +--- + +## Packaging the MCPB bundle + +### What gets bundled + +``` +codeant.mcpb (zip) +├── manifest.json # from mcpb/manifest.json, version pinned to package.json +├── icon.png # 256×256, from mcpb/icon.png +├── server/ +│ └── index.js # thin entry wrapper, calls startMcpServer() +├── src/ # the entire CLI source tree +├── node_modules/ # production deps only (npm install --omit=dev) +└── package.json # trimmed: only name/version/type/dependencies +``` + +The bundle is self-contained — Claude Desktop ships its own Node runtime, so no global install is required on the user's machine. + +### Build it + +```bash +npm run mcpb:build +``` + +Under the hood this runs [scripts/build-mcpb.mjs](scripts/build-mcpb.mjs): + +1. Clean `dist/mcpb-stage/` and `dist/codeant.mcpb`. +2. Copy `src/` and `mcpb/server/` into the staging dir. +3. Write a trimmed `package.json` containing only production deps. +4. Run `npm install --omit=dev` inside the staging dir. +5. Copy `mcpb/manifest.json` (with `version` overridden from the root `package.json`) and `mcpb/icon.png`. +6. Zip the staging dir as `dist/codeant.mcpb`. + +Result is typically ~11 MB. Verify the bundled server speaks MCP: + +```bash +cd dist/mcpb-stage +(printf '%s\n' \ + '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \ + '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; sleep 1) | node server/index.js +``` + +Expect 11 tools in the `tools/list` response (or 13 if `CODEANT_READ_ONLY=0`). + +### Bumping the version + +Edit `version` in [package.json](package.json). The build script pins the manifest's `version` field from there, so you do not need to touch [mcpb/manifest.json](mcpb/manifest.json) for normal bumps. + +### Replacing the icon + +`mcpb/icon.png` should be a 256×256 PNG. The current one was rasterized from `../vscode-build/assets/Logo.svg` via `qlmanage`. To regenerate from a new SVG: + +```bash +qlmanage -t -s 256 -o /tmp /path/to/Logo.svg +cp /tmp/Logo.svg.png mcpb/icon.png +npm run mcpb:build +``` + +--- + +## Submitting to the Anthropic directory + +CodeAnt's MCP server uses stdio + a packaged bundle, so the submission route is **Desktop Extensions (MCPB)**, not the remote-connector form. + +- **Submission URL:** https://claude.com/docs/connectors/building/submission +- **Bundle:** upload `dist/codeant.mcpb` +- **Required metadata:** already in [mcpb/manifest.json](mcpb/manifest.json) — display name, description, author, homepage, documentation, repository, license, keywords, `privacy_policies`, `tools` static listing, `user_config` schema. +- **Privacy policy.** Linked from both [README.md](README.md#privacy-policy) and the manifest's `privacy_policies` field (`https://codeant.ai/privacy`). + +Reviewer notes worth preparing: + +- **Auth model.** CodeAnt uses a user-scoped API token entered into `user_config.api_token`. The token never leaves the user's machine — the bundle talks to `api.codeant.ai` (or the user's self-hosted URL) directly. No third-party OAuth flow needed. +- **Sandbox creds.** Email support@codeant.ai for a reviewer sandbox token; paste it into the submission form's reviewer-notes field along with an org slug that has scans + PRs to browse. +- **Write tools.** Gated behind `user_config.read_only` (defaults to on). Reviewers can toggle off to test `codeant_scans_start` / `codeant_pr_resolve`. + +--- + +## Troubleshooting + +| Symptom | Where to look | +|---|---| +| Server fails to start in Claude Desktop | `~/Library/Logs/Claude/mcp-server-codeant.log` | +| Tool calls return errors | Same log — server stderr is captured there. | +| Tool not listed at all | Check `CODEANT_READ_ONLY` — write tools are hidden when set to `1`. | +| Auth errors on every tool | Confirm `CODEANT_API_TOKEN` is set in the MCP env (not just in `~/.codeant/config.json`). | +| MCPB install dialog never appears | Open `.mcpb` with `open -a Claude dist/codeant.mcpb` to force the desktop app to handle it. | diff --git a/mcpb/icon.png b/mcpb/icon.png new file mode 100644 index 0000000..35f3130 Binary files /dev/null and b/mcpb/icon.png differ diff --git a/mcpb/manifest.json b/mcpb/manifest.json new file mode 100644 index 0000000..6a2dd9c --- /dev/null +++ b/mcpb/manifest.json @@ -0,0 +1,93 @@ +{ + "manifest_version": "0.3", + "name": "codeant", + "display_name": "CodeAnt AI", + "version": "0.5.0", + "description": "Drive CodeAnt AI security scans and code review from Claude — org-wide secret triage, cross-repo SAST/SCA findings, on-demand scans, and local PR review.", + "long_description": "CodeAnt AI inside Claude. Ask things like \"how many critical SAST findings do I have across my org?\", \"show every exposed secret in payments-service\", or \"review my staged changes\" — Claude calls the CodeAnt API directly via this MCP server.\n\nIncludes 11 read-only tools (orgs, repos, scan history, scan metadata, findings, dismissed alerts, PRs, comments, comment search, local review) and 2 opt-in write tools (trigger a scan, resolve a PR conversation) gated behind a setting.\n\nRequires a CodeAnt account. Sign up at https://codeant.ai and grab an API key from your settings page.", + "author": { + "name": "CodeAnt AI", + "email": "support@codeant.ai", + "url": "https://codeant.ai" + }, + "homepage": "https://codeant.ai", + "documentation": "https://docs.codeant.ai/cli/claude-code-plugin", + "support": "https://docs.codeant.ai/support", + "repository": { + "type": "git", + "url": "https://github.com/CodeAnt-AI/codeant-cli" + }, + "license": "MIT", + "keywords": [ + "code-review", + "security", + "secrets", + "sast", + "sca", + "static-analysis", + "pull-requests" + ], + "privacy_policies": [ + "https://codeant.ai/privacy" + ], + "icon": "icon.png", + "server": { + "type": "node", + "entry_point": "server/index.js", + "mcp_config": { + "command": "node", + "args": ["${__dirname}/server/index.js"], + "env": { + "CODEANT_API_TOKEN": "${user_config.api_token}", + "CODEANT_API_URL": "${user_config.base_url}", + "CODEANT_READ_ONLY": "${user_config.read_only}" + } + } + }, + "compatibility": { + "runtimes": { + "node": ">=18.0.0" + } + }, + "tools_generated": false, + "tools": [ + { "name": "codeant_scans_orgs", "description": "List the CodeAnt organizations the current user is authenticated to." }, + { "name": "codeant_scans_repos", "description": "List repositories connected to CodeAnt for an organization." }, + { "name": "codeant_scans_history", "description": "Show recent scan runs for a single repository." }, + { "name": "codeant_scans_get", "description": "Get summary metadata for a single scan (no findings)." }, + { "name": "codeant_scans_results", "description": "Fetch full findings (SAST, SCA, secrets, IaC, etc.) for a scan." }, + { "name": "codeant_scans_dismissed", "description": "List dismissed alerts for a repository." }, + { "name": "codeant_pr_list", "description": "List pull requests / merge requests across GitHub, GitLab, Bitbucket, Azure DevOps." }, + { "name": "codeant_pr_get", "description": "Fetch detailed information for a single PR/MR." }, + { "name": "codeant_pr_comments", "description": "List comments on a PR/MR with optional filters." }, + { "name": "codeant_comments_search", "description": "Search across CodeAnt review comments by free-text query." }, + { "name": "codeant_review_local", "description": "Run a CodeAnt AI review on local working-copy changes." }, + { "name": "codeant_login", "description": "Open app.codeant.ai in the browser and poll until the user completes sign-in; saves the resulting API token." }, + { "name": "codeant_scans_start", "description": "Trigger a new scan run (write — gated behind read_only=false)." }, + { "name": "codeant_pr_resolve", "description": "Resolve a PR conversation thread (write — gated behind read_only=false)." } + ], + "user_config": { + "api_token": { + "type": "string", + "title": "CodeAnt API token", + "description": "Optional. Leave blank and run the `codeant_login` tool to sign in through your browser instead. Otherwise paste a token from your CodeAnt account settings at app.codeant.ai.", + "required": false, + "sensitive": true, + "default": "" + }, + "base_url": { + "type": "string", + "title": "API base URL", + "description": "Override only if you are on a self-hosted CodeAnt instance.", + "required": false, + "default": "https://api.codeant.ai" + }, + "read_only": { + "type": "boolean", + "title": "Read-only mode", + "description": "When enabled, write tools (trigger scan, resolve PR thread) are hidden. Recommended.", + "required": false, + "default": true + } + } +} diff --git a/mcpb/server/index.js b/mcpb/server/index.js new file mode 100644 index 0000000..0ffe93d --- /dev/null +++ b/mcpb/server/index.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node +// MCPB bundle entry point. The build script (scripts/build-mcpb.mjs) copies +// the entire `src/` tree alongside this file so the relative import below resolves. +import { startMcpServer } from '../src/mcp/server.js'; + +startMcpServer().catch((err) => { + // stderr is forwarded to mcp-server-codeant.log in Claude Desktop. + console.error('[codeant-mcpb] fatal:', err); + process.exit(1); +}); diff --git a/package-lock.json b/package-lock.json index e0f7171..707a7fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@gitbeaker/rest": "^43.8.0", + "@modelcontextprotocol/sdk": "^1.29.0", "@octokit/rest": "^22.0.1", "azure-devops-node-api": "^15.1.2", "bitbucket": "^2.12.0", @@ -19,7 +20,8 @@ "open": "^10.1.0", "posthog-node": "^5.28.5", "react": "^18.3.1", - "smol-toml": "^1.6.1" + "smol-toml": "^1.6.1", + "zod": "^3.25.76" }, "bin": { "codeant": "src/index.js" @@ -79,6 +81,18 @@ "node": ">=18.20.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -118,6 +132,46 @@ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -675,6 +729,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -699,6 +766,39 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", @@ -789,6 +889,30 @@ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -804,6 +928,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -961,6 +1094,28 @@ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-to-spaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", @@ -969,6 +1124,41 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -986,7 +1176,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -1059,6 +1248,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/des.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", @@ -1090,11 +1288,26 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -1138,6 +1351,12 @@ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==" }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -1155,6 +1374,36 @@ "@types/estree": "^1.0.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -1217,6 +1466,67 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-content-type-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", @@ -1232,6 +1542,67 @@ } ] }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1354,6 +1725,35 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.21", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.21.tgz", + "integrity": "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -1363,6 +1763,22 @@ "node": ">=16.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -1426,6 +1842,24 @@ } } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -1492,6 +1926,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -1524,6 +1964,15 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-md4": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", @@ -1534,6 +1983,18 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-with-bigint": { "version": "3.5.8", "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", @@ -1592,12 +2053,58 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -1647,8 +2154,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { "version": "3.3.11", @@ -1668,6 +2174,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -1714,6 +2229,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -1725,6 +2249,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -1772,6 +2317,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/patch-console": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", @@ -1788,6 +2342,16 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -1820,6 +2384,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -1910,6 +2483,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", @@ -1924,11 +2510,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/rate-limiter-flexible": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-8.3.0.tgz", "integrity": "sha512-mzwlfipDLlRinPgELqVDJetke6Snq26nL565m8nLWXIcWgosYSeNRgqwh7ZrZ4MfYs8CNfmLvR5SBVz3rISQsQ==" }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1961,6 +2571,15 @@ "react": "^18.3.1" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -2020,6 +2639,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -2032,6 +2667,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -2040,6 +2681,57 @@ "loose-envify": "^1.1.0" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2204,6 +2896,15 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -2294,6 +2995,15 @@ "node": ">=14.0.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -2327,6 +3037,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-rest-client": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.1.0.tgz", @@ -2358,11 +3099,29 @@ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -2989,6 +3748,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -3045,6 +3810,24 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/package.json b/package.json index 8ce44c9..0f6a9a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeant-cli", - "version": "0.4.14", + "version": "0.5.0", "description": "Code review CLI tool", "type": "module", "bin": { @@ -9,7 +9,8 @@ "scripts": { "start": "node src/index.js", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "mcpb:build": "node scripts/build-mcpb.mjs" }, "keywords": [ "cli", @@ -40,6 +41,7 @@ ], "dependencies": { "@gitbeaker/rest": "^43.8.0", + "@modelcontextprotocol/sdk": "^1.29.0", "@octokit/rest": "^22.0.1", "azure-devops-node-api": "^15.1.2", "bitbucket": "^2.12.0", @@ -49,7 +51,8 @@ "open": "^10.1.0", "posthog-node": "^5.28.5", "react": "^18.3.1", - "smol-toml": "^1.6.1" + "smol-toml": "^1.6.1", + "zod": "^3.25.76" }, "devDependencies": { "vitest": "^1.6.1" diff --git a/scripts/build-mcpb.mjs b/scripts/build-mcpb.mjs new file mode 100644 index 0000000..e796c46 --- /dev/null +++ b/scripts/build-mcpb.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node +// Build the CodeAnt MCPB bundle. +// +// Stages files into dist/mcpb-stage/, installs production dependencies, then +// zips the staged tree as dist/codeant.mcpb (a plain zip per the MCPB spec). +// +// Usage: +// node scripts/build-mcpb.mjs + +import { mkdir, cp, rm, writeFile, readFile, stat } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, '..'); +const distDir = join(repoRoot, 'dist'); +const stageDir = join(distDir, 'mcpb-stage'); +const outPath = join(distDir, 'codeant.mcpb'); + +const pkg = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8')); +const manifest = JSON.parse(await readFile(join(repoRoot, 'mcpb', 'manifest.json'), 'utf8')); + +// Pin the manifest version to package.json so a single bump propagates. +manifest.version = pkg.version; + +async function exists(p) { + try { await stat(p); return true; } catch { return false; } +} + +function run(cmd, args, opts = {}) { + const r = spawnSync(cmd, args, { stdio: 'inherit', ...opts }); + if (r.status !== 0) throw new Error(`${cmd} ${args.join(' ')} exited with ${r.status}`); +} + +async function main() { + console.log(`[build-mcpb] repo root: ${repoRoot}`); + console.log(`[build-mcpb] cleaning ${stageDir}`); + await rm(stageDir, { recursive: true, force: true }); + await rm(outPath, { force: true }); + await mkdir(stageDir, { recursive: true }); + + console.log('[build-mcpb] staging src/ → mcpb-stage/src/'); + await cp(join(repoRoot, 'src'), join(stageDir, 'src'), { recursive: true }); + + console.log('[build-mcpb] staging mcpb/server/ → mcpb-stage/server/'); + await cp(join(repoRoot, 'mcpb', 'server'), join(stageDir, 'server'), { recursive: true }); + + console.log('[build-mcpb] writing manifest.json'); + await writeFile(join(stageDir, 'manifest.json'), JSON.stringify(manifest, null, 2)); + + console.log('[build-mcpb] writing trimmed package.json (production deps only)'); + const stagedPkg = { + name: 'codeant-mcpb', + version: pkg.version, + private: true, + type: pkg.type, + dependencies: pkg.dependencies, + }; + await writeFile(join(stageDir, 'package.json'), JSON.stringify(stagedPkg, null, 2)); + + console.log('[build-mcpb] installing production dependencies (npm install --omit=dev)'); + run('npm', ['install', '--omit=dev', '--no-audit', '--no-fund', '--silent'], { cwd: stageDir }); + + const iconSrc = join(repoRoot, 'mcpb', 'icon.png'); + if (await exists(iconSrc)) { + await cp(iconSrc, join(stageDir, 'icon.png')); + console.log('[build-mcpb] included icon.png'); + } else { + console.log('[build-mcpb] note: mcpb/icon.png not found — directory listing will use the default icon'); + } + + console.log(`[build-mcpb] zipping → ${outPath}`); + run('zip', ['-r', '-q', outPath, '.'], { cwd: stageDir }); + + const { size } = await stat(outPath); + console.log(`[build-mcpb] done: ${outPath} (${(size / 1024 / 1024).toFixed(2)} MB)`); + console.log('[build-mcpb] sanity check: list top-level entries inside the bundle'); + run('unzip', ['-l', outPath]); +} + +main().catch((err) => { + console.error('[build-mcpb] FAILED:', err); + process.exit(1); +}); diff --git a/src/index.js b/src/index.js index d921c8d..4c30079 100755 --- a/src/index.js +++ b/src/index.js @@ -397,6 +397,15 @@ program // ─── Settings commands ─── registerSettingsCommands(program, { runCmd }); + // ─── MCP server (for Claude Code plugin and other MCP clients) ─── + program + .command('mcp') + .description('Run the CodeAnt MCP server over stdio (used by Claude Code plugin)') + .action(async () => { + const { startMcpServer } = await import('./mcp/server.js'); + await startMcpServer(); + }); + // ─── Telemetry control ─── program .command('set-telemetry ') diff --git a/src/mcp/server.js b/src/mcp/server.js new file mode 100644 index 0000000..d48f254 --- /dev/null +++ b/src/mcp/server.js @@ -0,0 +1,477 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { createRequire } from 'module'; + +import { runOrgs } from '../commands/scans/orgs.js'; +import { runRepos } from '../commands/scans/repos.js'; +import { runHistory } from '../commands/scans/history.js'; +import { runGet } from '../commands/scans/get.js'; +import { runResults } from '../commands/scans/results.js'; +import { runDismissed } from '../commands/scans/dismissed.js'; +import { runStartScan } from '../commands/scans/start-scan.js'; +import { runReviewHeadless } from '../reviewHeadless.js'; +import * as scm from '../scm/index.js'; +import { isAlreadyLoggedIn, runLoginFlow } from '../utils/loginFlow.js'; +import { getConfigValue } from '../utils/config.js'; + +const require = createRequire(import.meta.url); +const pkg = require('../../package.json'); + +const READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }; +const WRITE_NON_DESTRUCTIVE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }; + +// Write-side tools are gated behind CODEANT_READ_ONLY. Default = read-only. +function isReadOnly() { + const v = process.env.CODEANT_READ_ONLY; + if (v === undefined) return true; + return v !== '0' && v.toLowerCase() !== 'false'; +} + +function ok(value) { + return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] }; +} + +function fail(err) { + const message = err instanceof Error ? err.message : String(err); + return { + isError: true, + content: [{ type: 'text', text: JSON.stringify({ error: message }, null, 2) }], + }; +} + +function resolveRepoOpts(input) { + const remote = input.remote || scm.detectRemote(); + const name = input.name || scm.detectRepoName(); + const defaultBranch = input.defaultBranch || scm.detectDefaultBranch(); + if (!remote) throw new Error('Could not detect remote. Pass `remote` (github|gitlab|bitbucket|azure).'); + if (!name) throw new Error('Could not detect repo name. Pass `name` (owner/repo).'); + return { ...input, remote, name, defaultBranch }; +} + +// Capture stdout from a function that writes JSON to stdout (used for `scans results` and `scans start-scan`). +async function captureStdout(fn) { + const chunks = []; + const origWrite = process.stdout.write.bind(process.stdout); + process.stdout.write = (chunk) => { + chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8')); + return true; + }; + try { + await fn(); + } finally { + process.stdout.write = origWrite; + } + return chunks.join(''); +} + +async function ensureAuthenticated() { + const envToken = process.env.CODEANT_API_TOKEN; + if (envToken && envToken.trim()) return; + if (isAlreadyLoggedIn()) { + process.env.CODEANT_API_TOKEN = getConfigValue('apiKeyV2'); + return; + } + + console.error('[codeant-mcp] No API token configured — opening browser for sign-in.'); + try { + await runLoginFlow(); + console.error('[codeant-mcp] Login complete.'); + } catch (err) { + console.error(`[codeant-mcp] Login failed: ${err.message}. The server will start anyway; call the codeant_login tool to retry.`); + } +} + +export async function startMcpServer() { + await ensureAuthenticated(); + + const server = new McpServer({ name: 'codeant', version: pkg.version }); + const readOnly = isReadOnly(); + + // ─── Scans: discovery ──────────────────────────────────────────────────── + server.registerTool( + 'codeant_scans_orgs', + { + title: 'List CodeAnt organizations', + description: 'List the CodeAnt organizations the current user is authenticated to. Use this first when the user has not specified an org.', + inputSchema: {}, + annotations: READ, + }, + async () => { + try { return ok(await runOrgs()); } catch (err) { return fail(err); } + } + ); + + server.registerTool( + 'codeant_scans_repos', + { + title: 'List repositories in a CodeAnt org', + description: 'List repositories connected to CodeAnt for a given organization. Use this to enumerate repos before fanning out org-wide queries (e.g. "secrets across all repos"). If `org` is omitted and the user has exactly one org, it is auto-picked.', + inputSchema: { + org: z.string().optional().describe('Organization name. Optional when only one org is authenticated.'), + }, + annotations: READ, + }, + async ({ org }) => { + try { return ok(await runRepos({ org })); } catch (err) { return fail(err); } + } + ); + + // ─── Scans: history + metadata ─────────────────────────────────────────── + server.registerTool( + 'codeant_scans_history', + { + title: 'List scan history for a repo', + description: 'Show recent scan runs for a single repository. Use this to find a scan ID/commit SHA to drill into, or to answer "when did this repo last get scanned".', + inputSchema: { + repo: z.string().describe('Repository in owner/repo form.'), + branch: z.string().optional().describe('Filter by branch name.'), + since: z.string().optional().describe('ISO 8601 date; only return scans newer than this.'), + limit: z.number().int().positive().max(100).optional().describe('Max scans returned (default 20).'), + }, + annotations: READ, + }, + async ({ repo, branch, since, limit }) => { + try { return ok(await runHistory({ repo, branch, since, limit: limit ?? 20 })); } catch (err) { return fail(err); } + } + ); + + server.registerTool( + 'codeant_scans_get', + { + title: 'Get scan metadata summary', + description: 'Get summary metadata for a single scan (severity + category counts only — no findings). Use this to size up a scan before pulling full results.', + inputSchema: { + repo: z.string().describe('Repository in owner/repo form.'), + scan: z.string().optional().describe('Specific commit SHA. Either `scan` or `branch` should be provided.'), + branch: z.string().optional().describe('Resolve the latest scan on this branch.'), + types: z.string().optional().describe('Comma-separated scan types (default "all"). e.g. "sast,secrets".'), + }, + annotations: READ, + }, + async ({ repo, scan, branch, types }) => { + try { return ok(await runGet({ repo, scan, branch, types: types ?? 'all' })); } catch (err) { return fail(err); } + } + ); + + // ─── Scans: findings ───────────────────────────────────────────────────── + server.registerTool( + 'codeant_scans_results', + { + title: 'Fetch scan findings', + description: 'Fetch full findings (SAST, SCA, secrets, IaC, dead code, anti-patterns, etc.) for a single scan on a single repository. Returns the raw findings as JSON. For org-wide queries, call `codeant_scans_repos` first and fan out per-repo.', + inputSchema: { + repo: z.string().describe('Repository in owner/repo form.'), + scan: z.string().optional().describe('Specific commit SHA.'), + branch: z.string().optional().describe('Resolve the latest scan on this branch.'), + types: z.string().optional().describe('Comma-separated types: sast,sca,secrets,iac,dead_code,sbom,anti_patterns,docstring,complex_functions,all (default "all").'), + severity: z.string().optional().describe('Comma-separated severities (e.g. "critical,high").'), + path: z.string().optional().describe('File path glob filter.'), + check: z.string().optional().describe('Filter by check ID or name (regex).'), + filterDismissed: z.boolean().optional().describe('Exclude dismissed findings (default false).'), + includeFalsePositives: z.boolean().optional().describe('Include false positives (default true).'), + fields: z.string().optional().describe('Project findings to a subset of fields (comma-separated).'), + limit: z.number().int().positive().max(500).optional().describe('Max findings per page (default 100).'), + offset: z.number().int().nonnegative().optional().describe('Pagination offset (default 0).'), + }, + annotations: READ, + }, + async (input) => { + try { + const text = await captureStdout(() => + runResults({ + repo: input.repo, + scan: input.scan, + branch: input.branch, + types: input.types ?? 'all', + severity: input.severity, + path: input.path, + check: input.check, + filterDismissed: input.filterDismissed ?? false, + includeFalsePositives: input.includeFalsePositives ?? true, + format: 'json', + output: undefined, + fields: input.fields, + limit: input.limit ?? 100, + offset: input.offset ?? 0, + failFast: false, + }) + ); + // runResults already emits JSON; pass it through unparsed to preserve shape. + return { content: [{ type: 'text', text: text || '{}' }] }; + } catch (err) { + return fail(err); + } + } + ); + + server.registerTool( + 'codeant_scans_dismissed', + { + title: 'List dismissed alerts', + description: 'List dismissed alerts (false positives, accepted risk, etc.) for a repository. Useful when triaging to avoid re-surfacing already-handled findings.', + inputSchema: { + repo: z.string().describe('Repository in owner/repo form.'), + analysisType: z.enum(['security', 'secrets']).optional().describe('Analysis type (default "security").'), + }, + annotations: READ, + }, + async ({ repo, analysisType }) => { + try { return ok(await runDismissed({ repo, analysisType: analysisType ?? 'security' })); } catch (err) { return fail(err); } + } + ); + + // ─── Pull requests (SCM, read-only) ────────────────────────────────────── + server.registerTool( + 'codeant_pr_list', + { + title: 'List pull requests', + description: 'List pull requests / merge requests on the current repo (auto-detected from git remote unless `name`+`remote` are provided).', + inputSchema: { + name: z.string().optional().describe('Repository in owner/repo form. Auto-detected if omitted.'), + remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional().describe('Auto-detected if omitted.'), + defaultBranch: z.string().optional(), + sourceBranch: z.string().optional(), + author: z.string().optional().describe('Filter by author login (fuzzy).'), + state: z.enum(['open', 'closed']).optional().describe('Default "open".'), + limit: z.number().int().positive().max(100).optional(), + offset: z.number().int().nonnegative().optional(), + }, + annotations: READ, + }, + async (input) => { + try { + const opts = resolveRepoOpts(input); + return ok( + await scm.listPullRequests({ + name: opts.name, + remote: opts.remote, + defaultBranch: opts.defaultBranch, + sourceBranch: opts.sourceBranch, + authorLogin: opts.author, + state: opts.state ?? 'open', + limit: opts.limit ?? 20, + offset: opts.offset ?? 0, + }) + ); + } catch (err) { return fail(err); } + } + ); + + server.registerTool( + 'codeant_pr_get', + { + title: 'Get pull request details', + description: 'Fetch detailed information for a single PR/MR including review analysis.', + inputSchema: { + prNumber: z.number().int().positive(), + name: z.string().optional(), + remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional(), + defaultBranch: z.string().optional(), + }, + annotations: READ, + }, + async (input) => { + try { + const opts = resolveRepoOpts(input); + return ok( + await scm.getPullRequest({ + name: opts.name, + remote: opts.remote, + defaultBranch: opts.defaultBranch, + prNumber: input.prNumber, + }) + ); + } catch (err) { return fail(err); } + } + ); + + server.registerTool( + 'codeant_pr_comments', + { + title: 'List PR comments', + description: 'List comments on a PR/MR with optional filters (CodeAnt-authored only, resolved/unresolved, date range).', + inputSchema: { + prNumber: z.number().int().positive(), + name: z.string().optional(), + remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional(), + defaultBranch: z.string().optional(), + codeantGenerated: z.boolean().optional().describe('Only return comments authored by CodeAnt.'), + addressed: z.boolean().optional().describe('Filter by addressed/resolved status.'), + createdAfter: z.string().optional().describe('ISO 8601.'), + createdBefore: z.string().optional().describe('ISO 8601.'), + }, + annotations: READ, + }, + async (input) => { + try { + const opts = resolveRepoOpts(input); + return ok( + await scm.listPullRequestComments({ + name: opts.name, + remote: opts.remote, + defaultBranch: opts.defaultBranch, + prNumber: input.prNumber, + codeantGenerated: input.codeantGenerated, + addressed: input.addressed, + createdAfter: input.createdAfter, + createdBefore: input.createdBefore, + }) + ); + } catch (err) { return fail(err); } + } + ); + + server.registerTool( + 'codeant_comments_search', + { + title: 'Search CodeAnt review comments', + description: 'Search across CodeAnt review comments by free-text query. Returns matching comments with repo, PR, and file context.', + inputSchema: { + query: z.string(), + name: z.string().optional(), + remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional(), + limit: z.number().int().positive().max(50).optional(), + includeAddressed: z.boolean().optional(), + createdAfter: z.string().optional().describe('ISO 8601.'), + }, + annotations: READ, + }, + async (input) => { + try { + const opts = resolveRepoOpts(input); + return ok( + await scm.searchComments({ + name: opts.name, + remote: opts.remote, + query: input.query, + limit: input.limit ?? 10, + includeAddressed: input.includeAddressed ?? false, + createdAfter: input.createdAfter, + }) + ); + } catch (err) { return fail(err); } + } + ); + + // ─── Local review (read-only — does not modify files) ──────────────────── + server.registerTool( + 'codeant_review_local', + { + title: 'Review local working-copy changes', + description: 'Run a CodeAnt AI review on local working-copy changes and return the findings as JSON. Does not modify files — pair with editor tools to apply fixes. Use this for "review my changes" / "check my staged files" prompts.', + inputSchema: { + scope: z + .enum(['all', 'uncommitted', 'staged-only', 'committed', 'last-commit', 'last-n-commits', 'base-branch', 'base-commit']) + .optional() + .describe('Review scope. Default "uncommitted".'), + lastNCommits: z.number().int().positive().max(5).optional(), + baseBranch: z.string().optional(), + baseCommit: z.string().optional(), + include: z.array(z.string()).optional().describe('Glob patterns to include.'), + exclude: z.array(z.string()).optional().describe('Glob patterns to exclude.'), + }, + annotations: READ, + }, + async (input) => { + try { + const result = await runReviewHeadless({ + workspacePath: process.cwd(), + scanType: input.scope ?? 'uncommitted', + lastNCommits: input.lastNCommits ?? 1, + include: input.include ?? [], + exclude: input.exclude ?? [], + baseBranch: input.baseBranch ?? null, + baseCommit: input.baseCommit ?? null, + onProgress: () => {}, + onFilesReady: () => {}, + }); + return ok(result); + } catch (err) { return fail(err); } + } + ); + + // ─── Auth (always registered — login is needed even in read-only mode) ─── + server.registerTool( + 'codeant_login', + { + title: 'Sign in to CodeAnt AI', + description: 'Opens app.codeant.ai in the user\'s browser and waits up to 10 minutes for them to complete sign-in. Tell the user to check their browser and finish the flow there. On success the API token is saved to ~/.codeant/config.json (apiKeyV2) and set on the running MCP process, so subsequent tool calls are authenticated without restart. Returns { alreadyLoggedIn: true } immediately if a token is already configured, unless `force` is true.', + inputSchema: { + force: z.boolean().optional().describe('Re-authenticate even if a token is already configured. Default false.'), + }, + annotations: { ...WRITE_NON_DESTRUCTIVE, idempotentHint: true }, + }, + async ({ force }) => { + try { + const envToken = process.env.CODEANT_API_TOKEN; + if (!force && ((envToken && envToken.trim()) || isAlreadyLoggedIn())) { + return ok({ alreadyLoggedIn: true }); + } + const { token, loginUrl } = await runLoginFlow(); + const masked = token ? `${token.slice(0, 8)}…` : null; + return ok({ status: 'success', loginUrl, token: masked }); + } catch (err) { return fail(err); } + } + ); + + // ─── Write-side tools (gated behind CODEANT_READ_ONLY=0) ───────────────── + if (!readOnly) { + server.registerTool( + 'codeant_scans_start', + { + title: 'Trigger a new scan', + description: 'Trigger a new scan run for a repository. WRITE OPERATION — only enabled when CODEANT_READ_ONLY=0.', + inputSchema: { + repo: z.string().optional().describe('owner/repo (auto-detected from git remote if omitted).'), + branch: z.string().optional(), + commit: z.string().optional(), + include: z.string().optional().describe('Comma-separated globs.'), + exclude: z.string().optional().describe('Comma-separated globs.'), + }, + annotations: WRITE_NON_DESTRUCTIVE, + }, + async (input) => { + try { + const text = await captureStdout(() => runStartScan(input)); + return { content: [{ type: 'text', text: text || '{}' }] }; + } catch (err) { return fail(err); } + } + ); + + server.registerTool( + 'codeant_pr_resolve', + { + title: 'Resolve a PR conversation', + description: 'Resolve a conversation/comment thread on a PR. WRITE OPERATION — only enabled when CODEANT_READ_ONLY=0.', + inputSchema: { + prNumber: z.number().int().positive(), + name: z.string().optional(), + remote: z.enum(['github', 'gitlab', 'bitbucket', 'azure']).optional(), + commentId: z.number().int().optional(), + threadId: z.string().optional(), + discussionId: z.string().optional(), + }, + annotations: { ...WRITE_NON_DESTRUCTIVE, idempotentHint: true }, + }, + async (input) => { + try { + const opts = resolveRepoOpts(input); + return ok( + await scm.resolveConversation({ + name: opts.name, + remote: opts.remote, + prNumber: input.prNumber, + commentId: input.commentId, + threadId: input.threadId, + discussionId: input.discussionId, + }) + ); + } catch (err) { return fail(err); } + } + ); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); +} diff --git a/src/utils/loginFlow.js b/src/utils/loginFlow.js new file mode 100644 index 0000000..fbf01b1 --- /dev/null +++ b/src/utils/loginFlow.js @@ -0,0 +1,74 @@ +import { randomUUID } from 'crypto'; +import { getConfigValue, setConfigValue } from './config.js'; +import { getBaseUrl } from './baseUrl.js'; + +const DEFAULT_POLL_INTERVAL = 10_000; +const DEFAULT_TIMEOUT = 10 * 60 * 1000; + +export function isAlreadyLoggedIn() { + return !!getConfigValue('apiKeyV2'); +} + +export async function startLoginFlow() { + const token = randomUUID(); + const baseUrl = getBaseUrl(); + const loginUrl = `https://app.codeant.ai?ideLoginToken=${token}`; + const pollUrl = `${baseUrl}/extension/login/status?apiKey=${token}`; + + let browserOpened = false; + try { + const { default: open } = await import('open'); + await open(loginUrl); + browserOpened = true; + } catch { + // Caller can fall back to printing loginUrl. + } + + return { token, loginUrl, pollUrl, browserOpened }; +} + +export async function awaitLoginCompletion({ + token, + pollUrl, + pollIntervalMs = DEFAULT_POLL_INTERVAL, + timeoutMs = DEFAULT_TIMEOUT, + signal, +} = {}) { + if (!token || !pollUrl) { + throw new Error('awaitLoginCompletion requires token and pollUrl'); + } + + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + if (signal?.aborted) throw new Error('Login aborted'); + + try { + const response = await fetch(pollUrl); + const data = await response.json(); + if (data.status === 'yes') { + setConfigValue('apiKeyV2', token); + process.env.CODEANT_API_TOKEN = token; + return { ok: true, token }; + } + } catch { + // Network blip — keep polling. + } + + const remaining = deadline - Date.now(); + if (remaining <= 0) break; + await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining))); + } + + throw new Error('Login timed out. Please try again.'); +} + +export async function runLoginFlow({ + pollIntervalMs = DEFAULT_POLL_INTERVAL, + timeoutMs = DEFAULT_TIMEOUT, + signal, +} = {}) { + const { token, loginUrl, pollUrl, browserOpened } = await startLoginFlow(); + const result = await awaitLoginCompletion({ token, pollUrl, pollIntervalMs, timeoutMs, signal }); + return { ...result, loginUrl, browserOpened }; +}