mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-14 11:59:32 +00:00
Merge pull request #220 from siteboon/feature/agent-auto-pr
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/merge": "^6.11.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@replit/codemirror-minimap": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@uiw/react-codemirror": "^4.23.13",
|
||||
@@ -2186,7 +2187,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
|
||||
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
@@ -2196,7 +2196,6 @@
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.4.tgz",
|
||||
"integrity": "sha512-jOT8V1Ba5BdC79sKrRWDdMT5l1R+XNHTPR6CPWzUP2EcfAcvIHZWF0eAbmRcpOOP5gVIwnqNg0C4nvh6Abc3OA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
@@ -2215,7 +2214,6 @@
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
|
||||
"integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.0.0",
|
||||
@@ -2229,14 +2227,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/endpoint/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2246,7 +2242,6 @@
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz",
|
||||
"integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/request": "^10.0.2",
|
||||
@@ -2261,14 +2256,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2278,14 +2271,12 @@
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz",
|
||||
"integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz",
|
||||
"integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.1.0"
|
||||
@@ -2301,14 +2292,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2318,7 +2307,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
|
||||
"integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
@@ -2331,7 +2319,6 @@
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.1.0.tgz",
|
||||
"integrity": "sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^15.0.0"
|
||||
@@ -2347,7 +2334,6 @@
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz",
|
||||
"integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^11.0.0",
|
||||
@@ -2364,7 +2350,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
|
||||
"integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.0.0"
|
||||
@@ -2377,14 +2362,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request-error/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2394,14 +2377,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2411,7 +2392,6 @@
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz",
|
||||
"integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^7.0.2",
|
||||
@@ -2427,7 +2407,6 @@
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz",
|
||||
"integrity": "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^26.0.0"
|
||||
@@ -3414,7 +3393,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
|
||||
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
@@ -4981,7 +4959,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
|
||||
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -11192,7 +11169,6 @@
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
|
||||
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/merge": "^6.11.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@replit/codemirror-minimap": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@uiw/react-codemirror": "^4.23.13",
|
||||
@@ -75,8 +76,8 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
|
||||
@@ -550,6 +550,24 @@
|
||||
<td><span class="badge badge-optional">Optional</span></td>
|
||||
<td>GitHub token for private repos</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>branchName</code></td>
|
||||
<td>string</td>
|
||||
<td><span class="badge badge-optional">Optional</span></td>
|
||||
<td>Custom branch name to use. If provided, <code>createBranch</code> is automatically enabled. Branch names are validated against Git naming rules. Works with <code>githubUrl</code> or <code>projectPath</code> (if it has a GitHub remote).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>createBranch</code></td>
|
||||
<td>boolean</td>
|
||||
<td><span class="badge badge-optional">Optional</span></td>
|
||||
<td>Create a new branch after successful completion (default: <code>false</code>). Automatically set to <code>true</code> if <code>branchName</code> is provided. Works with <code>githubUrl</code> or <code>projectPath</code> (if it has a GitHub remote).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>createPR</code></td>
|
||||
<td>boolean</td>
|
||||
<td><span class="badge badge-optional">Optional</span></td>
|
||||
<td>Create a pull request after successful completion (default: <code>false</code>). PR title and description auto-generated from commit messages. Works with <code>githubUrl</code> or <code>projectPath</code> (if it has a GitHub remote).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -669,7 +687,15 @@ data: {"type":"done"}</code></pre>
|
||||
"cacheCreationTokens": 0,
|
||||
"totalTokens": 200
|
||||
},
|
||||
"projectPath": "/path/to/project"
|
||||
"projectPath": "/path/to/project",
|
||||
"branch": {
|
||||
"name": "fix-authentication-bug-abc123",
|
||||
"url": "https://github.com/user/repo/tree/fix-authentication-bug-abc123"
|
||||
},
|
||||
"pullRequest": {
|
||||
"number": 42,
|
||||
"url": "https://github.com/user/repo/pull/42"
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
@@ -700,6 +726,9 @@ data: {"type":"done"}</code></pre>
|
||||
|
||||
<h3>CI/CD Integration</h3>
|
||||
<p>Integrate with GitHub Actions or other CI/CD pipelines.</p>
|
||||
|
||||
<h3>Create Branch and Pull Request</h3>
|
||||
<p>Automatically create a new branch and pull request after the agent completes its work. Branch names are auto-generated from the message, and PR title/description are auto-generated from commit messages.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -741,6 +770,49 @@ data: {"type":"done"}</code></pre>
|
||||
"githubToken": "${{ secrets.GITHUB_TOKEN }}"
|
||||
}'</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-block">
|
||||
<h4>Create Branch and PR</h4>
|
||||
<pre><code class="language-bash">curl -X POST <span class="api-url">http://localhost:3001</span>/api/agent \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: ck_..." \
|
||||
-d '{
|
||||
"githubUrl": "https://github.com/user/repo",
|
||||
"message": "Fix authentication bug",
|
||||
"createBranch": true,
|
||||
"createPR": true,
|
||||
"stream": false
|
||||
}'</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-block">
|
||||
<h4>Custom Branch Name</h4>
|
||||
<pre><code class="language-bash">curl -X POST <span class="api-url">http://localhost:3001</span>/api/agent \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: ck_..." \
|
||||
-d '{
|
||||
"githubUrl": "https://github.com/user/repo",
|
||||
"message": "Add user authentication",
|
||||
"branchName": "feature/user-auth",
|
||||
"createPR": true,
|
||||
"stream": false
|
||||
}'</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-block">
|
||||
<h4>Branch & PR Response</h4>
|
||||
<pre><code class="language-json">{
|
||||
"success": true,
|
||||
"branch": {
|
||||
"name": "feature/user-auth",
|
||||
"url": "https://github.com/user/repo/tree/feature/user-auth"
|
||||
},
|
||||
"pullRequest": {
|
||||
"number": 42,
|
||||
"url": "https://github.com/user/repo/pull/42"
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { apiKeysDb, githubTokensDb } from '../database/db.js';
|
||||
import { addProjectManually } from '../projects.js';
|
||||
import { queryClaudeSDK } from '../claude-sdk.js';
|
||||
import { spawnCursor } from '../cursor-cli.js';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -81,6 +82,194 @@ function normalizeGitHubUrl(url) {
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse GitHub URL to extract owner and repo
|
||||
* @param {string} url - GitHub URL (HTTPS or SSH)
|
||||
* @returns {{owner: string, repo: string}} - Parsed owner and repo
|
||||
*/
|
||||
function parseGitHubUrl(url) {
|
||||
// Handle HTTPS URLs: https://github.com/owner/repo or https://github.com/owner/repo.git
|
||||
// Handle SSH URLs: git@github.com:owner/repo or git@github.com:owner/repo.git
|
||||
const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
||||
if (!match) {
|
||||
throw new Error('Invalid GitHub URL format');
|
||||
}
|
||||
return {
|
||||
owner: match[1],
|
||||
repo: match[2].replace('.git', '')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generate a branch name from a message
|
||||
* @param {string} message - The agent message
|
||||
* @returns {string} - Generated branch name
|
||||
*/
|
||||
function autogenerateBranchName(message) {
|
||||
// Convert to lowercase, replace spaces/special chars with hyphens
|
||||
let branchName = message
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||
|
||||
// Limit length to 50 characters
|
||||
if (branchName.length > 50) {
|
||||
branchName = branchName.substring(0, 50).replace(/-$/, '');
|
||||
}
|
||||
|
||||
// Add timestamp suffix to ensure uniqueness
|
||||
const timestamp = Date.now().toString(36).substring(-6);
|
||||
branchName = `${branchName}-${timestamp}`;
|
||||
|
||||
return branchName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a Git branch name
|
||||
* @param {string} branchName - Branch name to validate
|
||||
* @returns {{valid: boolean, error?: string}} - Validation result
|
||||
*/
|
||||
function validateBranchName(branchName) {
|
||||
if (!branchName || branchName.trim() === '') {
|
||||
return { valid: false, error: 'Branch name cannot be empty' };
|
||||
}
|
||||
|
||||
// Git branch name rules
|
||||
const invalidPatterns = [
|
||||
{ pattern: /^\./, message: 'Branch name cannot start with a dot' },
|
||||
{ pattern: /\.$/, message: 'Branch name cannot end with a dot' },
|
||||
{ pattern: /\.\./, message: 'Branch name cannot contain consecutive dots (..)' },
|
||||
{ pattern: /\s/, message: 'Branch name cannot contain spaces' },
|
||||
{ pattern: /[~^:?*\[\\]/, message: 'Branch name cannot contain special characters: ~ ^ : ? * [ \\' },
|
||||
{ pattern: /@{/, message: 'Branch name cannot contain @{' },
|
||||
{ pattern: /\/$/, message: 'Branch name cannot end with a slash' },
|
||||
{ pattern: /^\//, message: 'Branch name cannot start with a slash' },
|
||||
{ pattern: /\/\//, message: 'Branch name cannot contain consecutive slashes' },
|
||||
{ pattern: /\.lock$/, message: 'Branch name cannot end with .lock' }
|
||||
];
|
||||
|
||||
for (const { pattern, message } of invalidPatterns) {
|
||||
if (pattern.test(branchName)) {
|
||||
return { valid: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ASCII control characters
|
||||
if (/[\x00-\x1F\x7F]/.test(branchName)) {
|
||||
return { valid: false, error: 'Branch name cannot contain control characters' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent commit messages from a repository
|
||||
* @param {string} projectPath - Path to the git repository
|
||||
* @param {number} limit - Number of commits to retrieve (default: 5)
|
||||
* @returns {Promise<string[]>} - Array of commit messages
|
||||
*/
|
||||
async function getCommitMessages(projectPath, limit = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const gitProcess = spawn('git', ['log', `-${limit}`, '--pretty=format:%s'], {
|
||||
cwd: projectPath,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
gitProcess.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
gitProcess.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
gitProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
const messages = stdout.trim().split('\n').filter(msg => msg.length > 0);
|
||||
resolve(messages);
|
||||
} else {
|
||||
reject(new Error(`Failed to get commit messages: ${stderr}`));
|
||||
}
|
||||
});
|
||||
|
||||
gitProcess.on('error', (error) => {
|
||||
reject(new Error(`Failed to execute git: ${error.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new branch on GitHub using the API
|
||||
* @param {Octokit} octokit - Octokit instance
|
||||
* @param {string} owner - Repository owner
|
||||
* @param {string} repo - Repository name
|
||||
* @param {string} branchName - Name of the new branch
|
||||
* @param {string} baseBranch - Base branch to branch from (default: 'main')
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function createGitHubBranch(octokit, owner, repo, branchName, baseBranch = 'main') {
|
||||
try {
|
||||
// Get the SHA of the base branch
|
||||
const { data: ref } = await octokit.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `heads/${baseBranch}`
|
||||
});
|
||||
|
||||
const baseSha = ref.object.sha;
|
||||
|
||||
// Create the new branch
|
||||
await octokit.git.createRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `refs/heads/${branchName}`,
|
||||
sha: baseSha
|
||||
});
|
||||
|
||||
console.log(`✅ Created branch '${branchName}' on GitHub`);
|
||||
} catch (error) {
|
||||
if (error.status === 422 && error.message.includes('Reference already exists')) {
|
||||
console.log(`ℹ️ Branch '${branchName}' already exists on GitHub`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pull request on GitHub
|
||||
* @param {Octokit} octokit - Octokit instance
|
||||
* @param {string} owner - Repository owner
|
||||
* @param {string} repo - Repository name
|
||||
* @param {string} branchName - Head branch name
|
||||
* @param {string} title - PR title
|
||||
* @param {string} body - PR body/description
|
||||
* @param {string} baseBranch - Base branch (default: 'main')
|
||||
* @returns {Promise<{number: number, url: string}>} - PR number and URL
|
||||
*/
|
||||
async function createGitHubPR(octokit, owner, repo, branchName, title, body, baseBranch = 'main') {
|
||||
const { data: pr } = await octokit.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
head: branchName,
|
||||
base: baseBranch,
|
||||
body
|
||||
});
|
||||
|
||||
console.log(`✅ Created pull request #${pr.number}: ${pr.html_url}`);
|
||||
|
||||
return {
|
||||
number: pr.number,
|
||||
url: pr.html_url
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a GitHub repository to a directory
|
||||
* @param {string} githubUrl - GitHub repository URL
|
||||
@@ -361,27 +550,243 @@ class ResponseCollector {
|
||||
/**
|
||||
* POST /api/agent
|
||||
*
|
||||
* Trigger an AI agent (Claude or Cursor) to work on a project
|
||||
* Trigger an AI agent (Claude or Cursor) to work on a project.
|
||||
* Supports automatic GitHub branch and pull request creation after successful completion.
|
||||
*
|
||||
* Body:
|
||||
* - githubUrl: string (conditionally required) - GitHub repository URL to clone
|
||||
* - projectPath: string (conditionally required) - Path to existing project or where to clone
|
||||
* - message: string (required) - Message to send to the AI agent
|
||||
* - provider: string (optional) - 'claude' or 'cursor' (default: 'claude')
|
||||
* - stream: boolean (optional) - Whether to stream responses (default: true)
|
||||
* - model: string (optional) - Model to use (for Cursor)
|
||||
* - cleanup: boolean (optional) - Whether to cleanup project after completion (default: true)
|
||||
* - githubToken: string (optional) - GitHub token for private repos (overrides stored token)
|
||||
* ================================================================================================
|
||||
* REQUEST BODY PARAMETERS
|
||||
* ================================================================================================
|
||||
*
|
||||
* Note: Either githubUrl OR projectPath must be provided. If both are provided, githubUrl will be cloned to projectPath.
|
||||
* @param {string} githubUrl - (Conditionally Required) GitHub repository URL to clone.
|
||||
* Supported formats:
|
||||
* - HTTPS: https://github.com/owner/repo
|
||||
* - HTTPS with .git: https://github.com/owner/repo.git
|
||||
* - SSH: git@github.com:owner/repo
|
||||
* - SSH with .git: git@github.com:owner/repo.git
|
||||
*
|
||||
* @param {string} projectPath - (Conditionally Required) Path to existing project OR destination for cloning.
|
||||
* Behavior depends on usage:
|
||||
* - If used alone: Must point to existing project directory
|
||||
* - If used with githubUrl: Target location for cloning
|
||||
* - If omitted with githubUrl: Auto-generates temporary path in ~/.claude/external-projects/
|
||||
*
|
||||
* @param {string} message - (Required) Task description for the AI agent. Used as:
|
||||
* - Instructions for the agent
|
||||
* - Source for auto-generated branch names (if createBranch=true and no branchName)
|
||||
* - Fallback for PR title if no commits are made
|
||||
*
|
||||
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor'
|
||||
* Default: 'claude'
|
||||
*
|
||||
* @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
|
||||
* Default: true
|
||||
* - true: Returns text/event-stream with incremental updates
|
||||
* - false: Returns complete JSON response after completion
|
||||
*
|
||||
* @param {string} model - (Optional) Model identifier for Cursor provider.
|
||||
* Only applicable when provider='cursor'.
|
||||
* Examples: 'gpt-4', 'claude-3-opus', etc.
|
||||
*
|
||||
* @param {boolean} cleanup - (Optional) Auto-cleanup project directory after completion.
|
||||
* Default: true
|
||||
* Behavior:
|
||||
* - Only applies when cloning via githubUrl (not for existing projectPath)
|
||||
* - Deletes cloned repository after 5 seconds
|
||||
* - Also deletes associated Claude session directory
|
||||
* - Remote branch and PR remain on GitHub if created
|
||||
*
|
||||
* @param {string} githubToken - (Optional) GitHub Personal Access Token for authentication.
|
||||
* Overrides stored token from user settings.
|
||||
* Required for:
|
||||
* - Private repositories
|
||||
* - Branch/PR creation features
|
||||
* Token must have 'repo' scope for full functionality.
|
||||
*
|
||||
* @param {string} branchName - (Optional) Custom name for the Git branch.
|
||||
* If provided, createBranch is automatically set to true.
|
||||
* Validation rules (errors returned if violated):
|
||||
* - Cannot be empty or whitespace only
|
||||
* - Cannot start or end with dot (.)
|
||||
* - Cannot contain consecutive dots (..)
|
||||
* - Cannot contain spaces
|
||||
* - Cannot contain special characters: ~ ^ : ? * [ \
|
||||
* - Cannot contain @{
|
||||
* - Cannot start or end with forward slash (/)
|
||||
* - Cannot contain consecutive slashes (//)
|
||||
* - Cannot end with .lock
|
||||
* - Cannot contain ASCII control characters
|
||||
* Examples: 'feature/user-auth', 'bugfix/login-error', 'refactor/db-optimization'
|
||||
*
|
||||
* @param {boolean} createBranch - (Optional) Create a new Git branch after successful agent completion.
|
||||
* Default: false (or true if branchName is provided)
|
||||
* Behavior:
|
||||
* - Creates branch locally and pushes to remote
|
||||
* - If branch exists locally: Checks out existing branch (no error)
|
||||
* - If branch exists on remote: Uses existing branch (no error)
|
||||
* - Branch name: Custom (if branchName provided) or auto-generated from message
|
||||
* - Requires either githubUrl OR projectPath with GitHub remote
|
||||
*
|
||||
* @param {boolean} createPR - (Optional) Create a GitHub Pull Request after successful completion.
|
||||
* Default: false
|
||||
* Behavior:
|
||||
* - PR title: First commit message (or fallback to message parameter)
|
||||
* - PR description: Auto-generated from all commit messages
|
||||
* - Base branch: Always 'main' (currently hardcoded)
|
||||
* - If PR already exists: GitHub returns error with details
|
||||
* - Requires either githubUrl OR projectPath with GitHub remote
|
||||
*
|
||||
* ================================================================================================
|
||||
* PATH HANDLING BEHAVIOR
|
||||
* ================================================================================================
|
||||
*
|
||||
* Scenario 1: Only githubUrl provided
|
||||
* Input: { githubUrl: "https://github.com/owner/repo" }
|
||||
* Action: Clones to auto-generated temporary path: ~/.claude/external-projects/<hash>/
|
||||
* Cleanup: Yes (if cleanup=true)
|
||||
*
|
||||
* Scenario 2: Only projectPath provided
|
||||
* Input: { projectPath: "/home/user/my-project" }
|
||||
* Action: Uses existing project at specified path
|
||||
* Validation: Path must exist and be accessible
|
||||
* Cleanup: No (never cleanup existing projects)
|
||||
*
|
||||
* Scenario 3: Both githubUrl and projectPath provided
|
||||
* Input: { githubUrl: "https://github.com/owner/repo", projectPath: "/custom/path" }
|
||||
* Action: Clones githubUrl to projectPath location
|
||||
* Validation:
|
||||
* - If projectPath exists with git repo:
|
||||
* - Compares remote URL with githubUrl
|
||||
* - If URLs match: Reuses existing repo
|
||||
* - If URLs differ: Returns error
|
||||
* Cleanup: Yes (if cleanup=true)
|
||||
*
|
||||
* ================================================================================================
|
||||
* GITHUB BRANCH/PR CREATION REQUIREMENTS
|
||||
* ================================================================================================
|
||||
*
|
||||
* For createBranch or createPR to work, one of the following must be true:
|
||||
*
|
||||
* Option A: githubUrl provided
|
||||
* - Repository URL directly specified
|
||||
* - Works with both cloning and existing paths
|
||||
*
|
||||
* Option B: projectPath with GitHub remote
|
||||
* - Project must be a Git repository
|
||||
* - Must have 'origin' remote configured
|
||||
* - Remote URL must point to github.com
|
||||
* - System auto-detects GitHub URL via: git remote get-url origin
|
||||
*
|
||||
* Additional Requirements:
|
||||
* - Valid GitHub token (from settings or githubToken parameter)
|
||||
* - Token must have 'repo' scope for private repos
|
||||
* - Project must have commits (for PR creation)
|
||||
*
|
||||
* ================================================================================================
|
||||
* VALIDATION & ERROR HANDLING
|
||||
* ================================================================================================
|
||||
*
|
||||
* Input Validations (400 Bad Request):
|
||||
* - Either githubUrl OR projectPath must be provided (not neither)
|
||||
* - message must be non-empty string
|
||||
* - provider must be 'claude' or 'cursor'
|
||||
* - createBranch/createPR requires githubUrl OR projectPath (not neither)
|
||||
* - branchName must pass Git naming rules (if provided)
|
||||
*
|
||||
* Runtime Validations (500 Internal Server Error or specific error in response):
|
||||
* - projectPath must exist (if used alone)
|
||||
* - GitHub URL format must be valid
|
||||
* - Git remote URL must include github.com (for projectPath + branch/PR)
|
||||
* - GitHub token must be available (for private repos and branch/PR)
|
||||
* - Directory conflicts handled (existing path with different repo)
|
||||
*
|
||||
* Branch Name Validation Errors (returned in response, not HTTP error):
|
||||
* Invalid names return: { branch: { error: "Invalid branch name: <reason>" } }
|
||||
* Examples:
|
||||
* - "my branch" → "Branch name cannot contain spaces"
|
||||
* - ".feature" → "Branch name cannot start with a dot"
|
||||
* - "feature.lock" → "Branch name cannot end with .lock"
|
||||
*
|
||||
* ================================================================================================
|
||||
* RESPONSE FORMATS
|
||||
* ================================================================================================
|
||||
*
|
||||
* Streaming Response (stream=true):
|
||||
* Content-Type: text/event-stream
|
||||
* Events:
|
||||
* - { type: "status", message: "...", projectPath: "..." }
|
||||
* - { type: "claude-response", data: {...} }
|
||||
* - { type: "github-branch", branch: { name: "...", url: "..." } }
|
||||
* - { type: "github-pr", pullRequest: { number: 42, url: "..." } }
|
||||
* - { type: "github-error", error: "..." }
|
||||
* - { type: "done" }
|
||||
*
|
||||
* Non-Streaming Response (stream=false):
|
||||
* Content-Type: application/json
|
||||
* {
|
||||
* success: true,
|
||||
* sessionId: "session-123",
|
||||
* messages: [...], // Assistant messages only (filtered)
|
||||
* tokens: {
|
||||
* inputTokens: 150,
|
||||
* outputTokens: 50,
|
||||
* cacheReadTokens: 0,
|
||||
* cacheCreationTokens: 0,
|
||||
* totalTokens: 200
|
||||
* },
|
||||
* projectPath: "/path/to/project",
|
||||
* branch: { // Only if createBranch=true
|
||||
* name: "feature/xyz",
|
||||
* url: "https://github.com/owner/repo/tree/feature/xyz"
|
||||
* } | { error: "..." },
|
||||
* pullRequest: { // Only if createPR=true
|
||||
* number: 42,
|
||||
* url: "https://github.com/owner/repo/pull/42"
|
||||
* } | { error: "..." }
|
||||
* }
|
||||
*
|
||||
* Error Response:
|
||||
* HTTP Status: 400, 401, 500
|
||||
* Content-Type: application/json
|
||||
* { success: false, error: "Error description" }
|
||||
*
|
||||
* ================================================================================================
|
||||
* EXAMPLES
|
||||
* ================================================================================================
|
||||
*
|
||||
* Example 1: Clone and process with auto-cleanup
|
||||
* POST /api/agent
|
||||
* { "githubUrl": "https://github.com/user/repo", "message": "Fix bug" }
|
||||
*
|
||||
* Example 2: Use existing project with custom branch and PR
|
||||
* POST /api/agent
|
||||
* {
|
||||
* "projectPath": "/home/user/project",
|
||||
* "message": "Add feature",
|
||||
* "branchName": "feature/new-feature",
|
||||
* "createPR": true
|
||||
* }
|
||||
*
|
||||
* Example 3: Clone to specific path with auto-generated branch
|
||||
* POST /api/agent
|
||||
* {
|
||||
* "githubUrl": "https://github.com/user/repo",
|
||||
* "projectPath": "/tmp/work",
|
||||
* "message": "Refactor code",
|
||||
* "createBranch": true,
|
||||
* "cleanup": false
|
||||
* }
|
||||
*/
|
||||
router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken } = req.body;
|
||||
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken, branchName } = req.body;
|
||||
|
||||
// Parse stream and cleanup as booleans (handle string "true"/"false" from curl)
|
||||
const stream = req.body.stream === undefined ? true : (req.body.stream === true || req.body.stream === 'true');
|
||||
const cleanup = req.body.cleanup === undefined ? true : (req.body.cleanup === true || req.body.cleanup === 'true');
|
||||
|
||||
// If branchName is provided, automatically enable createBranch
|
||||
const createBranch = branchName ? true : (req.body.createBranch === true || req.body.createBranch === 'true');
|
||||
const createPR = req.body.createPR === true || req.body.createPR === 'true';
|
||||
|
||||
// Validate inputs
|
||||
if (!githubUrl && !projectPath) {
|
||||
return res.status(400).json({ error: 'Either githubUrl or projectPath is required' });
|
||||
@@ -395,6 +800,12 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
return res.status(400).json({ error: 'provider must be "claude" or "cursor"' });
|
||||
}
|
||||
|
||||
// Validate GitHub branch/PR creation requirements
|
||||
// Allow branch/PR creation with projectPath as long as it has a GitHub remote
|
||||
if ((createBranch || createPR) && !githubUrl && !projectPath) {
|
||||
return res.status(400).json({ error: 'createBranch and createPR require either githubUrl or projectPath with a GitHub remote' });
|
||||
}
|
||||
|
||||
let finalProjectPath = null;
|
||||
let writer = null;
|
||||
|
||||
@@ -492,6 +903,187 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
}, writer);
|
||||
}
|
||||
|
||||
// Handle GitHub branch and PR creation after successful agent completion
|
||||
let branchInfo = null;
|
||||
let prInfo = null;
|
||||
|
||||
if (createBranch || createPR) {
|
||||
try {
|
||||
console.log('🔄 Starting GitHub branch/PR creation workflow...');
|
||||
|
||||
// Get GitHub token
|
||||
const tokenToUse = githubToken || githubTokensDb.getActiveGithubToken(req.user.id);
|
||||
|
||||
if (!tokenToUse) {
|
||||
throw new Error('GitHub token required for branch/PR creation. Please configure a GitHub token in settings.');
|
||||
}
|
||||
|
||||
// Initialize Octokit
|
||||
const octokit = new Octokit({ auth: tokenToUse });
|
||||
|
||||
// Get GitHub URL - either from parameter or from git remote
|
||||
let repoUrl = githubUrl;
|
||||
if (!repoUrl) {
|
||||
console.log('🔍 Getting GitHub URL from git remote...');
|
||||
try {
|
||||
repoUrl = await getGitRemoteUrl(finalProjectPath);
|
||||
if (!repoUrl.includes('github.com')) {
|
||||
throw new Error('Project does not have a GitHub remote configured');
|
||||
}
|
||||
console.log(`✅ Found GitHub remote: ${repoUrl}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get GitHub remote URL: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse GitHub URL to get owner and repo
|
||||
const { owner, repo } = parseGitHubUrl(repoUrl);
|
||||
console.log(`📦 Repository: ${owner}/${repo}`);
|
||||
|
||||
// Use provided branch name or auto-generate from message
|
||||
const finalBranchName = branchName || autogenerateBranchName(message);
|
||||
if (branchName) {
|
||||
console.log(`🌿 Using provided branch name: ${finalBranchName}`);
|
||||
|
||||
// Validate custom branch name
|
||||
const validation = validateBranchName(finalBranchName);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Invalid branch name: ${validation.error}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`🌿 Auto-generated branch name: ${finalBranchName}`);
|
||||
}
|
||||
|
||||
if (createBranch) {
|
||||
// Create and checkout the new branch locally
|
||||
console.log('🔄 Creating local branch...');
|
||||
const checkoutProcess = spawn('git', ['checkout', '-b', finalBranchName], {
|
||||
cwd: finalProjectPath,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let stderr = '';
|
||||
checkoutProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
checkoutProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log(`✅ Created and checked out local branch '${finalBranchName}'`);
|
||||
resolve();
|
||||
} else {
|
||||
// Branch might already exist locally, try to checkout
|
||||
if (stderr.includes('already exists')) {
|
||||
console.log(`ℹ️ Branch '${finalBranchName}' already exists locally, checking out...`);
|
||||
const checkoutExisting = spawn('git', ['checkout', finalBranchName], {
|
||||
cwd: finalProjectPath,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
checkoutExisting.on('close', (checkoutCode) => {
|
||||
if (checkoutCode === 0) {
|
||||
console.log(`✅ Checked out existing branch '${finalBranchName}'`);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Failed to checkout existing branch: ${stderr}`));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Failed to create branch: ${stderr}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Push the branch to remote
|
||||
console.log('🔄 Pushing branch to remote...');
|
||||
const pushProcess = spawn('git', ['push', '-u', 'origin', finalBranchName], {
|
||||
cwd: finalProjectPath,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let stderr = '';
|
||||
let stdout = '';
|
||||
pushProcess.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||
pushProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
pushProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log(`✅ Pushed branch '${finalBranchName}' to remote`);
|
||||
resolve();
|
||||
} else {
|
||||
// Check if branch exists on remote but has different commits
|
||||
if (stderr.includes('already exists') || stderr.includes('up-to-date')) {
|
||||
console.log(`ℹ️ Branch '${finalBranchName}' already exists on remote, using existing branch`);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Failed to push branch: ${stderr}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
branchInfo = {
|
||||
name: finalBranchName,
|
||||
url: `https://github.com/${owner}/${repo}/tree/${finalBranchName}`
|
||||
};
|
||||
}
|
||||
|
||||
if (createPR) {
|
||||
// Get commit messages to generate PR description
|
||||
console.log('🔄 Generating PR title and description...');
|
||||
const commitMessages = await getCommitMessages(finalProjectPath, 5);
|
||||
|
||||
// Use the first commit message as the PR title, or fallback to the agent message
|
||||
const prTitle = commitMessages.length > 0 ? commitMessages[0] : message;
|
||||
|
||||
// Generate PR body from commit messages
|
||||
let prBody = '## Changes\n\n';
|
||||
if (commitMessages.length > 0) {
|
||||
prBody += commitMessages.map(msg => `- ${msg}`).join('\n');
|
||||
} else {
|
||||
prBody += `Agent task: ${message}`;
|
||||
}
|
||||
prBody += '\n\n---\n*This pull request was automatically created by Claude Code UI Agent.*';
|
||||
|
||||
console.log(`📝 PR Title: ${prTitle}`);
|
||||
|
||||
// Create the pull request
|
||||
console.log('🔄 Creating pull request...');
|
||||
prInfo = await createGitHubPR(octokit, owner, repo, finalBranchName, prTitle, prBody, 'main');
|
||||
}
|
||||
|
||||
// Send branch/PR info in response
|
||||
if (stream) {
|
||||
if (branchInfo) {
|
||||
writer.send({
|
||||
type: 'github-branch',
|
||||
branch: branchInfo
|
||||
});
|
||||
}
|
||||
if (prInfo) {
|
||||
writer.send({
|
||||
type: 'github-pr',
|
||||
pullRequest: prInfo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ GitHub branch/PR creation error:', error);
|
||||
|
||||
// Send error but don't fail the entire request
|
||||
if (stream) {
|
||||
writer.send({
|
||||
type: 'github-error',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
// Store error info for non-streaming response
|
||||
if (!stream) {
|
||||
branchInfo = { error: error.message };
|
||||
prInfo = { error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle response based on streaming mode
|
||||
if (stream) {
|
||||
// Streaming mode: end the SSE stream
|
||||
@@ -501,13 +1093,23 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
const assistantMessages = writer.getAssistantMessages();
|
||||
const tokenSummary = writer.getTotalTokens();
|
||||
|
||||
res.json({
|
||||
const response = {
|
||||
success: true,
|
||||
sessionId: writer.getSessionId(),
|
||||
messages: assistantMessages,
|
||||
tokens: tokenSummary,
|
||||
projectPath: finalProjectPath
|
||||
});
|
||||
};
|
||||
|
||||
// Add branch/PR info if created
|
||||
if (branchInfo) {
|
||||
response.branch = branchInfo;
|
||||
}
|
||||
if (prInfo) {
|
||||
response.pullRequest = prInfo;
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
}
|
||||
|
||||
// Clean up if requested
|
||||
|
||||
Reference in New Issue
Block a user