claude-issue-solver 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # Claude Issue Solver
2
+
3
+ Automatically solve GitHub issues using [Claude Code](https://claude.ai/code).
4
+
5
+ This CLI tool fetches an issue from your repo, creates a worktree, opens Claude Code in a new terminal to solve it, and creates a PR when done.
6
+
7
+ ## Demo
8
+
9
+ ```bash
10
+ $ claude-issue
11
+
12
+ Open issues for my-project:
13
+
14
+ ? Select an issue to solve:
15
+ ❯ #42 Add dark mode support
16
+ #38 Fix login bug on mobile
17
+ #35 Update dependencies
18
+ Cancel
19
+
20
+ 📋 Fetching issue #42...
21
+ ✔ Found issue #42
22
+ 📌 Issue: Add dark mode support
23
+ 🌿 Creating worktree with branch: issue-42-add-dark-mode-support
24
+ 🤖 Opening new terminal to run Claude Code...
25
+
26
+ ✅ Worktree created at: ../my-project-issue-42-add-dark-mode-support
27
+ Claude is running in a new terminal window.
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - 🎯 **Interactive issue selection** - Lists open issues with arrow-key navigation
33
+ - 🌿 **Worktree isolation** - Each issue gets its own worktree, work on multiple issues in parallel
34
+ - 🤖 **Automatic PR creation** - Creates a PR that closes the issue when merged
35
+ - 📁 **Works with any repo** - Auto-detects project name from git remote
36
+ - 💻 **Opens in new terminal** - Keeps your current terminal free (supports iTerm2 and Terminal.app on macOS)
37
+
38
+ ## Requirements
39
+
40
+ - [Node.js](https://nodejs.org/) >= 18
41
+ - [Claude Code CLI](https://claude.ai/code) - `npm install -g @anthropic-ai/claude-code`
42
+ - [GitHub CLI](https://cli.github.com/) - `brew install gh` (and run `gh auth login`)
43
+ - Git
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ npm install -g claude-issue-solver
49
+ ```
50
+
51
+ Or install from source:
52
+
53
+ ```bash
54
+ git clone https://github.com/MikeOuroumis/claude-issue-solver.git
55
+ cd claude-issue-solver
56
+ npm install
57
+ npm run build
58
+ npm link
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ Run from any git repository with GitHub issues:
64
+
65
+ ```bash
66
+ # Interactive: show issues and select one
67
+ claude-issue
68
+
69
+ # Solve a specific issue directly
70
+ claude-issue 42
71
+
72
+ # List open issues
73
+ claude-issue list
74
+
75
+ # Create PR for a solved issue (if you skipped it earlier)
76
+ claude-issue pr 42
77
+
78
+ # Clean up worktree and branch after PR is merged
79
+ claude-issue clean 42
80
+
81
+ # Show help
82
+ claude-issue --help
83
+ ```
84
+
85
+ ## How it works
86
+
87
+ 1. **Fetches issue** - Gets title and description from GitHub
88
+ 2. **Creates worktree** - Makes a new git worktree with branch `issue-{number}-{slug}`
89
+ 3. **Sets up environment** - Copies `.env` files, symlinks `node_modules`
90
+ 4. **Opens Claude** - Launches Claude Code in a new terminal with the issue as context
91
+ 5. **Interactive session** - Claude stays open so you can ask for changes
92
+ 6. **Creates PR** - When you exit, prompts to create a PR that closes the issue
93
+
94
+ ## Workflow
95
+
96
+ ```
97
+ ┌─────────────────┐
98
+ │ claude-issue │
99
+ │ (select 42) │
100
+ └────────┬────────┘
101
+
102
+
103
+ ┌─────────────────┐
104
+ │ Create worktree │
105
+ │ ../project-issue│
106
+ │ -42-fix-bug │
107
+ └────────┬────────┘
108
+
109
+
110
+ ┌─────────────────┐
111
+ │ Open new term │
112
+ │ with Claude │
113
+ └────────┬────────┘
114
+
115
+
116
+ ┌─────────────────┐
117
+ │ Claude solves │
118
+ │ issue & commits │
119
+ └────────┬────────┘
120
+
121
+
122
+ ┌─────────────────┐
123
+ │ Create PR │
124
+ │ "Closes #42" │
125
+ └────────┬────────┘
126
+
127
+
128
+ ┌─────────────────┐
129
+ │ claude-issue │
130
+ │ clean 42 │
131
+ └─────────────────┘
132
+ ```
133
+
134
+ ## Tips
135
+
136
+ - Use `/exit` in Claude to end the session and trigger PR creation
137
+ - Worktrees share the same `.git` so commits are visible in main repo
138
+ - Run `claude-issue clean <number>` after merging to clean up
139
+ - You can work on multiple issues in parallel - each gets its own worktree
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1 @@
1
+ export declare function cleanCommand(issueNumber: number): Promise<void>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.cleanCommand = cleanCommand;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const ora_1 = __importDefault(require("ora"));
42
+ const inquirer_1 = __importDefault(require("inquirer"));
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const child_process_1 = require("child_process");
46
+ const github_1 = require("../utils/github");
47
+ const git_1 = require("../utils/git");
48
+ const helpers_1 = require("../utils/helpers");
49
+ async function cleanCommand(issueNumber) {
50
+ const spinner = (0, ora_1.default)(`Fetching issue #${issueNumber}...`).start();
51
+ const issue = (0, github_1.getIssue)(issueNumber);
52
+ if (!issue) {
53
+ spinner.fail(`Could not find issue #${issueNumber}`);
54
+ process.exit(1);
55
+ }
56
+ spinner.succeed(`Found issue #${issueNumber}`);
57
+ const projectRoot = (0, git_1.getProjectRoot)();
58
+ const projectName = (0, git_1.getProjectName)();
59
+ const branchSlug = (0, helpers_1.slugify)(issue.title);
60
+ const branchName = `issue-${issueNumber}-${branchSlug}`;
61
+ const worktreePath = path.join(path.dirname(projectRoot), `${projectName}-${branchName}`);
62
+ console.log();
63
+ console.log(chalk_1.default.bold(`🧹 Cleaning up issue #${issueNumber}`));
64
+ console.log(chalk_1.default.dim(` Branch: ${branchName}`));
65
+ console.log(chalk_1.default.dim(` Worktree: ${worktreePath}`));
66
+ console.log();
67
+ const { confirm } = await inquirer_1.default.prompt([
68
+ {
69
+ type: 'confirm',
70
+ name: 'confirm',
71
+ message: 'Remove worktree and delete branch?',
72
+ default: false,
73
+ },
74
+ ]);
75
+ if (!confirm) {
76
+ console.log(chalk_1.default.dim('Cancelled.'));
77
+ return;
78
+ }
79
+ // Remove worktree
80
+ if (fs.existsSync(worktreePath)) {
81
+ const worktreeSpinner = (0, ora_1.default)('Removing worktree...').start();
82
+ try {
83
+ (0, child_process_1.execSync)(`git worktree remove "${worktreePath}" --force`, {
84
+ cwd: projectRoot,
85
+ stdio: 'pipe',
86
+ });
87
+ worktreeSpinner.succeed('Worktree removed');
88
+ }
89
+ catch {
90
+ worktreeSpinner.warn('Could not remove worktree (may already be removed)');
91
+ }
92
+ }
93
+ // Delete branch
94
+ const branchSpinner = (0, ora_1.default)('Deleting branch...').start();
95
+ try {
96
+ (0, child_process_1.execSync)(`git branch -D "${branchName}"`, {
97
+ cwd: projectRoot,
98
+ stdio: 'pipe',
99
+ });
100
+ branchSpinner.succeed('Branch deleted');
101
+ }
102
+ catch {
103
+ branchSpinner.warn('Could not delete branch (may already be deleted)');
104
+ }
105
+ console.log();
106
+ console.log(chalk_1.default.green('✅ Cleanup complete!'));
107
+ }
@@ -0,0 +1 @@
1
+ export declare function listCommand(): Promise<void>;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listCommand = listCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const github_1 = require("../utils/github");
9
+ const git_1 = require("../utils/git");
10
+ async function listCommand() {
11
+ const projectName = (0, git_1.getProjectName)();
12
+ console.log(chalk_1.default.bold(`\nOpen issues for ${projectName}:\n`));
13
+ const issues = (0, github_1.listIssues)();
14
+ if (issues.length === 0) {
15
+ console.log(chalk_1.default.yellow('No open issues found.'));
16
+ return;
17
+ }
18
+ for (const issue of issues) {
19
+ console.log(` ${chalk_1.default.cyan(`#${issue.number}`)}\t${issue.title}`);
20
+ }
21
+ console.log();
22
+ }
@@ -0,0 +1 @@
1
+ export declare function prCommand(issueNumber: number): Promise<void>;
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.prCommand = prCommand;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const ora_1 = __importDefault(require("ora"));
42
+ const inquirer_1 = __importDefault(require("inquirer"));
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const child_process_1 = require("child_process");
46
+ const github_1 = require("../utils/github");
47
+ const git_1 = require("../utils/git");
48
+ const helpers_1 = require("../utils/helpers");
49
+ async function prCommand(issueNumber) {
50
+ const spinner = (0, ora_1.default)(`Fetching issue #${issueNumber}...`).start();
51
+ const issue = (0, github_1.getIssue)(issueNumber);
52
+ if (!issue) {
53
+ spinner.fail(`Could not find issue #${issueNumber}`);
54
+ process.exit(1);
55
+ }
56
+ spinner.succeed(`Found issue #${issueNumber}`);
57
+ const projectRoot = (0, git_1.getProjectRoot)();
58
+ const projectName = (0, git_1.getProjectName)();
59
+ const branchSlug = (0, helpers_1.slugify)(issue.title);
60
+ const branchName = `issue-${issueNumber}-${branchSlug}`;
61
+ const worktreePath = path.join(path.dirname(projectRoot), `${projectName}-${branchName}`);
62
+ if (!fs.existsSync(worktreePath)) {
63
+ console.log(chalk_1.default.red(`\n❌ Worktree not found at ${worktreePath}`));
64
+ console.log(chalk_1.default.dim(` Make sure you've run: claude-issue ${issueNumber}`));
65
+ process.exit(1);
66
+ }
67
+ const commits = (0, git_1.getCommitCount)(worktreePath);
68
+ if (commits === 0) {
69
+ console.log(chalk_1.default.yellow(`\n⚠️ No commits found on branch ${branchName}`));
70
+ console.log(chalk_1.default.dim(` Make sure Claude has committed changes before creating a PR.`));
71
+ process.exit(1);
72
+ }
73
+ console.log();
74
+ console.log(chalk_1.default.bold(`📋 Issue #${issueNumber}: ${issue.title}`));
75
+ console.log(chalk_1.default.dim(`🌿 Branch: ${branchName}`));
76
+ console.log(chalk_1.default.dim(`📝 Commits: ${commits}`));
77
+ console.log();
78
+ const { confirm } = await inquirer_1.default.prompt([
79
+ {
80
+ type: 'confirm',
81
+ name: 'confirm',
82
+ message: `Create PR to close issue #${issueNumber}?`,
83
+ default: true,
84
+ },
85
+ ]);
86
+ if (!confirm) {
87
+ console.log(chalk_1.default.dim('Cancelled.'));
88
+ return;
89
+ }
90
+ const pushSpinner = (0, ora_1.default)('Pushing branch and creating PR...').start();
91
+ try {
92
+ (0, child_process_1.execSync)(`git push -u origin "${branchName}"`, {
93
+ cwd: worktreePath,
94
+ stdio: 'pipe',
95
+ });
96
+ const commitList = (0, git_1.getCommitList)(worktreePath);
97
+ const prBody = `## Summary
98
+
99
+ Closes #${issueNumber}
100
+
101
+ ## Changes
102
+
103
+ ${commitList}
104
+
105
+ ---
106
+
107
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)`;
108
+ const prUrl = (0, child_process_1.execSync)(`gh pr create --title "Fix #${issueNumber}: ${issue.title.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}" --head "${branchName}" --base main`, { cwd: worktreePath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
109
+ pushSpinner.succeed('PR created!');
110
+ console.log();
111
+ console.log(chalk_1.default.green(`✅ PR created: ${prUrl}`));
112
+ console.log();
113
+ console.log(chalk_1.default.dim(`The PR will automatically close issue #${issueNumber} when merged.`));
114
+ console.log(chalk_1.default.dim(`To clean up after merge: claude-issue clean ${issueNumber}`));
115
+ }
116
+ catch (error) {
117
+ pushSpinner.fail('Failed to create PR');
118
+ console.error(error);
119
+ process.exit(1);
120
+ }
121
+ }
@@ -0,0 +1 @@
1
+ export declare function selectCommand(): Promise<void>;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.selectCommand = selectCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const inquirer_1 = __importDefault(require("inquirer"));
9
+ const github_1 = require("../utils/github");
10
+ const git_1 = require("../utils/git");
11
+ const solve_1 = require("./solve");
12
+ async function selectCommand() {
13
+ const projectName = (0, git_1.getProjectName)();
14
+ console.log(chalk_1.default.bold(`\nOpen issues for ${projectName}:\n`));
15
+ const issues = (0, github_1.listIssues)();
16
+ if (issues.length === 0) {
17
+ console.log(chalk_1.default.yellow('No open issues found.'));
18
+ return;
19
+ }
20
+ const choices = issues.map((issue) => ({
21
+ name: `#${issue.number}\t${issue.title}`,
22
+ value: issue.number,
23
+ }));
24
+ choices.push({
25
+ name: chalk_1.default.dim('Cancel'),
26
+ value: -1,
27
+ });
28
+ const { issueNumber } = await inquirer_1.default.prompt([
29
+ {
30
+ type: 'list',
31
+ name: 'issueNumber',
32
+ message: 'Select an issue to solve:',
33
+ choices,
34
+ pageSize: 15,
35
+ },
36
+ ]);
37
+ if (issueNumber === -1) {
38
+ console.log(chalk_1.default.dim('Cancelled.'));
39
+ return;
40
+ }
41
+ await (0, solve_1.solveCommand)(issueNumber);
42
+ }
@@ -0,0 +1 @@
1
+ export declare function solveCommand(issueNumber: number): Promise<void>;
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.solveCommand = solveCommand;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const ora_1 = __importDefault(require("ora"));
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const child_process_1 = require("child_process");
45
+ const github_1 = require("../utils/github");
46
+ const git_1 = require("../utils/git");
47
+ const helpers_1 = require("../utils/helpers");
48
+ async function solveCommand(issueNumber) {
49
+ const spinner = (0, ora_1.default)(`Fetching issue #${issueNumber}...`).start();
50
+ const issue = (0, github_1.getIssue)(issueNumber);
51
+ if (!issue) {
52
+ spinner.fail(`Could not find issue #${issueNumber}`);
53
+ process.exit(1);
54
+ }
55
+ spinner.succeed(`Found issue #${issueNumber}`);
56
+ console.log();
57
+ console.log(chalk_1.default.bold(`📌 Issue: ${issue.title}`));
58
+ console.log(chalk_1.default.dim(`🔗 URL: ${issue.url}`));
59
+ console.log();
60
+ const projectRoot = (0, git_1.getProjectRoot)();
61
+ const projectName = (0, git_1.getProjectName)();
62
+ const branchSlug = (0, helpers_1.slugify)(issue.title);
63
+ const branchName = `issue-${issueNumber}-${branchSlug}`;
64
+ const worktreePath = path.join(path.dirname(projectRoot), `${projectName}-${branchName}`);
65
+ // Fetch latest main
66
+ const fetchSpinner = (0, ora_1.default)('Fetching latest main...').start();
67
+ try {
68
+ (0, child_process_1.execSync)('git fetch origin main --quiet', { cwd: projectRoot, stdio: 'pipe' });
69
+ fetchSpinner.succeed('Fetched latest main');
70
+ }
71
+ catch {
72
+ fetchSpinner.warn('Could not fetch origin/main');
73
+ }
74
+ // Check if worktree already exists
75
+ if (fs.existsSync(worktreePath)) {
76
+ console.log(chalk_1.default.yellow(`\n🌿 Worktree already exists at: ${worktreePath}`));
77
+ console.log(chalk_1.default.dim(` Resuming work on issue #${issueNumber}...`));
78
+ }
79
+ else {
80
+ const worktreeSpinner = (0, ora_1.default)(`Creating worktree with branch: ${branchName}`).start();
81
+ try {
82
+ if ((0, git_1.branchExists)(branchName)) {
83
+ (0, child_process_1.execSync)(`git worktree add "${worktreePath}" "${branchName}"`, {
84
+ cwd: projectRoot,
85
+ stdio: 'pipe',
86
+ });
87
+ }
88
+ else {
89
+ (0, child_process_1.execSync)(`git worktree add "${worktreePath}" -b "${branchName}" origin/main`, {
90
+ cwd: projectRoot,
91
+ stdio: 'pipe',
92
+ });
93
+ }
94
+ worktreeSpinner.succeed(`Created worktree at: ${worktreePath}`);
95
+ }
96
+ catch (error) {
97
+ worktreeSpinner.fail('Failed to create worktree');
98
+ console.error(error);
99
+ process.exit(1);
100
+ }
101
+ // Copy env files and symlink node_modules
102
+ const setupSpinner = (0, ora_1.default)('Setting up worktree...').start();
103
+ (0, helpers_1.copyEnvFiles)(projectRoot, worktreePath);
104
+ (0, helpers_1.symlinkNodeModules)(projectRoot, worktreePath);
105
+ setupSpinner.succeed('Worktree setup complete');
106
+ }
107
+ // Build the prompt for Claude
108
+ const prompt = `Please solve this GitHub issue:
109
+
110
+ ## Issue #${issueNumber}: ${issue.title}
111
+
112
+ ${issue.body}
113
+
114
+ ---
115
+
116
+ Instructions:
117
+ 1. Analyze the issue and understand what needs to be done
118
+ 2. Implement the necessary changes
119
+ 3. Make sure to run tests if applicable
120
+ 4. When done, commit your changes with a descriptive message that references the issue`;
121
+ // Create runner script
122
+ const runnerScript = path.join(worktreePath, '.claude-runner.sh');
123
+ const runnerContent = `#!/bin/bash
124
+ cd "${worktreePath}"
125
+ echo "🤖 Claude Code - Issue #${issueNumber}: ${issue.title}"
126
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
127
+ echo ""
128
+ echo "Claude will stay open after solving the issue."
129
+ echo "You can ask for more changes or type /exit when done."
130
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
131
+ echo ""
132
+
133
+ claude --dangerously-skip-permissions "${prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"
134
+
135
+ echo ""
136
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
137
+ echo "Claude session ended."
138
+ echo ""
139
+
140
+ COMMITS=$(git log origin/main..HEAD --oneline 2>/dev/null | wc -l | tr -d ' ')
141
+
142
+ if [ "$COMMITS" -eq 0 ]; then
143
+ echo "⚠️ No commits were made."
144
+ echo "To clean up: claude-issue clean ${issueNumber}"
145
+ else
146
+ echo "✅ Found $COMMITS commit(s)"
147
+ echo ""
148
+ read -p "Create PR to close issue #${issueNumber}? (Y/n) " -n 1 -r
149
+ echo ""
150
+
151
+ if [[ ! $REPLY =~ ^[Nn]$ ]]; then
152
+ echo ""
153
+ echo "📤 Pushing branch and creating PR..."
154
+
155
+ git push -u origin "${branchName}"
156
+
157
+ COMMIT_LIST=$(git log origin/main..HEAD --pretty=format:'- %s' | head -10)
158
+
159
+ PR_URL=$(gh pr create \\
160
+ --title "Fix #${issueNumber}: ${issue.title.replace(/"/g, '\\"')}" \\
161
+ --body "## Summary
162
+
163
+ Closes #${issueNumber}
164
+
165
+ ## Changes
166
+
167
+ $COMMIT_LIST
168
+
169
+ ---
170
+
171
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)" \\
172
+ --head "${branchName}" \\
173
+ --base main)
174
+
175
+ echo ""
176
+ echo "✅ PR created: $PR_URL"
177
+ echo ""
178
+ echo "The PR will automatically close issue #${issueNumber} when merged."
179
+ fi
180
+
181
+ echo ""
182
+ echo "To clean up after merge: claude-issue clean ${issueNumber}"
183
+ fi
184
+
185
+ echo ""
186
+ rm -f "${runnerScript}"
187
+ `;
188
+ fs.writeFileSync(runnerScript, runnerContent, { mode: 0o755 });
189
+ console.log();
190
+ console.log(chalk_1.default.cyan('🤖 Opening new terminal to run Claude Code...'));
191
+ console.log();
192
+ (0, helpers_1.openInNewTerminal)(`'${runnerScript}'`);
193
+ console.log(chalk_1.default.green(`✅ Worktree created at: ${worktreePath}`));
194
+ console.log(chalk_1.default.dim(` Claude is running in a new terminal window.`));
195
+ console.log();
196
+ console.log(chalk_1.default.dim(` You can also open VS Code: code ${worktreePath}`));
197
+ console.log(chalk_1.default.dim(` To clean up later: claude-issue clean ${issueNumber}`));
198
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const helpers_1 = require("./utils/helpers");
10
+ const git_1 = require("./utils/git");
11
+ const list_1 = require("./commands/list");
12
+ const solve_1 = require("./commands/solve");
13
+ const pr_1 = require("./commands/pr");
14
+ const clean_1 = require("./commands/clean");
15
+ const select_1 = require("./commands/select");
16
+ const program = new commander_1.Command();
17
+ program
18
+ .name('claude-issue')
19
+ .description('Automatically solve GitHub issues using Claude Code')
20
+ .version('1.0.0');
21
+ // Check requirements before any command
22
+ program.hook('preAction', () => {
23
+ if (!(0, git_1.isGitRepo)()) {
24
+ console.log(chalk_1.default.red('❌ Not in a git repository'));
25
+ process.exit(1);
26
+ }
27
+ const { ok, missing } = (0, helpers_1.checkRequirements)();
28
+ if (!ok) {
29
+ console.log(chalk_1.default.red('\n❌ Missing requirements:\n'));
30
+ for (const m of missing) {
31
+ console.log(chalk_1.default.yellow(` • ${m}`));
32
+ }
33
+ console.log();
34
+ process.exit(1);
35
+ }
36
+ });
37
+ // Default command - interactive selection
38
+ program
39
+ .argument('[issue]', 'Issue number to solve')
40
+ .action(async (issue) => {
41
+ if (issue) {
42
+ const issueNumber = parseInt(issue, 10);
43
+ if (isNaN(issueNumber)) {
44
+ console.log(chalk_1.default.red(`❌ Invalid issue number: ${issue}`));
45
+ process.exit(1);
46
+ }
47
+ await (0, solve_1.solveCommand)(issueNumber);
48
+ }
49
+ else {
50
+ await (0, select_1.selectCommand)();
51
+ }
52
+ });
53
+ // List command
54
+ program
55
+ .command('list')
56
+ .alias('ls')
57
+ .description('List open issues')
58
+ .action(list_1.listCommand);
59
+ // PR command
60
+ program
61
+ .command('pr <issue>')
62
+ .description('Create PR for a solved issue')
63
+ .action(async (issue) => {
64
+ const issueNumber = parseInt(issue, 10);
65
+ if (isNaN(issueNumber)) {
66
+ console.log(chalk_1.default.red(`❌ Invalid issue number: ${issue}`));
67
+ process.exit(1);
68
+ }
69
+ await (0, pr_1.prCommand)(issueNumber);
70
+ });
71
+ // Clean command
72
+ program
73
+ .command('clean <issue>')
74
+ .alias('rm')
75
+ .description('Remove worktree and branch for an issue')
76
+ .action(async (issue) => {
77
+ const issueNumber = parseInt(issue, 10);
78
+ if (isNaN(issueNumber)) {
79
+ console.log(chalk_1.default.red(`❌ Invalid issue number: ${issue}`));
80
+ process.exit(1);
81
+ }
82
+ await (0, clean_1.cleanCommand)(issueNumber);
83
+ });
84
+ program.parse();
@@ -0,0 +1,8 @@
1
+ export declare function exec(command: string, cwd?: string): string;
2
+ export declare function execOrFail(command: string, cwd?: string): string;
3
+ export declare function getProjectRoot(): string;
4
+ export declare function getProjectName(): string;
5
+ export declare function isGitRepo(): boolean;
6
+ export declare function branchExists(branchName: string): boolean;
7
+ export declare function getCommitCount(worktreePath: string): number;
8
+ export declare function getCommitList(worktreePath: string, limit?: number): string;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exec = exec;
4
+ exports.execOrFail = execOrFail;
5
+ exports.getProjectRoot = getProjectRoot;
6
+ exports.getProjectName = getProjectName;
7
+ exports.isGitRepo = isGitRepo;
8
+ exports.branchExists = branchExists;
9
+ exports.getCommitCount = getCommitCount;
10
+ exports.getCommitList = getCommitList;
11
+ const child_process_1 = require("child_process");
12
+ function exec(command, cwd) {
13
+ try {
14
+ return (0, child_process_1.execSync)(command, {
15
+ cwd,
16
+ encoding: 'utf-8',
17
+ stdio: ['pipe', 'pipe', 'pipe'],
18
+ }).trim();
19
+ }
20
+ catch (error) {
21
+ return '';
22
+ }
23
+ }
24
+ function execOrFail(command, cwd) {
25
+ return (0, child_process_1.execSync)(command, {
26
+ cwd,
27
+ encoding: 'utf-8',
28
+ stdio: ['pipe', 'pipe', 'pipe'],
29
+ }).trim();
30
+ }
31
+ function getProjectRoot() {
32
+ try {
33
+ return exec('git rev-parse --show-toplevel') || process.cwd();
34
+ }
35
+ catch {
36
+ return process.cwd();
37
+ }
38
+ }
39
+ function getProjectName() {
40
+ // Try to get from git remote
41
+ const remoteUrl = exec('git config --get remote.origin.url');
42
+ if (remoteUrl) {
43
+ // Extract repo name from URL (handles both HTTPS and SSH)
44
+ const match = remoteUrl.match(/\/([^/]+?)(\.git)?$/);
45
+ if (match) {
46
+ return match[1].replace('.git', '');
47
+ }
48
+ }
49
+ // Fallback to current directory name
50
+ return require('path').basename(process.cwd());
51
+ }
52
+ function isGitRepo() {
53
+ try {
54
+ (0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'pipe' });
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ function branchExists(branchName) {
62
+ try {
63
+ (0, child_process_1.execSync)(`git show-ref --verify --quiet refs/heads/${branchName}`, {
64
+ stdio: 'pipe',
65
+ });
66
+ return true;
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ function getCommitCount(worktreePath) {
73
+ try {
74
+ const output = exec('git log origin/main..HEAD --oneline', worktreePath);
75
+ if (!output)
76
+ return 0;
77
+ return output.split('\n').filter(Boolean).length;
78
+ }
79
+ catch {
80
+ return 0;
81
+ }
82
+ }
83
+ function getCommitList(worktreePath, limit = 10) {
84
+ return exec(`git log origin/main..HEAD --pretty=format:'- %s' | head -${limit}`, worktreePath);
85
+ }
@@ -0,0 +1,13 @@
1
+ export interface Issue {
2
+ number: number;
3
+ title: string;
4
+ body: string;
5
+ url: string;
6
+ }
7
+ export interface IssueListItem {
8
+ number: number;
9
+ title: string;
10
+ }
11
+ export declare function getIssue(issueNumber: number): Issue | null;
12
+ export declare function listIssues(limit?: number): IssueListItem[];
13
+ export declare function createPullRequest(title: string, body: string, branch: string, base?: string): string;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getIssue = getIssue;
4
+ exports.listIssues = listIssues;
5
+ exports.createPullRequest = createPullRequest;
6
+ const child_process_1 = require("child_process");
7
+ function getIssue(issueNumber) {
8
+ try {
9
+ const output = (0, child_process_1.execSync)(`gh issue view ${issueNumber} --json title,body,url`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
10
+ const data = JSON.parse(output);
11
+ return {
12
+ number: issueNumber,
13
+ title: data.title,
14
+ body: data.body || '',
15
+ url: data.url,
16
+ };
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function listIssues(limit = 20) {
23
+ try {
24
+ const output = (0, child_process_1.execSync)(`gh issue list --state open --limit ${limit} --json number,title`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
25
+ return JSON.parse(output);
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ }
31
+ function createPullRequest(title, body, branch, base = 'main') {
32
+ const output = (0, child_process_1.execSync)(`gh pr create --title "${title}" --body "${body.replace(/"/g, '\\"')}" --head "${branch}" --base "${base}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
33
+ return output.trim();
34
+ }
@@ -0,0 +1,8 @@
1
+ export declare function slugify(text: string): string;
2
+ export declare function checkRequirements(): {
3
+ ok: boolean;
4
+ missing: string[];
5
+ };
6
+ export declare function openInNewTerminal(script: string): void;
7
+ export declare function copyEnvFiles(from: string, to: string): void;
8
+ export declare function symlinkNodeModules(from: string, to: string): void;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.slugify = slugify;
37
+ exports.checkRequirements = checkRequirements;
38
+ exports.openInNewTerminal = openInNewTerminal;
39
+ exports.copyEnvFiles = copyEnvFiles;
40
+ exports.symlinkNodeModules = symlinkNodeModules;
41
+ const child_process_1 = require("child_process");
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const os = __importStar(require("os"));
45
+ function slugify(text) {
46
+ return text
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9]/g, '-')
49
+ .replace(/-+/g, '-')
50
+ .replace(/^-|-$/g, '')
51
+ .slice(0, 50);
52
+ }
53
+ function checkRequirements() {
54
+ const missing = [];
55
+ try {
56
+ (0, child_process_1.execSync)('gh --version', { stdio: 'pipe' });
57
+ }
58
+ catch {
59
+ missing.push('gh (GitHub CLI) - Install: brew install gh');
60
+ }
61
+ try {
62
+ (0, child_process_1.execSync)('claude --version', { stdio: 'pipe' });
63
+ }
64
+ catch {
65
+ missing.push('claude (Claude Code CLI) - Install: npm install -g @anthropic-ai/claude-code');
66
+ }
67
+ return { ok: missing.length === 0, missing };
68
+ }
69
+ function openInNewTerminal(script) {
70
+ const platform = os.platform();
71
+ if (platform === 'darwin') {
72
+ // macOS - try iTerm2 first, then Terminal
73
+ const iTermScript = `
74
+ tell application "iTerm"
75
+ activate
76
+ set newWindow to (create window with default profile)
77
+ tell current session of newWindow
78
+ write text "${script.replace(/"/g, '\\"')}"
79
+ end tell
80
+ end tell
81
+ `;
82
+ const terminalScript = `
83
+ tell application "Terminal"
84
+ activate
85
+ do script "${script.replace(/"/g, '\\"')}"
86
+ end tell
87
+ `;
88
+ try {
89
+ // Check if iTerm is installed
90
+ if (fs.existsSync('/Applications/iTerm.app')) {
91
+ (0, child_process_1.execSync)(`osascript -e '${iTermScript}'`, { stdio: 'pipe' });
92
+ }
93
+ else {
94
+ (0, child_process_1.execSync)(`osascript -e '${terminalScript}'`, { stdio: 'pipe' });
95
+ }
96
+ }
97
+ catch {
98
+ // Fallback to Terminal
99
+ try {
100
+ (0, child_process_1.execSync)(`osascript -e '${terminalScript}'`, { stdio: 'pipe' });
101
+ }
102
+ catch {
103
+ console.log('Could not open new terminal. Run manually:');
104
+ console.log(script);
105
+ }
106
+ }
107
+ }
108
+ else if (platform === 'linux') {
109
+ // Linux - try common terminal emulators
110
+ const terminals = ['gnome-terminal', 'xterm', 'konsole'];
111
+ for (const term of terminals) {
112
+ try {
113
+ (0, child_process_1.execSync)(`which ${term}`, { stdio: 'pipe' });
114
+ if (term === 'gnome-terminal') {
115
+ (0, child_process_1.spawn)(term, ['--', 'bash', '-c', script], { detached: true });
116
+ }
117
+ else {
118
+ (0, child_process_1.spawn)(term, ['-e', script], { detached: true });
119
+ }
120
+ return;
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ }
126
+ console.log('Could not detect terminal emulator. Run manually:');
127
+ console.log(script);
128
+ }
129
+ else {
130
+ console.log('Unsupported platform. Run manually:');
131
+ console.log(script);
132
+ }
133
+ }
134
+ function copyEnvFiles(from, to) {
135
+ const envFiles = ['.env', '.env.local'];
136
+ for (const file of envFiles) {
137
+ const src = path.join(from, file);
138
+ const dest = path.join(to, file);
139
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
140
+ fs.copyFileSync(src, dest);
141
+ }
142
+ }
143
+ }
144
+ function symlinkNodeModules(from, to) {
145
+ const src = path.join(from, 'node_modules');
146
+ const dest = path.join(to, 'node_modules');
147
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
148
+ fs.symlinkSync(src, dest);
149
+ }
150
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "claude-issue-solver",
3
+ "version": "1.0.0",
4
+ "description": "Automatically solve GitHub issues using Claude Code",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "claude-issue": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/index.ts",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "claude",
16
+ "github",
17
+ "issues",
18
+ "ai",
19
+ "automation",
20
+ "cli"
21
+ ],
22
+ "author": "MikeOuroumis",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/MikeOuroumis/claude-issue-solver.git"
27
+ },
28
+ "dependencies": {
29
+ "chalk": "^4.1.2",
30
+ "commander": "^12.1.0",
31
+ "inquirer": "^8.2.6",
32
+ "ora": "^5.4.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/inquirer": "^8.2.10",
36
+ "@types/node": "^20.10.0",
37
+ "typescript": "^5.3.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ]
45
+ }