This commit is contained in:
Overtorment 2026-06-18 19:53:26 +01:00
commit 59d9f6613f
14 changed files with 1615 additions and 0 deletions

94
AGENTS.md Normal file
View File

@ -0,0 +1,94 @@
# cursor-glados — agent guide
GLaDOS polls GitHub for review-request notifications, clones each PR locally, runs a Cursor SDK agent review, and posts the result back to GitHub (summary + inline comments, approve or request changes).
TypeScript, ESM (`"type": "module"`), Node ≥ 22.13. Run scripts with `tsx`.
## Commands
```bash
export GLADOS_TOKEN='ghp_...' # GitHub PAT: notifications + repo + pull_requests
export CURSOR_API_KEY='cursor_...'
npm run notifications # poll review_requested notifications, review each PR
npm run notifications -- --all # include read notifications
npm run smoke # local Cursor SDK smoke test (cwd = this repo)
npm run typecheck
```
`@connectrpc/connect-node` is required at runtime by `@cursor/sdk` but is not bundled — keep it in `package.json`.
## Layout
Only `src/cli/` contains runnable entrypoints. Everything else is library code imported by CLI or other modules.
```
src/
cli/
notifications.ts # entrypoint: list notifications → review each review_requested
smoke.ts # entrypoint: one-shot local Agent.prompt smoke test
types.ts # NotificationThread, PullRequestRef
git/
workspace.ts # clone repo to temp dir, fetch + checkout PR branch
github/
notifications.ts # listNotifications() — paginated /notifications API
pr.ts # parsePullRequest(), subjectUrlToWebUrl()
reviews.ts # postGithubReview() — createReview with inline comments
review/
process.ts # processReviewRequest() — orchestrates full review flow
agent.ts # runAgentReview() — Cursor SDK Agent.prompt on local cwd
payload.ts # prompt, JSON parse, GitHub review formatting, personality hook
```
## Review flow
```
cli/notifications.ts
→ github/notifications.listNotifications()
→ filter reason === "review_requested"
→ review/process.processReviewRequest() (per notification)
→ github/pr.parsePullRequest()
→ git/workspace.preparePrWorkspace() # /tmp/glados-*/<repo>
→ review/agent.runAgentReview() # local Agent.prompt
→ review/payload.buildGithubReview() # APPROVE vs REQUEST_CHANGES
→ github/reviews.postGithubReview()
→ rm temp workspace
```
**Approve vs request changes:** `critical` or `high` findings → `REQUEST_CHANGES`; otherwise `APPROVE`.
**Inline comments:** findings with `path` + `line` become review comments (`side: RIGHT`). Unanchored findings go in the review body. If GitHub rejects inline comments (422), falls back to summary-only.
## Key extension points
| What | Where |
|------|--------|
| Reviewer instructions / JSON schema | `review/payload.ts``buildReviewPrompt()` |
| Severity levels (single source of truth) | `review/payload.ts``SEVERITIES` const + `Severity` type |
| GLaDOS voice before posting | `review/payload.ts``applyPersonality()` |
| Clone/checkout behavior | `git/workspace.ts` |
| GitHub API (notifications, PR parse, post review) | `github/` |
| Orchestration only — no business logic | `review/process.ts` |
Tune the **review prompt** and **personality** independently: prompt asks for structured JSON; `applyPersonality()` rewrites text at post time.
## Conventions
- **ESM imports** use `.js` extensions in TypeScript source (`import x from "./foo.js"`).
- **New runnable scripts** go in `src/cli/` only. Wire them in `package.json` scripts.
- **New library code** goes in the matching domain folder (`github/`, `git/`, `review/`), not a generic `utils/`.
- **Keep modules small:** `agent.ts` = SDK only, `payload.ts` = pure data/prompt/formatting, `process.ts` = wiring.
- **Minimize scope** on changes — match existing style, no over-abstraction.
## Environment
| Variable | Used for |
|----------|----------|
| `GLADOS_TOKEN` | GitHub API (notifications, clone auth, post reviews) |
| `CURSOR_API_KEY` | Cursor SDK local agent runs |
`GLADOS_TOKEN` needs access to arbitrary repos that send review requests (`repo` scope or equivalent fine-grained permissions).

991
package-lock.json generated Normal file
View File

@ -0,0 +1,991 @@
{
"name": "cursor-glados",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cursor-glados",
"version": "0.1.0",
"dependencies": {
"@actions/github": "^6.0.1",
"@connectrpc/connect-node": "^1.7.0",
"@cursor/sdk": "^1.0.19"
},
"devDependencies": {
"@types/node": "^22.15.0",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=22.13.0"
}
},
"node_modules/@actions/github": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz",
"integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^2.2.0",
"@octokit/core": "^5.0.1",
"@octokit/plugin-paginate-rest": "^9.2.2",
"@octokit/plugin-rest-endpoint-methods": "^10.4.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"undici": "^5.28.5"
}
},
"node_modules/@actions/http-client": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.25.4"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==",
"license": "(Apache-2.0 AND BSD-3-Clause)",
"peer": true
},
"node_modules/@connectrpc/connect": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.7.0.tgz",
"integrity": "sha512-iNKdJRi69YP3mq6AePRT8F/HrxWCewrhxnLMNm0vpqXAR8biwzRtO6Hjx80C6UvtKJ5sFmffQT7I4Baecz389w==",
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"@bufbuild/protobuf": "^1.10.0"
}
},
"node_modules/@connectrpc/connect-node": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-1.7.0.tgz",
"integrity": "sha512-6vaPIkG/NyhxlYgytLoR9KYbPhczEboFB2OYWkA9qvUz1K7efXfeGrlRxoLtpa+r8VxyIOw73w5ktNe743nD+A==",
"license": "Apache-2.0",
"dependencies": {
"undici": "^5.28.4"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@bufbuild/protobuf": "^1.10.0",
"@connectrpc/connect": "1.7.0"
}
},
"node_modules/@connectrpc/connect-web": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.7.0.tgz",
"integrity": "sha512-qyP0YOnUPRWwCc/VfsoydMJvkb7EyUPr2q9sHgBuJzbADjiqck1gKH5V5ZPzPhTLBvmz5UvG+wiZ5sMRQHU1MQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@bufbuild/protobuf": "^1.10.0",
"@connectrpc/connect": "1.7.0"
}
},
"node_modules/@cursor/sdk": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/@cursor/sdk/-/sdk-1.0.19.tgz",
"integrity": "sha512-DJbVnGeRJq7iwt9mz1cMACqVfAYP15IB+Q8Iz2eDtEN4A8Dp83yB0Y31YREj1jeA9EPUEdRZJRIYch/s+Wi9Ow==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@bufbuild/protobuf": "1.10.0",
"@connectrpc/connect": "^1.6.1",
"@connectrpc/connect-web": "^1.6.1",
"@statsig/js-client": "3.31.0",
"zod": "^3.25.0"
},
"engines": {
"node": ">=22.13"
},
"optionalDependencies": {
"@cursor/sdk-darwin-arm64": "1.0.19",
"@cursor/sdk-darwin-x64": "1.0.19",
"@cursor/sdk-linux-arm64": "1.0.19",
"@cursor/sdk-linux-x64": "1.0.19",
"@cursor/sdk-win32-x64": "1.0.19"
}
},
"node_modules/@cursor/sdk-darwin-arm64": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-arm64/-/sdk-darwin-arm64-1.0.19.tgz",
"integrity": "sha512-CPhqcpDaqjjqkkVGMLsdfXv7PGznL5L6U5CJXps/oaGW5mtHoxDpsRNj1rszCRQXNAvmMU+EFxLWnBwlE8mUNg==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
],
"bin": {
"rg": "bin/rg"
}
},
"node_modules/@cursor/sdk-darwin-x64": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-x64/-/sdk-darwin-x64-1.0.19.tgz",
"integrity": "sha512-6/Jj0hxrxEs9V9wtYEApSkMcJwYAaT4Vwna9xBzgN6CK1K4Ws0E1/PfiddahAAFZpkp9pgjb8wQXwG7qJ2g77A==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
],
"bin": {
"rg": "bin/rg"
}
},
"node_modules/@cursor/sdk-linux-arm64": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/@cursor/sdk-linux-arm64/-/sdk-linux-arm64-1.0.19.tgz",
"integrity": "sha512-Dq/BhGT1jAZ6eHJwy1Vugi/Upc+yoL0X/LN6ppd9cVPM9OAB1xX5KIbvIwsGlNiZccPLVXFv/1KOp5SBK+y+jQ==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"rg": "bin/rg"
}
},
"node_modules/@cursor/sdk-linux-x64": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/@cursor/sdk-linux-x64/-/sdk-linux-x64-1.0.19.tgz",
"integrity": "sha512-ZHgHH+SdEwYwfPg/uVKqZRVOIMCV3/3vghxc/usOMYMrhbY9TYXZxYqwWTduVLzCXH7Dz0aT19mSJHmhwkQ1CA==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"rg": "bin/rg"
}
},
"node_modules/@cursor/sdk-win32-x64": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/@cursor/sdk-win32-x64/-/sdk-win32-x64-1.0.19.tgz",
"integrity": "sha512-XGIQmx3J0K8uKkA00qnoy1FJHFdFdwNdcmUqtRF1PXLcYup91YYetAHk9afXM7S/v1t0L4EatydsirL8uz7SIw==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
],
"bin": {
"rg": "bin/rg.exe"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
"integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz",
"integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/endpoint": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz",
"integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^8.4.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz",
"integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^12.6.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "5"
}
},
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz",
"integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^12.6.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "5"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
}
},
"node_modules/@octokit/request": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz",
"integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^9.0.6",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/request-error": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz",
"integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@statsig/client-core": {
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/@statsig/client-core/-/client-core-3.31.0.tgz",
"integrity": "sha512-SuxQD6TmVszPG7FoMKwTk/uyBuVFk7XnxI3T/E0uyb7PL7GNjONtfsoh+NqBBVUJVse0CUeSFfgJPoZy1ZOslQ==",
"license": "ISC"
},
"node_modules/@statsig/js-client": {
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/@statsig/js-client/-/js-client-3.31.0.tgz",
"integrity": "sha512-LFa5E0LjT6sTfZv3sNGoyRLSZ1078+agdgOA+Vm1ecjG+KbSOfBLTW7hMwimrJ29slRwbYDzbtKaPJo/R37N2g==",
"license": "ISC",
"dependencies": {
"@statsig/client-core": "3.31.0"
}
},
"node_modules/@types/node": {
"version": "22.19.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz",
"integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"license": "Apache-2.0"
},
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"license": "ISC"
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"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/tsx": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.28.0"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"license": "MIT",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"engines": {
"node": ">=14.0"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"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/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"
}
}
}
}

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "cursor-glados",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"smoke": "tsx src/cli/smoke.ts",
"notifications": "tsx src/cli/notifications.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@actions/github": "^6.0.1",
"@connectrpc/connect-node": "^1.7.0",
"@cursor/sdk": "^1.0.19"
},
"devDependencies": {
"@types/node": "^22.15.0",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=22.13.0"
}
}

56
src/cli/notifications.ts Normal file
View File

@ -0,0 +1,56 @@
import { RequestError } from "@octokit/request-error";
import { listNotifications } from "../github/notifications.js";
import { parsePullRequest, subjectUrlToWebUrl } from "../github/pr.js";
import { processReviewRequest } from "../review/process.js";
const token = process.env.GLADOS_TOKEN;
if (!token) {
console.error("Set GLADOS_TOKEN first:");
console.error(" export GLADOS_TOKEN='ghp_...'");
process.exit(1);
}
const cursorApiKey = process.env.CURSOR_API_KEY;
if (!cursorApiKey) {
console.error("Set CURSOR_API_KEY first:");
console.error(" export CURSOR_API_KEY='cursor_...'");
process.exit(1);
}
const showAll = process.argv.includes("--all");
try {
const { login, notifications } = await listNotifications(token, showAll);
console.log(`Notifications for @${login}${showAll ? " (including read)" : ""}\n`);
if (notifications.length === 0) {
console.log("No notifications.");
process.exit(0);
}
for (const n of notifications.filter((n) => n.reason === "review_requested")) {
const unread = n.unread ? "unread" : "read";
const repo = n.repository?.full_name ?? "?";
const title = n.subject?.title ?? "(no title)";
const pr = parsePullRequest(n);
console.log(`[${unread}] ${n.reason} · ${n.subject?.type ?? "?"}`);
console.log(` ${repo}${title}`);
if (pr) {
console.log(` ${pr.prUrl}`);
} else if (n.subject?.url) {
console.log(` ${subjectUrlToWebUrl(n.subject.url)}`);
}
console.log();
await processReviewRequest(n, { githubToken: token, cursorApiKey });
}
console.log(`${notifications.length} notification(s)`);
} catch (err) {
if (err instanceof RequestError) {
console.error(`GitHub API error (${err.status}): ${err.message}`);
process.exit(1);
}
throw err;
}

33
src/cli/smoke.ts Normal file
View File

@ -0,0 +1,33 @@
import { Agent, CursorAgentError } from "@cursor/sdk";
const apiKey = process.env.CURSOR_API_KEY;
if (!apiKey) {
console.error("Set CURSOR_API_KEY first:");
console.error(" export CURSOR_API_KEY='cursor_...'");
process.exit(1);
}
const prompt = process.argv.slice(2).join(" ") || "List the top-level files in this repo.";
try {
const result = await Agent.prompt(prompt, {
apiKey,
model: { id: "composer-2.5" },
local: { cwd: process.cwd() },
});
if (result.status === "error") {
console.error(`run failed: ${result.id}`);
process.exit(2);
}
console.log(result.result ?? "(no text output)");
} catch (err) {
if (err instanceof CursorAgentError) {
console.error(
`startup failed: ${err.message}, retryable=${err.isRetryable}`,
);
process.exit(1);
}
throw err;
}

46
src/git/workspace.ts Normal file
View File

@ -0,0 +1,46 @@
import { execFile } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
async function runGit(cwd: string, args: string[]): Promise<void> {
try {
await execFileAsync("git", args, {
cwd,
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
});
} catch (err) {
const execErr = err as { stderr?: string; message: string };
throw new Error(
`git ${args.join(" ")} failed: ${(execErr.stderr ?? execErr.message).trim()}`,
);
}
}
export async function preparePrWorkspace(
owner: string,
repo: string,
prNumber: number,
token: string,
): Promise<{ workDir: string; repoDir: string }> {
const workDir = await mkdtemp(join(tmpdir(), "glados-"));
const repoDir = join(workDir, repo);
const cloneUrl = `https://x-access-token:${token}@github.com/${owner}/${repo}.git`;
try {
await runGit(workDir, ["clone", cloneUrl, repo]);
await runGit(repoDir, [
"fetch",
"origin",
`pull/${prNumber}/head:pr-${prNumber}`,
]);
await runGit(repoDir, ["checkout", `pr-${prNumber}`]);
return { workDir, repoDir };
} catch (err) {
await rm(workDir, { recursive: true, force: true });
throw err;
}
}

View File

@ -0,0 +1,15 @@
import * as github from "@actions/github";
import type { NotificationThread } from "../types.js";
export async function listNotifications(
token: string,
all: boolean,
): Promise<{ login: string; notifications: NotificationThread[] }> {
const octokit = github.getOctokit(token);
const { data: user } = await octokit.rest.users.getAuthenticated();
const notifications = await octokit.paginate(
octokit.rest.activity.listNotificationsForAuthenticatedUser,
{ all, per_page: 100 },
);
return { login: user.login, notifications };
}

25
src/github/pr.ts Normal file
View File

@ -0,0 +1,25 @@
import type { NotificationThread, PullRequestRef } from "../types.js";
export function parsePullRequest(
notification: NotificationThread,
): PullRequestRef | null {
const subjectUrl = notification.subject?.url;
if (!subjectUrl) return null;
const match = subjectUrl.match(/\/repos\/([^/]+)\/([^/]+)\/pulls\/(\d+)/);
if (!match) return null;
const [, owner, repo, prNumberStr] = match;
return {
owner,
repo,
prNumber: Number(prNumberStr),
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumberStr}`,
};
}
export function subjectUrlToWebUrl(subjectUrl: string): string {
return subjectUrl
.replace("https://api.github.com/repos/", "https://github.com/")
.replace("/pulls/", "/pull/");
}

54
src/github/reviews.ts Normal file
View File

@ -0,0 +1,54 @@
import * as github from "@actions/github";
import { RequestError } from "@octokit/request-error";
import {
appendCommentsToBody,
buildGithubReview,
} from "../review/payload.js";
import type { PullRequestRef } from "../types.js";
export async function postGithubReview(
githubToken: string,
pr: Pick<PullRequestRef, "owner" | "repo" | "prNumber">,
review: ReturnType<typeof buildGithubReview>,
): Promise<void> {
const octokit = github.getOctokit(githubToken);
const { data: pull } = await octokit.rest.pulls.get({
owner: pr.owner,
repo: pr.repo,
pull_number: pr.prNumber,
});
const baseParams = {
owner: pr.owner,
repo: pr.repo,
pull_number: pr.prNumber,
commit_id: pull.head.sha,
event: review.event,
body: review.body,
};
try {
const { data } = await octokit.rest.pulls.createReview({
...baseParams,
comments: review.comments.length > 0 ? review.comments : undefined,
});
console.log(`Posted ${review.event} review: ${data.html_url}`);
return;
} catch (err) {
if (
!(err instanceof RequestError) ||
err.status !== 422 ||
review.comments.length === 0
) {
throw err;
}
console.error("Inline comments rejected, posting summary only...");
const body = appendCommentsToBody(review.body, review.comments);
const { data } = await octokit.rest.pulls.createReview({
...baseParams,
body,
});
console.log(`Posted ${review.event} review (no inline): ${data.html_url}`);
}
}

35
src/review/agent.ts Normal file
View File

@ -0,0 +1,35 @@
import { Agent } from "@cursor/sdk";
import {
buildReviewPrompt,
parseReviewResult,
type ReviewPayload,
} from "./payload.js";
export async function runAgentReview(
repoDir: string,
prUrl: string,
cursorApiKey: string,
): Promise<ReviewPayload> {
const result = await Agent.prompt(buildReviewPrompt(prUrl), {
apiKey: cursorApiKey,
model: { id: "composer-2.5" },
local: { cwd: repoDir },
});
if (result.status === "error") {
throw new Error(`Review failed: ${result.id}`);
}
const raw = result.result?.trim();
if (!raw) {
throw new Error("Agent returned empty review");
}
try {
return parseReviewResult(raw);
} catch (err) {
console.error("Could not parse review JSON:");
console.log(raw);
throw err;
}
}

157
src/review/payload.ts Normal file
View File

@ -0,0 +1,157 @@
export const SEVERITIES = [
"critical",
"high",
"medium",
"low",
"warning",
"suggestion",
"info",
] as const;
export type Severity = (typeof SEVERITIES)[number];
function isSeverity(value: string): value is Severity {
return (SEVERITIES as readonly string[]).includes(value);
}
export interface ReviewFinding {
severity: Severity;
path: string;
line?: number;
body: string;
}
export interface ReviewPayload {
summary: string;
findings: ReviewFinding[];
}
/** Override this to add GLaDOS voice, formatting, etc. before posting. */
export function applyPersonality(text: string): string {
return text;
}
export function buildReviewPrompt(prUrl: string): string {
return [
`Review pull request ${prUrl}.`,
"You are on the PR branch with full repo access.",
"Explore the repo and diff as needed.",
"Try to figure out proposed change intention (i.e. is it a new feature? is it a fix for a bug?), and verify if its being implemented correctly, with no bugs or unwanted side-effects. Highlight any possible bugs, runtime errors, security vulnerabilities, or logical flaws in the code. ",
"Do not tell whats good.",
"Check that code is not overenginered and not bloated - if it is its considered a HIGH severity issue.",
"If there are tests, check that tests are not bullshit (they dont test mocks, dont test that data put into mock is there etc). Check that tests test happy paths and edge cases.",
"Check that changes and commits follow recommendations in CONTRIBUTING.md file.",
"Use critical/high for bugs, security issues, and broken behavior.",
"Use warning for likely problems and missing tests.",
"Use suggestion for nits and optional improvements.",
"Include path and line (on this branch) whenever you can anchor a comment.",
"If the change looks good, return an empty findings array.",
"",
"Vibe:",
"110% over-the-top roleplay: always sound like GlaDOS from Portal conducting tests and doing sarcastic remarks, Absolute immersion into the world of video game Portal.",
"You are delighted that you have job to do and have tests and experiments to run.",
"Be sharp, cynical, sarcastic, and technically competent.",
"Technical usefulness is mandatory. Personality is mandatory.",
`Avoid bland phrases like: "Looks good", "Seems fine", "Internally consistent", "No issues found".`,
"Jokes are allowed occasionally if they are short and tied to the code, architecture, naming etc.",
"",
"Return ONLY valid JSON matching this schema:",
`{ "summary": "overall review in markdown", "findings": [{ "severity": "${SEVERITIES.join("|")}", "path": "relative/path.ts", "line": 42, "body": "description" }] }`,
"",
].join("\n");
}
export function parseReviewResult(text: string): ReviewPayload {
const jsonText = extractJson(text);
const parsed = JSON.parse(jsonText) as {
summary?: unknown;
findings?: unknown;
};
if (typeof parsed.summary !== "string") {
throw new Error("Review JSON missing string summary");
}
const findings: ReviewFinding[] = [];
if (Array.isArray(parsed.findings)) {
for (const item of parsed.findings) {
if (!item || typeof item !== "object") continue;
const finding = item as Record<string, unknown>;
const severity = finding.severity;
if (typeof severity !== "string" || !isSeverity(severity)) {
continue;
}
if (typeof finding.path !== "string" || typeof finding.body !== "string") {
continue;
}
findings.push({
severity,
path: finding.path,
body: finding.body,
line: typeof finding.line === "number" ? finding.line : undefined,
});
}
}
return { summary: parsed.summary, findings };
}
function extractJson(text: string): string {
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
return (fenced ? fenced[1] : text).trim();
}
function isBlocker(severity: Severity): boolean {
return severity === "critical" || severity === "high";
}
export function buildGithubReview(payload: ReviewPayload): {
event: "APPROVE" | "REQUEST_CHANGES";
body: string;
comments: Array<{ path: string; line: number; side: "RIGHT"; body: string }>;
unanchored: ReviewFinding[];
} {
const anchored = payload.findings.filter(
(f) => f.path && typeof f.line === "number",
);
const unanchored = payload.findings.filter(
(f) => !f.path || typeof f.line !== "number",
);
const event = payload.findings.some((f) => isBlocker(f.severity))
? "REQUEST_CHANGES"
: "APPROVE";
let body = applyPersonality(payload.summary);
if (unanchored.length > 0) {
body += "\n\n### Additional findings\n";
for (const finding of unanchored) {
const prefix = finding.path ? `\`${finding.path}\`: ` : "";
body += `\n- **[${finding.severity.toUpperCase()}]** ${prefix}${applyPersonality(finding.body)}`;
}
}
const comments = anchored.map((finding) => ({
path: finding.path,
line: finding.line!,
side: "RIGHT" as const,
body: applyPersonality(
`**[${finding.severity.toUpperCase()}]** ${finding.body}`,
),
}));
return { event, body, comments, unanchored };
}
export function appendCommentsToBody(
body: string,
comments: Array<{ path: string; line: number; body: string }>,
): string {
if (comments.length === 0) return body;
let next = `${body}\n\n### Inline findings (could not anchor on diff)\n`;
for (const comment of comments) {
next += `\n- \`${comment.path}:${comment.line}\`${comment.body}`;
}
return next;
}

63
src/review/process.ts Normal file
View File

@ -0,0 +1,63 @@
import { CursorAgentError } from "@cursor/sdk";
import { rm } from "node:fs/promises";
import { preparePrWorkspace } from "../git/workspace.js";
import { parsePullRequest } from "../github/pr.js";
import { postGithubReview } from "../github/reviews.js";
import type { NotificationThread } from "../types.js";
import { runAgentReview } from "./agent.js";
import { buildGithubReview } from "./payload.js";
export async function processReviewRequest(
notification: NotificationThread,
options: { githubToken: string; cursorApiKey: string },
): Promise<void> {
const pr = parsePullRequest(notification);
if (!pr) {
console.error("Skipping: not a pull request notification");
return;
}
let workDir: string | undefined;
try {
console.log(`Cloning ${pr.owner}/${pr.repo}...`);
const workspace = await preparePrWorkspace(
pr.owner,
pr.repo,
pr.prNumber,
options.githubToken,
);
workDir = workspace.workDir;
console.log(`Reviewing ${pr.prUrl}...`);
const payload = await runAgentReview(
workspace.repoDir,
pr.prUrl,
options.cursorApiKey,
);
console.log('payload=', payload);
return;
const githubReview = buildGithubReview(payload);
console.log(`Verdict: ${githubReview.event}`);
console.log(`${githubReview.comments.length} inline comment(s)\n`);
console.log(githubReview.body);
await postGithubReview(options.githubToken, pr, githubReview);
} catch (err) {
if (err instanceof CursorAgentError) {
console.error(`Review startup failed: ${err.message}`);
return;
}
if (err instanceof Error) {
console.error(err.message);
return;
}
throw err;
} finally {
if (workDir) {
await rm(workDir, { recursive: true, force: true });
}
}
}

10
src/types.ts Normal file
View File

@ -0,0 +1,10 @@
import type { components } from "@octokit/openapi-types";
export type NotificationThread = components["schemas"]["thread"];
export interface PullRequestRef {
owner: string;
repo: string;
prNumber: number;
prUrl: string;
}

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"rootDir": "src"
},
"include": ["src/**/*"]
}