pullsmith 0.0.1 → 0.0.3
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 +140 -2
- package/bin/pullsmith.js +22 -1
- package/package.json +7 -2
- package/src/commands/doctor.js +50 -0
- package/src/commands/init.js +202 -0
- package/src/commands/validate.js +47 -0
- package/src/lib/auth.js +25 -0
- package/src/lib/config.js +1 -0
- package/src/lib/git.js +16 -0
- package/src/utils/extract/agent/model.sh +17 -0
- package/src/utils/extract/agent/prompt.sh +20 -0
- package/src/utils/templates/prompt.yaml +8 -0
- package/src/utils/templates/workflow.yaml +69 -0
package/README.md
CHANGED
|
@@ -1,2 +1,140 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# Pullsmith
|
|
2
|
+
|
|
3
|
+
Run AI agents in your CI/CD pipeline with a single YAML file.
|
|
4
|
+
|
|
5
|
+
Pullsmith connects your GitHub repo, writes a lightweight `.pullsmith` config, and installs a GitHub Actions workflow that can ask Claude Code to investigate an error, make a fix, push a branch, and open a pull request.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g pullsmith
|
|
9
|
+
pullsmith init
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What It Does
|
|
13
|
+
|
|
14
|
+
Pullsmith gives your repository an AI-powered repair loop:
|
|
15
|
+
|
|
16
|
+
1. A Sentry-style error title is passed into a GitHub Actions workflow.
|
|
17
|
+
2. The workflow reads your `.pullsmith` agent config.
|
|
18
|
+
3. Claude Code runs inside CI with your selected model and prompt.
|
|
19
|
+
4. If code changes are produced, Pullsmith commits them to a new branch.
|
|
20
|
+
5. A pull request is opened with the proposed fix.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
Install the CLI:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g pullsmith
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run setup from the root of a GitHub repository:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pullsmith init
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Pullsmith will:
|
|
37
|
+
|
|
38
|
+
- Open your browser to authenticate with Pullsmith.
|
|
39
|
+
- Save CLI credentials locally at `~/.pullsmith/credentials`.
|
|
40
|
+
- Create a `.pullsmith` config file if one does not exist.
|
|
41
|
+
- Check that your repo has an Anthropic/Claude API key in GitHub Actions secrets.
|
|
42
|
+
- Create `.github/workflows/pullsmith.yaml`.
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Node.js 18 or newer.
|
|
47
|
+
- A GitHub repository with an `origin` remote.
|
|
48
|
+
- A Pullsmith account at `https://pullsmith.dev`.
|
|
49
|
+
- A GitHub Actions secret named `ANTHROPIC_API_KEY` or `CLAUDE_API_KEY`.
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
| Command | Description |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `pullsmith init` | Authenticate, create `.pullsmith`, and install the GitHub Actions workflow. |
|
|
56
|
+
| `pullsmith validate` | Validate your `.pullsmith` file with the Pullsmith API. |
|
|
57
|
+
| `pullsmith doctor` | Check that the current repo is connected and ready. |
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
Pullsmith stores agent behavior in `.pullsmith`:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
sentry_agent: sentry_error_fixer
|
|
65
|
+
|
|
66
|
+
agents:
|
|
67
|
+
- name: sentry_error_fixer
|
|
68
|
+
prompt: |
|
|
69
|
+
Investigate the error, find the root cause, and make the smallest safe fix.
|
|
70
|
+
model: claude-haiku-4-5
|
|
71
|
+
provider: claude
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The generated GitHub Actions workflow reads this file to decide which agent, prompt, and model to use.
|
|
75
|
+
|
|
76
|
+
## GitHub Actions
|
|
77
|
+
|
|
78
|
+
After `pullsmith init`, your repo gets a workflow at:
|
|
79
|
+
|
|
80
|
+
```txt
|
|
81
|
+
.github/workflows/pullsmith.yaml
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The workflow can be run manually with an `error` input:
|
|
85
|
+
|
|
86
|
+
```txt
|
|
87
|
+
Sentry error title
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
When the workflow runs, it creates a branch named like:
|
|
91
|
+
|
|
92
|
+
```txt
|
|
93
|
+
pullsmith/fix-your-error-title
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then it opens a pull request with Claude Code's proposed fix.
|
|
97
|
+
|
|
98
|
+
## Local Development
|
|
99
|
+
|
|
100
|
+
By default, the CLI talks to production:
|
|
101
|
+
|
|
102
|
+
```txt
|
|
103
|
+
https://pullsmith.dev
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
To point the CLI at a local Pullsmith app, set `PULLSMITH_BASE_URL`:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
PULLSMITH_BASE_URL=http://localhost:3000 node bin/pullsmith.js init
|
|
110
|
+
PULLSMITH_BASE_URL=http://localhost:3000 node bin/pullsmith.js validate
|
|
111
|
+
PULLSMITH_BASE_URL=http://localhost:3000 node bin/pullsmith.js doctor
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Publishing
|
|
115
|
+
|
|
116
|
+
Maintainers can publish the package directly from this directory:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm publish
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If the version already exists on npm, bump it first:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm version patch
|
|
126
|
+
npm publish
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Security Notes
|
|
130
|
+
|
|
131
|
+
- Pullsmith stores local CLI credentials in `~/.pullsmith/credentials`.
|
|
132
|
+
- Anthropic credentials should stay in GitHub Actions secrets.
|
|
133
|
+
- The generated workflow grants `contents: write` and `pull-requests: write` so it can push a branch and open a PR.
|
|
134
|
+
- Review generated pull requests before merging.
|
|
135
|
+
|
|
136
|
+
## Links
|
|
137
|
+
|
|
138
|
+
- Website: https://pullsmith.dev
|
|
139
|
+
- Repository: https://github.com/pullsmith/cli
|
|
140
|
+
- Issues: https://github.com/pullsmith/cli/issues
|
package/bin/pullsmith.js
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { init } from "../src/commands/init.js"
|
|
4
|
+
import { validate } from "../src/commands/validate.js"
|
|
5
|
+
import { doctor } from "../src/commands/doctor.js"
|
|
6
|
+
// import { push } from "../src/commands/push.js"
|
|
7
|
+
|
|
8
|
+
const command = process.argv[2];
|
|
9
|
+
|
|
10
|
+
if (command == "init"){
|
|
11
|
+
init();
|
|
12
|
+
} else if(command == "validate"){
|
|
13
|
+
validate();
|
|
14
|
+
} else if(command == "doctor"){
|
|
15
|
+
doctor();
|
|
16
|
+
} else {
|
|
17
|
+
console.log("Usage: pullsmith <command>");
|
|
18
|
+
console.log("Commands: init, validate, doctor");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// console.log("pullsmith v0.0.1");
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pullsmith",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Run AI agents in your CI/CD pipeline with a single YAML file.",
|
|
5
|
+
"homepage": "https://pullsmith.dev",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/pullsmith/cli/issues"
|
|
8
|
+
},
|
|
5
9
|
"bin": {
|
|
6
10
|
"pullsmith": "./bin/pullsmith.js"
|
|
7
11
|
},
|
|
12
|
+
"type": "module",
|
|
8
13
|
"repository": {
|
|
9
14
|
"type": "git",
|
|
10
15
|
"url": "https://github.com/pullsmith/cli"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getToken } from "../lib/auth.js";
|
|
2
|
+
import { getRepoRemoteUrl } from "../lib/git.js";
|
|
3
|
+
import { PULLSMITH_BASE_URL } from "../lib/config.js";
|
|
4
|
+
|
|
5
|
+
export async function doctor() {
|
|
6
|
+
let token;
|
|
7
|
+
let repo;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
token = getToken();
|
|
11
|
+
repo = getRepoRemoteUrl();
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error(err.message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(`${PULLSMITH_BASE_URL}/api/doctor`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"Authorization": `Bearer ${token}`
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({ repo })
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const text = await response.text();
|
|
29
|
+
const error = getResponseError(text);
|
|
30
|
+
console.error(error ?? "Pullsmith doctor failed.");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log("Pullsmith doctor passed.");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(err.message ?? "Pullsmith doctor failed.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getResponseError(text) {
|
|
42
|
+
if (!text) return null;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(text);
|
|
46
|
+
return data?.error ?? text;
|
|
47
|
+
} catch {
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { getRepoRemoteUrl } from "../lib/git.js";
|
|
4
|
+
import { saveToken as saveTokenLocally } from "../lib/auth.js";
|
|
5
|
+
import { PULLSMITH_BASE_URL } from "../lib/config.js";
|
|
6
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
|
|
9
|
+
const PORT = 9421;
|
|
10
|
+
|
|
11
|
+
export async function init() {
|
|
12
|
+
let repoUrl;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
repoUrl = getRepoRemoteUrl();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error(err.message);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const connectUrl = `${PULLSMITH_BASE_URL}/connect?repo=${encodeURIComponent(repoUrl)}`;
|
|
22
|
+
|
|
23
|
+
console.log("Opening browser to authenticate...");
|
|
24
|
+
|
|
25
|
+
if (process.platform === "darwin") {
|
|
26
|
+
// macOS
|
|
27
|
+
exec(`open "${connectUrl}"`);
|
|
28
|
+
} else if (process.platform === "win32") {
|
|
29
|
+
exec(`start "" "${connectUrl}"`);
|
|
30
|
+
} else {
|
|
31
|
+
exec(`xdg-open "${connectUrl}"`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await waitForNextJsInternalToken(repoUrl);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(err.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createPullsmithFile(){
|
|
44
|
+
try {
|
|
45
|
+
const filePath = join(process.cwd(), ".pullsmith");
|
|
46
|
+
|
|
47
|
+
if (existsSync(filePath)) {
|
|
48
|
+
console.log(".pullsmith file already exits, skipping.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const promptPath = new URL("../../src/utils/templates/prompt.yaml", import.meta.url);
|
|
53
|
+
const prompt = readFileSync(promptPath, "utf8");
|
|
54
|
+
|
|
55
|
+
writeFileSync(filePath, prompt, "utf8");
|
|
56
|
+
|
|
57
|
+
console.log(".pullsmith file created!");
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new Error("☹️ Failed to create local .pullsmith file.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
function getGithubWorkflowContent(secretName) {
|
|
65
|
+
const templatePath = new URL("../../src/utils/templates/workflow.yaml", import.meta.url);
|
|
66
|
+
return readFileSync(templatePath, "utf8").replaceAll("__SECRET_NAME__", secretName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
function createGithubWorkflowFile(secretName) {
|
|
71
|
+
try {
|
|
72
|
+
const dirPath = join(process.cwd(), ".github", "workflows");
|
|
73
|
+
const filePath = join(dirPath, "pullsmith.yaml");
|
|
74
|
+
|
|
75
|
+
if (existsSync(filePath)) {
|
|
76
|
+
console.log("Workflow file already exists, skipping.");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
mkdirSync(dirPath, { recursive: true });
|
|
81
|
+
|
|
82
|
+
writeFileSync(filePath, getGithubWorkflowContent(secretName), "utf8");
|
|
83
|
+
|
|
84
|
+
console.log("✅ Workflow file created at .github/workflows/pullsmith.yml");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw new Error(`Failed to create Github workflow file: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deleteWorkflowFile() {
|
|
91
|
+
try {
|
|
92
|
+
// .github/workflows/pullsmith.yml
|
|
93
|
+
const filePath = join(process.cwd(), ".github", "workflows", "pullsmith.yml");
|
|
94
|
+
if (existsSync(filePath)) {
|
|
95
|
+
rmSync(filePath);
|
|
96
|
+
console.log("🗑️ Removed old workflow file.");
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw new Error("☹️ Failed to delete local .pullsmith file.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// We only get this token if Github auth is successful
|
|
104
|
+
function waitForNextJsInternalToken(repoUrl) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const server = http.createServer(async (req, res) => {
|
|
107
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
108
|
+
const internalNextJSToken = url.searchParams.get("token");
|
|
109
|
+
|
|
110
|
+
if (internalNextJSToken) {
|
|
111
|
+
saveTokenLocally(internalNextJSToken);
|
|
112
|
+
deleteWorkflowFile();
|
|
113
|
+
createPullsmithFile();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const secretName = await checkThatClaudeAPIKeyIsInSecrets(internalNextJSToken, repoUrl);
|
|
117
|
+
createGithubWorkflowFile(secretName);
|
|
118
|
+
|
|
119
|
+
res.writeHead(200);
|
|
120
|
+
res.end("Authentication successful! You can close this tab.");
|
|
121
|
+
server.close();
|
|
122
|
+
console.log("Authenticated successfully!");
|
|
123
|
+
resolve();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
reject(err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
server.listen(PORT, () => {
|
|
131
|
+
console.log(`Waiting for authentication on port ${PORT}...`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
server.on("error", reject);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
const missingClaudeSecretsMsg = (secretUrl) => `❌ No Anthropic API key found in Github Actions secrets.\n
|
|
140
|
+
Please add your Claude API key at:
|
|
141
|
+
${secretUrl}
|
|
142
|
+
Make sure to name it: ANTHROPIC_API_KEY or CLAUDE_API_KEY
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async function checkThatClaudeAPIKeyIsInSecrets(nextJsInternalToken, repoUrl) {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch(`${PULLSMITH_BASE_URL}/api/init`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"Authorization": `Bearer ${nextJsInternalToken}`
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify({ repo: repoUrl })
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
// if the github token access has expired this fires
|
|
161
|
+
throw new Error(data.error ?? "Problem with Github Auth") ;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (data.secretName) {
|
|
165
|
+
console.log(`✅ You have a Claude API key in Github Actions: ${data.secretName}`);
|
|
166
|
+
return data.secretName;
|
|
167
|
+
} else {
|
|
168
|
+
throw new Error(missingClaudeSecretsMsg(data.secretsUrl))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
} catch (err) {
|
|
172
|
+
throw new Error(err.message ?? 'Failed to find Claude API key in Github Actions.');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// async function checkAnthropicSecret(repoUrl, token) {
|
|
177
|
+
// const { owner, repo } = parseRepoInfo(repoUrl);
|
|
178
|
+
//
|
|
179
|
+
// const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/actions/secrets`, {
|
|
180
|
+
// headers: {
|
|
181
|
+
// "Authorization": `Bearer ${token}`,
|
|
182
|
+
// "Accept": "application/vnd.github+json"
|
|
183
|
+
// }
|
|
184
|
+
// });
|
|
185
|
+
//
|
|
186
|
+
// const data = await response.json();
|
|
187
|
+
// const secrets = data.secrets || [];
|
|
188
|
+
//
|
|
189
|
+
// const hasKey = secrets.some(s =>
|
|
190
|
+
// s.name.includes("ANTHROPIC") || s.name.includes("CLAUDE")
|
|
191
|
+
// );
|
|
192
|
+
//
|
|
193
|
+
// if (!hasKey) {
|
|
194
|
+
// const secretsUrl = `https://github.com/${owner}/${repo}/settings/secrets/actions/new`;
|
|
195
|
+
// console.log(`\n⚠️ No Anthropic API key found in your repo secrets.`);
|
|
196
|
+
// console.log(` Please add your API key at:`);
|
|
197
|
+
// console.log(` ${secretsUrl}`);
|
|
198
|
+
// console.log(` Name it: ANTHROPIC_API_KEY or CLAUDE_API_KEY\n`);
|
|
199
|
+
// } else {
|
|
200
|
+
// console.log("✅ Anthropic API key found in repo secrets.");
|
|
201
|
+
// }
|
|
202
|
+
// }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { PULLSMITH_BASE_URL } from "../lib/config.js";
|
|
4
|
+
|
|
5
|
+
export async function validate() {
|
|
6
|
+
const filePath = join(process.cwd(), ".pullsmith");
|
|
7
|
+
|
|
8
|
+
if (!existsSync(filePath)) {
|
|
9
|
+
console.error("Missing .pullsmith file. Run `pullsmith init` first.");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const file = readFileSync(filePath, "utf8");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${PULLSMITH_BASE_URL}/api/validate`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/json"
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({ file })
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
const error = getResponseError(text);
|
|
27
|
+
console.error(error ?? "Failed to validate .pullsmith file.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(".pullsmith is valid.");
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err.message ?? "Failed to validate .pullsmith file.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getResponseError(text) {
|
|
39
|
+
if (!text) return null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const data = JSON.parse(text);
|
|
43
|
+
return data?.error ?? text;
|
|
44
|
+
} catch {
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/lib/auth.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
const credentialsPath = join(homedir(), ".pullsmith", "credentials");
|
|
6
|
+
|
|
7
|
+
// Saves generated token from Pullsmith.dev in local machine ./pullsmith/credentials
|
|
8
|
+
export function saveToken(token) {
|
|
9
|
+
try {
|
|
10
|
+
mkdirSync(join(homedir(), ".pullsmith"), { recursive: true });
|
|
11
|
+
writeFileSync(credentialsPath, JSON.stringify({ token }), "utf8");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
throw new Error("☹️ Failed to save local credentials in .pullsmith file.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Reads the token so the CLI can make authenticated requests to Pullsmith's API
|
|
18
|
+
export function getToken() {
|
|
19
|
+
try {
|
|
20
|
+
const file = readFileSync(credentialsPath, "utf8");
|
|
21
|
+
return JSON.parse(file).token;
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error("Not authenticated. Run `pullsmith init` first.");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PULLSMITH_BASE_URL = process.env.PULLSMITH_BASE_URL ?? "https://pullsmith.dev";
|
package/src/lib/git.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
export function getRepoRemoteUrl(){
|
|
4
|
+
try {
|
|
5
|
+
return execSync("git remote get-url origin", { stdio: "pipe" }).toString().trim();
|
|
6
|
+
} catch (err) {
|
|
7
|
+
throw new Error("☹️ Looks like you're not inside a Git repo. cd into your project, then try again.");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseRepoInfo(remoteUrl) {
|
|
12
|
+
const match = remoteUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
|
|
13
|
+
if (!match) throw new Error("Could not parse GitHub repo info from remote URL.");
|
|
14
|
+
return { owner: match[1], repo: match[2] };
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
AGENT=$(awk -F': *' '/^sentry_agent:/{print $2; exit}' .pullsmith)
|
|
4
|
+
|
|
5
|
+
MODEL=$(awk -v a="$AGENT" '
|
|
6
|
+
/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/ {
|
|
7
|
+
name=$0; sub(/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/,"",name)
|
|
8
|
+
here=(name==a); next
|
|
9
|
+
}
|
|
10
|
+
here && /^[[:space:]]*model:[[:space:]]*/ {
|
|
11
|
+
m=$0; sub(/^[[:space:]]*model:[[:space:]]*/,"",m)
|
|
12
|
+
sub(/[[:space:]]*(#.*)?$/,"",m) # strip trailing spaces/comments
|
|
13
|
+
print m; exit
|
|
14
|
+
}
|
|
15
|
+
' .pullsmith)
|
|
16
|
+
|
|
17
|
+
printf '%s\n' "$MODEL"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
AGENT=$(awk -F': *' '/^sentry_agent:/{print $2; exit}' .pullsmith)
|
|
4
|
+
|
|
5
|
+
PROMPT=$(awk -v a="$AGENT" '
|
|
6
|
+
/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/ {
|
|
7
|
+
name=$0; sub(/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/,"",name)
|
|
8
|
+
here=(name==a); inp=0; next
|
|
9
|
+
}
|
|
10
|
+
here && /prompt:[[:space:]]*\|/ { inp=1; ind=-1; next }
|
|
11
|
+
inp {
|
|
12
|
+
if ($0 ~ /^[[:space:]]*$/) { print ""; next }
|
|
13
|
+
n=match($0,/[^[:space:]]/)-1
|
|
14
|
+
if (ind<0) ind=n
|
|
15
|
+
if (n<ind) { inp=0; next }
|
|
16
|
+
print substr($0, ind+1)
|
|
17
|
+
}
|
|
18
|
+
' .pullsmith)
|
|
19
|
+
|
|
20
|
+
printf '%s\n' "$PROMPT"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Pullsmith
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
inputs:
|
|
5
|
+
error:
|
|
6
|
+
description: "Sentry error title"
|
|
7
|
+
required: false
|
|
8
|
+
default: "Sentry error"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
run:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pull-requests: write
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- name: Run Claude Code
|
|
20
|
+
env:
|
|
21
|
+
ANTHROPIC_API_KEY: ${{ secrets.__SECRET_NAME__ }}
|
|
22
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
23
|
+
run: |
|
|
24
|
+
ID=$(gh api "/users/pullsmith-dev[bot]" --jq .id)
|
|
25
|
+
git config user.email "${ID}+pullsmith-dev[bot]@users.noreply.github.com"
|
|
26
|
+
git config user.name "pullsmith-dev[bot]"
|
|
27
|
+
ERROR="${{ github.event.inputs.error }}"
|
|
28
|
+
SLUG=$(echo "$ERROR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//' | cut -c1-50)
|
|
29
|
+
BRANCH="pullsmith/fix-$SLUG"
|
|
30
|
+
AGENT=$(awk -F': *' '/^sentry_agent:/{print $2; exit}' .pullsmith)
|
|
31
|
+
PROMPT=$(awk -v a="$AGENT" '
|
|
32
|
+
/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/ {
|
|
33
|
+
name=$0; sub(/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/,"",name)
|
|
34
|
+
here=(name==a); inp=0; next
|
|
35
|
+
}
|
|
36
|
+
here && /prompt:[[:space:]]*|/ { inp=1; ind=-1; next }
|
|
37
|
+
inp {
|
|
38
|
+
if ($0 ~ /^[[:space:]]*$/) { print ""; next }
|
|
39
|
+
n=match($0,/[^[:space:]]/)-1
|
|
40
|
+
if (ind<0) ind=n
|
|
41
|
+
if (n<ind) { inp=0; next }
|
|
42
|
+
print substr($0, ind+1)
|
|
43
|
+
}
|
|
44
|
+
' .pullsmith)
|
|
45
|
+
MODEL=$(awk -v a="$AGENT" '
|
|
46
|
+
/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/ {
|
|
47
|
+
name=$0; sub(/^[[:space:]]*-?[[:space:]]*name:[[:space:]]*/,"",name)
|
|
48
|
+
here=(name==a); next
|
|
49
|
+
}
|
|
50
|
+
here && /^[[:space:]]*model:[[:space:]]*/ {
|
|
51
|
+
m=$0; sub(/^[[:space:]]*model:[[:space:]]*/,"",m)
|
|
52
|
+
sub(/[[:space:]]*(#.*)?$/,"",m) # strip trailing spaces/comments
|
|
53
|
+
print m; exit
|
|
54
|
+
}
|
|
55
|
+
' .pullsmith)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
git checkout -b "$BRANCH"
|
|
59
|
+
npx @anthropic-ai/claude-code -p --model "$MODEL" --dangerously-skip-permissions "$PROMPT. You are fixing this Sentry error: $ERROR. ONLY edit the code in the working tree to fix it. Do NOT run git, do NOT commit, do NOT push, and do NOT open a pull request - the surrounding workflow handles all of that."
|
|
60
|
+
|
|
61
|
+
git add -A
|
|
62
|
+
if git diff --cached --quiet; then
|
|
63
|
+
echo "No changes produced; nothing to do."
|
|
64
|
+
exit 0;
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
git commit -m "fix: $ERROR"
|
|
68
|
+
git push origin "$BRANCH"
|
|
69
|
+
gh pr create --title "$ERROR" --body "Automated fix by Pullsmith for Sentry error: $ERROR" --base "${{ github.ref_name }}" --head "$BRANCH"
|