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 CHANGED
@@ -1,2 +1,140 @@
1
- # COMING SOON
2
- CLI for Pullsmith
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
- console.log("pullsmith v0.0.1");
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.1",
4
- "description": "Trigger PRs from your Sentry errors",
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
+ }
@@ -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,8 @@
1
+ sentry_agent: sentry_error_fixer
2
+
3
+ agents:
4
+ - name: sentry_error_fixer
5
+ prompt: |
6
+ On a scale from 1 to 10, how beautiful is my code? Respond with only numbers 1 to 10.
7
+ model: claude-haiku-4-5
8
+ provider: claude
@@ -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"