@slahon/lazykit 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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # 🦥 LazyKit
2
+
3
+ **Drop an issue, get a PR.**
4
+
5
+ LazyKit wires Claude AI into your GitHub repo so that when you label an issue, Claude reads it, writes the code, and opens a pull request — while you do something else.
6
+
7
+ ## Quickstart
8
+
9
+ ```bash
10
+ npx @slahon/lazykit init
11
+ ```
12
+
13
+ Run this inside your project folder. It will:
14
+ - Create `.github/workflows/lazykit.yml`
15
+ - Create `CLAUDE.md` with project context for Claude
16
+ - Create the trigger label on GitHub
17
+ - Print the one manual step (adding your Claude token)
18
+
19
+ ## Requirements
20
+
21
+ - Node.js 18+
22
+ - A GitHub repository
23
+ - A Claude Pro or Max subscription ([claude.ai](https://claude.ai))
24
+ - Claude Code installed locally (`npm install -g @anthropic-ai/claude-code`)
25
+ - GitHub CLI (`gh`) — optional but recommended
26
+
27
+ ## How it works
28
+
29
+ 1. You open a GitHub issue describing what you want
30
+ 2. You apply the `lazykit` label (or whatever you named it)
31
+ 3. A GitHub Actions workflow fires and runs Claude Code
32
+ 4. Claude reads the issue, explores your codebase, writes the changes
33
+ 5. Claude opens a pull request with a title and description
34
+ 6. You review and merge
35
+
36
+ ## Authentication
37
+
38
+ LazyKit uses your Claude Pro/Max subscription via an OAuth token — no pay-per-token API billing.
39
+
40
+ After running `npx lazykit init`, you need to:
41
+
42
+ 1. Generate your token:
43
+ ```bash
44
+ claude setup-token
45
+ ```
46
+
47
+ 2. Add it to your repo as a secret named `CLAUDE_CODE_OAUTH_TOKEN`
48
+
49
+ ## CLAUDE.md
50
+
51
+ LazyKit creates a `CLAUDE.md` file at your repo root. This is Claude's project guide — it tells Claude about your stack, conventions, and what commands to run before opening a PR. Edit it to match your actual project.
52
+
53
+ ## Options
54
+
55
+ During `npx lazykit init` you will be asked:
56
+
57
+ | Option | Default | Description |
58
+ |--------|---------|-------------|
59
+ | Label name | `lazykit` | The GitHub label that triggers Claude |
60
+ | Stack | — | Your tech stack (goes into CLAUDE.md) |
61
+ | Lint command | `npm run lint` | Run before every PR |
62
+ | Test command | `npm test` | Run before every PR |
63
+
64
+ ## Tips
65
+
66
+ - **Keep issues small and specific.** "Add a /health endpoint that returns `{ status: 'ok' }`" works great. "Rewrite the auth system" does not.
67
+ - **Edit CLAUDE.md** to describe your folder structure, naming conventions, and any rules Claude must follow.
68
+ - The `lazykit` label is your control switch — Claude only runs when you apply it, so you stay in charge.
69
+
70
+ ## License
71
+
72
+ MIT
package/claude-md.js ADDED
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ function generateClaudeMd({ stack, lintCommand, testCommand }) {
4
+ const hasLint = lintCommand && lintCommand !== 'skip';
5
+ const hasTest = testCommand && testCommand !== 'skip';
6
+
7
+ return `# LazyKit — Project Guide for Claude
8
+
9
+ ## Stack
10
+ ${stack || 'Describe your tech stack here (e.g. TypeScript + Next.js, Postgres via Prisma)'}
11
+
12
+ ## Conventions
13
+ - Match the existing code style and patterns in whatever file you are editing.
14
+ - Follow the folder structure already present in the repo.
15
+ - Keep changes scoped to what the issue asks for — do not refactor unrelated code.
16
+
17
+ ## Before opening a PR, always:
18
+ ${hasLint ? `1. Run \`${lintCommand}\` and fix all errors and warnings.` : '1. Check your code for obvious errors.'}
19
+ ${hasTest ? `2. Run \`${testCommand}\` and make sure all tests pass.` : '2. Make sure existing functionality is not broken.'}
20
+ 3. If you cannot get lint/tests passing, comment on the issue explaining why instead of pushing broken code.
21
+
22
+ ## Never
23
+ - Touch \`.github/workflows/\`, secrets, or CI configuration.
24
+ - Add new dependencies without clearly noting them in the PR description.
25
+ - Make changes outside the scope of the issue.
26
+ - Merge your own pull requests.
27
+ `;
28
+ }
29
+
30
+ module.exports = { generateClaudeMd };
package/git.js ADDED
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+
5
+ function getGitRemote() {
6
+ try {
7
+ const remote = execSync('git remote get-url origin', { stdio: 'pipe' })
8
+ .toString()
9
+ .trim();
10
+ return remote;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ function parseGitHubRepo(remoteUrl) {
17
+ if (!remoteUrl) return null;
18
+
19
+ // Handle SSH: git@github.com:owner/repo.git
20
+ const sshMatch = remoteUrl.match(/git@github\.com[:/](.+?)\/(.+?)(?:\.git)?$/);
21
+ if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] };
22
+
23
+ // Handle HTTPS: https://github.com/owner/repo.git
24
+ const httpsMatch = remoteUrl.match(/https?:\/\/github\.com\/(.+?)\/(.+?)(?:\.git)?$/);
25
+ if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] };
26
+
27
+ return null;
28
+ }
29
+
30
+ function isGitRepo() {
31
+ try {
32
+ execSync('git rev-parse --git-dir', { stdio: 'ignore' });
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function isGhCliAvailable() {
40
+ try {
41
+ execSync('gh --version', { stdio: 'ignore' });
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ module.exports = { getGitRemote, parseGitHubRepo, isGitRepo, isGhCliAvailable };
package/index.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const { init } = require('../src/commands/init');
6
+
7
+ const [,, command] = process.argv;
8
+
9
+ switch (command) {
10
+ case 'init':
11
+ case undefined:
12
+ init().catch(err => {
13
+ console.error('\n❌ Setup failed:', err.message);
14
+ process.exit(1);
15
+ });
16
+ break;
17
+ default:
18
+ console.log(`
19
+ 🦥 LazyKit — Drop an issue, get a PR.
20
+
21
+ Usage:
22
+ npx lazykit init Set up LazyKit in your current GitHub repo
23
+ `);
24
+ }
package/init.js ADDED
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+
9
+ const { log } = require('../utils/logger');
10
+ const { ask, confirm, select } = require('../utils/prompt');
11
+ const { isGitRepo, getGitRemote, parseGitHubRepo, isGhCliAvailable } = require('../utils/git');
12
+ const { generateWorkflow } = require('../templates/workflow');
13
+ const { generateClaudeMd } = require('../templates/claude-md');
14
+
15
+ async function init() {
16
+ console.log(chalk.bold.white('\n🦥 LazyKit — Drop an issue, get a PR.\n'));
17
+ console.log(chalk.gray(' This will set up AI-powered issue-to-PR automation in your repo.\n'));
18
+
19
+ // ─── Step 1: Verify git repo ─────────────────────────────────────────────
20
+ if (!isGitRepo()) {
21
+ log.error('Not inside a git repository. Run this from your project root.');
22
+ process.exit(1);
23
+ }
24
+
25
+ const remote = getGitRemote();
26
+ const repoInfo = parseGitHubRepo(remote);
27
+
28
+ if (repoInfo) {
29
+ log.success(`Detected repo: ${chalk.cyan(`${repoInfo.owner}/${repoInfo.repo}`)}`);
30
+ } else {
31
+ log.warn('Could not detect GitHub repo from git remote. Continuing anyway.');
32
+ }
33
+
34
+ log.blank();
35
+
36
+ // ─── Step 2: Ask questions ───────────────────────────────────────────────
37
+ log.title('Configure LazyKit');
38
+ log.blank();
39
+
40
+ const label = await ask('Label name to trigger Claude', 'lazykit');
41
+
42
+ const stack = await ask('What is your stack?', 'e.g. TypeScript + Next.js, Postgres');
43
+
44
+ const lintCommand = await ask('Lint command', 'npm run lint');
45
+
46
+ const testCommand = await ask('Test command', 'npm test');
47
+
48
+ const wantClaudeMd = await confirm('Generate CLAUDE.md project guide?', true);
49
+
50
+ log.blank();
51
+
52
+ // ─── Step 3: Create workflow file ────────────────────────────────────────
53
+ const spinner = ora({ text: 'Creating workflow file...', color: 'cyan' }).start();
54
+
55
+ const workflowDir = path.join(process.cwd(), '.github', 'workflows');
56
+ fs.mkdirSync(workflowDir, { recursive: true });
57
+
58
+ const workflowPath = path.join(workflowDir, 'lazykit.yml');
59
+ const workflowContent = generateWorkflow({ label, lintCommand, testCommand });
60
+ fs.writeFileSync(workflowPath, workflowContent);
61
+
62
+ spinner.succeed(chalk.green('Created .github/workflows/lazykit.yml'));
63
+
64
+ // ─── Step 4: Create CLAUDE.md ────────────────────────────────────────────
65
+ if (wantClaudeMd) {
66
+ const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
67
+ const claudeMdContent = generateClaudeMd({ stack, lintCommand, testCommand });
68
+
69
+ if (fs.existsSync(claudeMdPath)) {
70
+ const overwrite = await confirm('CLAUDE.md already exists. Overwrite?', false);
71
+ if (overwrite) {
72
+ fs.writeFileSync(claudeMdPath, claudeMdContent);
73
+ log.success('Updated CLAUDE.md');
74
+ } else {
75
+ log.warn('Skipped CLAUDE.md — keeping existing file.');
76
+ }
77
+ } else {
78
+ fs.writeFileSync(claudeMdPath, claudeMdContent);
79
+ log.success('Created CLAUDE.md');
80
+ }
81
+ }
82
+
83
+ // ─── Step 5: Create GitHub label ─────────────────────────────────────────
84
+ const ghAvailable = isGhCliAvailable();
85
+
86
+ if (ghAvailable && repoInfo) {
87
+ const labelSpinner = ora({ text: `Creating '${label}' label on GitHub...`, color: 'cyan' }).start();
88
+ try {
89
+ execSync(
90
+ `gh label create "${label}" --color "#7B61FF" --description "Let Claude handle this" --repo ${repoInfo.owner}/${repoInfo.repo}`,
91
+ { stdio: 'pipe' }
92
+ );
93
+ labelSpinner.succeed(chalk.green(`Created '${label}' label on GitHub`));
94
+ } catch (err) {
95
+ const msg = err.stderr?.toString() || '';
96
+ if (msg.includes('already exists')) {
97
+ labelSpinner.succeed(chalk.green(`Label '${label}' already exists — skipping`));
98
+ } else {
99
+ labelSpinner.warn(chalk.yellow(`Could not create label automatically — you'll need to create it manually`));
100
+ }
101
+ }
102
+ } else {
103
+ log.warn(`gh CLI not found — create the '${label}' label manually on GitHub`);
104
+ }
105
+
106
+ // ─── Step 6: Done! Print final instructions ───────────────────────────────
107
+ console.log('\n' + chalk.bold.green(' ✨ LazyKit is almost ready!\n'));
108
+ log.divider();
109
+
110
+ console.log(chalk.bold('\n One manual step — add your Claude token as a repo secret:\n'));
111
+
112
+ console.log(chalk.gray(' 1.') + ' Run in your terminal:');
113
+ console.log(chalk.cyan(' claude setup-token'));
114
+
115
+ console.log(chalk.gray('\n 2.') + ' Copy the token it prints ' + chalk.gray('(sk-ant-oat01-...)'));
116
+
117
+ if (repoInfo) {
118
+ console.log(chalk.gray('\n 3.') + ' Go to:');
119
+ console.log(chalk.cyan(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`));
120
+ } else {
121
+ console.log(chalk.gray('\n 3.') + ' Go to:');
122
+ console.log(chalk.cyan(' Your repo → Settings → Secrets and variables → Actions'));
123
+ }
124
+
125
+ console.log(chalk.gray('\n 4.') + ' Click ' + chalk.bold('"New repository secret"') + ' and add:');
126
+ console.log(chalk.gray(' Name: ') + chalk.cyan('CLAUDE_CODE_OAUTH_TOKEN'));
127
+ console.log(chalk.gray(' Value: ') + chalk.cyan('the token you copied'));
128
+
129
+ console.log(chalk.gray('\n 5.') + ' Also enable PR creation:');
130
+ if (repoInfo) {
131
+ console.log(chalk.cyan(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`));
132
+ } else {
133
+ console.log(chalk.cyan(' Repo → Settings → Actions → General → Workflow permissions'));
134
+ }
135
+ console.log(chalk.gray(' Enable: ') + '"Allow GitHub Actions to create and approve pull requests"');
136
+
137
+ log.divider();
138
+
139
+ console.log(chalk.bold('\n Then commit and push the new files:\n'));
140
+ console.log(chalk.cyan(' git add .github/workflows/lazykit.yml' + (wantClaudeMd ? ' CLAUDE.md' : '')));
141
+ console.log(chalk.cyan(' git commit -m "Add LazyKit automation"'));
142
+ console.log(chalk.cyan(' git push'));
143
+
144
+ log.divider();
145
+
146
+ console.log(chalk.bold('\n Done! Here is how to use it:\n'));
147
+ console.log(` ${chalk.gray('1.')} Open a GitHub issue describing what you want`);
148
+ console.log(` ${chalk.gray('2.')} Apply the ${chalk.bold.magenta(label)} label`);
149
+ console.log(` ${chalk.gray('3.')} Watch Claude open a PR with the changes\n`);
150
+
151
+ console.log(chalk.bold.white(' 🦥 LazyKit is ready. Go be lazy.\n'));
152
+ }
153
+
154
+ module.exports = { init };
package/logger.js ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ const log = {
6
+ info: (msg) => console.log(chalk.cyan(' →'), msg),
7
+ success: (msg) => console.log(chalk.green(' ✓'), msg),
8
+ warn: (msg) => console.log(chalk.yellow(' ⚠'), msg),
9
+ error: (msg) => console.log(chalk.red(' ✗'), msg),
10
+ title: (msg) => console.log('\n' + chalk.bold.white(msg)),
11
+ divider: () => console.log(chalk.gray(' ' + '─'.repeat(50))),
12
+ blank: () => console.log(),
13
+ raw: (msg) => console.log(msg),
14
+ };
15
+
16
+ module.exports = { log };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@slahon/lazykit",
3
+ "version": "1.0.0",
4
+ "description": "Drop an issue, get a PR. AI-powered issue-to-PR automation using Claude.",
5
+ "bin": {
6
+ "lazykit": "./bin/index.js"
7
+ },
8
+ "scripts": {
9
+ "start": "node bin/index.js"
10
+ },
11
+ "keywords": [
12
+ "claude",
13
+ "ai",
14
+ "github-actions",
15
+ "automation",
16
+ "cli",
17
+ "issue-to-pr"
18
+ ],
19
+ "author": "",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "chalk": "^4.1.2",
23
+ "ora": "^5.4.1"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ }
28
+ }
package/prompt.js ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+ const chalk = require('chalk');
5
+
6
+ function ask(question, defaultValue = '') {
7
+ return new Promise((resolve) => {
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ });
12
+
13
+ const display = defaultValue
14
+ ? ` ${chalk.bold(question)} ${chalk.gray(`(${defaultValue})`)} › `
15
+ : ` ${chalk.bold(question)} › `;
16
+
17
+ rl.question(display, (answer) => {
18
+ rl.close();
19
+ resolve(answer.trim() || defaultValue);
20
+ });
21
+ });
22
+ }
23
+
24
+ function confirm(question, defaultValue = true) {
25
+ return new Promise((resolve) => {
26
+ const rl = readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout,
29
+ });
30
+
31
+ const hint = defaultValue ? chalk.gray('Y/n') : chalk.gray('y/N');
32
+ rl.question(` ${chalk.bold(question)} ${hint} › `, (answer) => {
33
+ rl.close();
34
+ if (!answer.trim()) return resolve(defaultValue);
35
+ resolve(answer.trim().toLowerCase() === 'y');
36
+ });
37
+ });
38
+ }
39
+
40
+ function select(question, options) {
41
+ return new Promise((resolve) => {
42
+ console.log(`\n ${chalk.bold(question)}`);
43
+ options.forEach((opt, i) => {
44
+ console.log(` ${chalk.gray(`${i + 1}.`)} ${opt.label}`);
45
+ });
46
+
47
+ const rl = readline.createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout,
50
+ });
51
+
52
+ rl.question(`\n ${chalk.gray('Enter number')} › `, (answer) => {
53
+ rl.close();
54
+ const index = parseInt(answer.trim()) - 1;
55
+ if (index >= 0 && index < options.length) {
56
+ resolve(options[index].value);
57
+ } else {
58
+ resolve(options[0].value);
59
+ }
60
+ });
61
+ });
62
+ }
63
+
64
+ module.exports = { ask, confirm, select };
package/workflow.js ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ function generateWorkflow({ label, lintCommand, testCommand }) {
4
+ const hasLint = lintCommand && lintCommand !== 'skip';
5
+ const hasTest = testCommand && testCommand !== 'skip';
6
+
7
+ const checks = [
8
+ hasLint ? `Run \`${lintCommand}\` and fix any issues before committing.` : null,
9
+ hasTest ? `Run \`${testCommand}\` and make sure all tests pass before committing.` : null,
10
+ 'If lint or tests fail and you cannot fix them, do NOT commit — instead post a comment on the issue explaining what went wrong.',
11
+ ].filter(Boolean);
12
+
13
+ const checkList = checks.map((c, i) => ` ${i + 1}. ${c}`).join('\n');
14
+
15
+ return `name: LazyKit
16
+
17
+ on:
18
+ issues:
19
+ types: [opened, labeled]
20
+ issue_comment:
21
+ types: [created]
22
+
23
+ concurrency:
24
+ group: lazykit-\${{ github.event.issue.number }}
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ lazykit:
29
+ if: >
30
+ contains(github.event.issue.labels.*.name, '${label}') ||
31
+ contains(github.event.comment.body, '@claude')
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 30
34
+ permissions:
35
+ contents: write
36
+ pull-requests: write
37
+ issues: write
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ with:
42
+ fetch-depth: 0
43
+
44
+ - uses: anthropics/claude-code-action@v1
45
+ with:
46
+ claude_code_oauth_token: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
47
+ label_trigger: ${label}
48
+ claude_args: --max-turns 50 --dangerously-skip-permissions
49
+ prompt: |
50
+ You are an autonomous coding agent called LazyKit.
51
+ Your job is to read the GitHub issue, understand the requirement,
52
+ implement the changes in the codebase, and open a pull request.
53
+
54
+ Before opening the PR:
55
+ ${checkList}
56
+
57
+ When opening the PR:
58
+ - Use a clear title that summarises what was done
59
+ - Write a description explaining what changed and why
60
+ - Reference the issue number so it auto-closes on merge
61
+ `;
62
+ }
63
+
64
+ module.exports = { generateWorkflow };