ira-review 2.0.1 → 3.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.github.md CHANGED
@@ -39,7 +39,7 @@ IRA runs a 13-step pipeline for each review. Every step after step 1 is designed
39
39
  7. Fetch source files for changed files (for full-file context)
40
40
  8. Run AI review on each file/issue (concurrent, configurable model)
41
41
  9. Calculate risk score (0-100) from issue severity, complexity, and security signals
42
- 10. Validate JIRA acceptance criteria against the diff (if configured)
42
+ 10. Automatically validate or generate JIRA acceptance criteria (if configured)
43
43
  11. Deduplicate: skip issues already commented on in previous runs
44
44
  12. Post summary + inline comments to the PR
45
45
  13. Send Slack/Teams notification (if configured, respects risk threshold)
@@ -98,7 +98,7 @@ flowchart LR
98
98
 
99
99
  ```
100
100
  src/
101
- ai/ AI provider abstraction (OpenAI, Anthropic, Azure, Ollama)
101
+ ai/ AI provider abstraction (OpenAI, Anthropic, Azure, Ollama, AMP)
102
102
  core/ Review engine, risk scorer, acceptance validator, test generator
103
103
  scm/ GitHub and Bitbucket clients (diff, comments, labels, build status)
104
104
  integrations/ JIRA client, Slack/Teams notifier
@@ -182,7 +182,7 @@ Each rule has a `message` (what to tell the developer), a `severity` (BLOCKER, C
182
182
  }
183
183
  ```
184
184
 
185
- Rules without `paths` apply to all files. Rules with `paths` are only checked against matching files. The file is validated at load time: invalid severity values and missing required fields are skipped with a warning. Maximum 30 rules per file. IRA rules are for nuanced, context-dependent standards that linters cannot express. Deterministic checks (naming conventions, import order, formatting) belong in ESLint.
185
+ Rules without `paths` apply to all files. Rules with `paths` are only checked against matching files. The file is validated at load time: invalid severity values and missing required fields are skipped with a warning. Maximum 100 rules per file. IRA rules are for nuanced, context-dependent standards that linters cannot express. Deterministic checks (naming conventions, import order, formatting) belong in ESLint.
186
186
 
187
187
  Rules are enforced in all review surfaces (CLI, CI/CD, VS Code extension) with no license gating. In the VS Code extension, run `IRA: Init Rules File` from the command palette to scaffold an empty `.ira-rules.json`. The extension ships a JSON Schema for the file, so you get autocomplete and validation as you edit.
188
188
 
@@ -210,7 +210,7 @@ IRA is not a SaaS product. There is no hosted service, no telemetry, no analytic
210
210
  | | CLI | VS Code Extension |
211
211
  |---|---|---|
212
212
  | **Use case** | CI pipelines, scripting, headless environments | Interactive development |
213
- | **AI default** | OpenAI (requires API key) | GitHub Copilot (zero config) |
213
+ | **AI default** | OpenAI (requires API key) | GitHub Copilot (zero config), AMP CLI also supported |
214
214
  | **Auth** | Environment variables or CLI flags | VS Code OAuth + OS keychain |
215
215
  | **Output** | Terminal + PR comments | Inline diagnostics, CodeLens, TreeView, risk badge |
216
216
  | **JIRA/Sonar** | CLI flags or env vars | VS Code settings |
@@ -282,7 +282,7 @@ Suggested Fix: Use parameterized queries:
282
282
  3. `Cmd+Shift+P` > `IRA: Review Current PR`
283
283
  4. Enter your PR number
284
284
 
285
- If you have GitHub Copilot, that is all you need. No API keys, no configuration.
285
+ If you have GitHub Copilot, that is all you need. No API keys, no configuration. Alternatively, set the AI provider to `amp` if you have the AMP CLI installed (`amp login`).
286
286
 
287
287
  ### CLI
288
288
 
@@ -393,6 +393,7 @@ npx ira-review review \
393
393
  | Provider | Notes |
394
394
  |---|---|
395
395
  | GitHub Copilot | VS Code only, zero config, uses existing session |
396
+ | AMP CLI | VS Code only, requires `amp` CLI installed and authenticated (`amp login`) |
396
397
  | OpenAI | Default for CLI |
397
398
  | Azure OpenAI | Requires `--ai-base-url` and `--ai-deployment` |
398
399
  | Anthropic | Pass key with `--ai-api-key` |
@@ -420,7 +421,7 @@ CLI flags override environment variables, which override the config file. Token
420
421
  ## Requirements
421
422
 
422
423
  - Node.js 18+
423
- - An AI provider API key (or Ollama running locally, or GitHub Copilot for the VS Code extension)
424
+ - An AI provider API key (or Ollama running locally, or GitHub Copilot / AMP CLI for the VS Code extension)
424
425
  - A GitHub or Bitbucket repo with an open PR
425
426
 
426
427
  ## License
package/README.md CHANGED
@@ -15,55 +15,19 @@ No install required. Drop `--dry-run` to post comments directly on the PR. For B
15
15
  ## What You Get
16
16
 
17
17
  ```
18
- 🔍 IRA Scanning PR before your reviewers do
19
-
20
- ✓ Config loaded — AI-only mode, openai, PR #42
21
- Diff loaded 4 files changed
22
- Review complete 3 issues found
23
-
24
- ────────────────────────────────────────────────────────────
25
- 📄 src/routes/auth.ts:31
26
- Rule: IRA/security (CRITICAL)
27
- Message: User input passed directly to SQL query
28
- Explain: The username parameter is concatenated into a SQL string,
29
- creating a SQL injection vector.
30
- Impact: Attacker could execute arbitrary SQL and gain database control.
31
- Fix: BEFORE: `db.query(`SELECT * FROM users WHERE name = ${username}`)`
32
- → AFTER: `db.query('SELECT * FROM users WHERE name = $1', [username])`
33
-
34
- ────────────────────────────────────────────────────────────
35
- 📄 src/middleware/cors.ts:8
36
- Rule: IRA/error-handling (MAJOR)
37
- Message: Empty catch block swallows CORS validation errors
38
- Explain: fetch() failure in CORS preflight is caught and ignored,
39
- leaving the request in an undefined state.
40
- Impact: Silent CORS failures in production with no logging.
41
- Fix: BEFORE: `} catch {}`
42
- → AFTER: `} catch (err) { logger.error('CORS preflight failed', err); throw err; }`
43
-
44
- # 🔍 IRA Review Summary
45
-
46
- ## 🟡 Risk: MEDIUM (38/100)
47
-
48
- | Metric | Value |
49
- |---------------|----------|
50
- | Review mode | AI-only |
51
- | Total issues | 3 |
52
- | Reviewed (AI) | 3 |
53
- | Framework | react |
54
-
55
- ## ✅ Requirements: AUTH-234 — 83% Complete (5/6)
56
-
57
- ✅ OAuth2 login flow implemented with Google provider
58
- ✅ JWT tokens generated on successful authentication
59
- ✅ Refresh token rotation with 7-day expiry
60
- ❌ Input validation on login endpoint — no email format check
61
- ✅ Logout endpoint clears session and revokes token
62
- ✅ Rate limiting on login attempts
63
-
64
- ⚠️ Edge Cases Not Covered
65
- - What happens when Google OAuth is unreachable?
66
- - Token refresh during concurrent requests?
18
+ IRA: Found 3 issues (Risk: MEDIUM - 47/100)
19
+
20
+ src/routes/todos.ts
21
+ [BLOCKER] SQL injection risk - user input passed directly to query
22
+ [MAJOR] Missing database index on frequently queried column
23
+
24
+ src/middleware/auth.ts
25
+ [CRITICAL] JWT secret hardcoded - move to environment variable
26
+
27
+ JIRA AC Validation (PROJ-1234):
28
+ AC 1: User can create a todo item COVERED
29
+ AC 2: Input is validated before save NOT COVERED
30
+ AC 3: Error returns 422 with details COVERED
67
31
  ```
68
32
 
69
33
  Each issue is posted as an inline comment on the exact PR line with explanation, impact, and a minimal BEFORE → AFTER fix.
@@ -114,7 +78,7 @@ Commit a `.ira-rules.json` to your repo root. Rules are injected into the AI pro
114
78
  **Rules:**
115
79
  - `message` + `severity` required. `bad`/`good` examples and `paths` are optional.
116
80
  - Rules without `paths` apply to all files. Rules with `paths` match only those directories.
117
- - Maximum 50 rules. Deterministic checks (naming, formatting) belong in ESLint.
81
+ - Maximum 100 rules. Deterministic checks (naming, formatting) belong in ESLint.
118
82
  - Invalid rules are skipped with a warning, not a crash.
119
83
  - No license gating. Works in CLI, CI/CD, and VS Code extension.
120
84
 
@@ -208,12 +172,12 @@ CLI flags override env vars, which override the config file. Token fields are bl
208
172
 
209
173
  **SCM:** GitHub, GitHub Enterprise, Bitbucket Cloud, Bitbucket Server/Data Center
210
174
 
211
- **AI:** OpenAI (default), Azure OpenAI, Anthropic, Ollama (local, no key needed)
175
+ **AI:** OpenAI (default), Azure OpenAI, Anthropic, Ollama (local, no key needed), AMP CLI (VS Code extension)
212
176
 
213
177
  ## Requirements
214
178
 
215
179
  - Node.js 18+
216
- - An AI provider API key (or Ollama running locally)
180
+ - An AI provider API key (or Ollama running locally, or AMP CLI / GitHub Copilot for the VS Code extension)
217
181
 
218
182
  ## Security
219
183
 
package/README.npm.md CHANGED
@@ -15,55 +15,19 @@ No install required. Drop `--dry-run` to post comments directly on the PR. For B
15
15
  ## What You Get
16
16
 
17
17
  ```
18
- 🔍 IRA Scanning PR before your reviewers do
19
-
20
- ✓ Config loaded — AI-only mode, openai, PR #42
21
- Diff loaded 4 files changed
22
- Review complete 3 issues found
23
-
24
- ────────────────────────────────────────────────────────────
25
- 📄 src/routes/auth.ts:31
26
- Rule: IRA/security (CRITICAL)
27
- Message: User input passed directly to SQL query
28
- Explain: The username parameter is concatenated into a SQL string,
29
- creating a SQL injection vector.
30
- Impact: Attacker could execute arbitrary SQL and gain database control.
31
- Fix: BEFORE: `db.query(`SELECT * FROM users WHERE name = ${username}`)`
32
- → AFTER: `db.query('SELECT * FROM users WHERE name = $1', [username])`
33
-
34
- ────────────────────────────────────────────────────────────
35
- 📄 src/middleware/cors.ts:8
36
- Rule: IRA/error-handling (MAJOR)
37
- Message: Empty catch block swallows CORS validation errors
38
- Explain: fetch() failure in CORS preflight is caught and ignored,
39
- leaving the request in an undefined state.
40
- Impact: Silent CORS failures in production with no logging.
41
- Fix: BEFORE: `} catch {}`
42
- → AFTER: `} catch (err) { logger.error('CORS preflight failed', err); throw err; }`
43
-
44
- # 🔍 IRA Review Summary
45
-
46
- ## 🟡 Risk: MEDIUM (38/100)
47
-
48
- | Metric | Value |
49
- |---------------|----------|
50
- | Review mode | AI-only |
51
- | Total issues | 3 |
52
- | Reviewed (AI) | 3 |
53
- | Framework | react |
54
-
55
- ## ✅ Requirements: AUTH-234 — 83% Complete (5/6)
56
-
57
- ✅ OAuth2 login flow implemented with Google provider
58
- ✅ JWT tokens generated on successful authentication
59
- ✅ Refresh token rotation with 7-day expiry
60
- ❌ Input validation on login endpoint — no email format check
61
- ✅ Logout endpoint clears session and revokes token
62
- ✅ Rate limiting on login attempts
63
-
64
- ⚠️ Edge Cases Not Covered
65
- - What happens when Google OAuth is unreachable?
66
- - Token refresh during concurrent requests?
18
+ IRA: Found 3 issues (Risk: MEDIUM - 47/100)
19
+
20
+ src/routes/todos.ts
21
+ [BLOCKER] SQL injection risk - user input passed directly to query
22
+ [MAJOR] Missing database index on frequently queried column
23
+
24
+ src/middleware/auth.ts
25
+ [CRITICAL] JWT secret hardcoded - move to environment variable
26
+
27
+ JIRA AC Validation (PROJ-1234):
28
+ AC 1: User can create a todo item COVERED
29
+ AC 2: Input is validated before save NOT COVERED
30
+ AC 3: Error returns 422 with details COVERED
67
31
  ```
68
32
 
69
33
  Each issue is posted as an inline comment on the exact PR line with explanation, impact, and a minimal BEFORE → AFTER fix.
@@ -114,7 +78,7 @@ Commit a `.ira-rules.json` to your repo root. Rules are injected into the AI pro
114
78
  **Rules:**
115
79
  - `message` + `severity` required. `bad`/`good` examples and `paths` are optional.
116
80
  - Rules without `paths` apply to all files. Rules with `paths` match only those directories.
117
- - Maximum 50 rules. Deterministic checks (naming, formatting) belong in ESLint.
81
+ - Maximum 100 rules. Deterministic checks (naming, formatting) belong in ESLint.
118
82
  - Invalid rules are skipped with a warning, not a crash.
119
83
  - No license gating. Works in CLI, CI/CD, and VS Code extension.
120
84
 
@@ -208,12 +172,12 @@ CLI flags override env vars, which override the config file. Token fields are bl
208
172
 
209
173
  **SCM:** GitHub, GitHub Enterprise, Bitbucket Cloud, Bitbucket Server/Data Center
210
174
 
211
- **AI:** OpenAI (default), Azure OpenAI, Anthropic, Ollama (local, no key needed)
175
+ **AI:** OpenAI (default), Azure OpenAI, Anthropic, Ollama (local, no key needed), AMP CLI (VS Code extension)
212
176
 
213
177
  ## Requirements
214
178
 
215
179
  - Node.js 18+
216
- - An AI provider API key (or Ollama running locally)
180
+ - An AI provider API key (or Ollama running locally, or AMP CLI / GitHub Copilot for the VS Code extension)
217
181
 
218
182
  ## Security
219
183
 
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BitbucketClient
4
- } from "./chunk-XOR6EFZE.js";
5
- import "./chunk-AHCFDKK4.js";
4
+ } from "./chunk-ZKADAXVW.js";
5
+ import "./chunk-AFLVYFZ2.js";
6
6
  export {
7
7
  BitbucketClient
8
8
  };
@@ -88,6 +88,41 @@ async function fetchWithTimeout(url, init = {}, timeoutMs = 3e4) {
88
88
  clearTimeout(timer);
89
89
  }
90
90
  }
91
+ function parseApiError(status, body, provider) {
92
+ const statusMessages = {
93
+ 400: "Bad request",
94
+ 401: "Authentication failed \u2014 check your token or credentials",
95
+ 403: "Access denied \u2014 you may not have permission for this resource",
96
+ 404: "Not found \u2014 check the URL, project key, or PR number",
97
+ 408: "Request timed out \u2014 try again in a moment",
98
+ 409: "Conflict \u2014 the resource may have been modified",
99
+ 422: "Invalid request \u2014 the server couldn't process it",
100
+ 429: "Rate limited \u2014 too many requests, try again shortly",
101
+ 500: "Server error \u2014 the service is having issues",
102
+ 502: "Bad gateway \u2014 the service is temporarily unavailable",
103
+ 503: "Service unavailable \u2014 try again in a moment",
104
+ 504: "Gateway timeout \u2014 the service took too long to respond"
105
+ };
106
+ const friendlyStatus = statusMessages[status] ?? `HTTP ${status}`;
107
+ const trimmed = body.trim();
108
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
109
+ try {
110
+ const json = JSON.parse(trimmed);
111
+ const msg = json.message ?? json.error?.message ?? json.error ?? json.errors?.[0]?.message ?? json.detail;
112
+ if (typeof msg === "string" && msg.length > 0 && msg.length < 200) {
113
+ return `${provider} (${status}): ${msg}`;
114
+ }
115
+ } catch {
116
+ }
117
+ }
118
+ if (trimmed.startsWith("<!") || trimmed.startsWith("<html") || trimmed.includes("<body")) {
119
+ return `${provider} (${status}): ${friendlyStatus}`;
120
+ }
121
+ if (trimmed.length > 0 && trimmed.length < 150) {
122
+ return `${provider} (${status}): ${trimmed}`;
123
+ }
124
+ return `${provider} (${status}): ${friendlyStatus}`;
125
+ }
91
126
  function sleep(ms) {
92
127
  return new Promise((resolve) => setTimeout(resolve, ms));
93
128
  }
@@ -95,5 +130,6 @@ function sleep(ms) {
95
130
  export {
96
131
  RetryableError,
97
132
  withRetry,
98
- fetchWithTimeout
133
+ fetchWithTimeout,
134
+ parseApiError
99
135
  };
@@ -2,8 +2,9 @@
2
2
  import {
3
3
  RetryableError,
4
4
  fetchWithTimeout,
5
+ parseApiError,
5
6
  withRetry
6
- } from "./chunk-AHCFDKK4.js";
7
+ } from "./chunk-AFLVYFZ2.js";
7
8
 
8
9
  // src/scm/github.ts
9
10
  var GitHubClient = class {
@@ -48,12 +49,26 @@ var GitHubClient = class {
48
49
  if (!response.ok) {
49
50
  const text = await response.text();
50
51
  throw new RetryableError(
51
- `GitHub API error (${response.status}): ${text}`,
52
+ parseApiError(response.status, text, "GitHub"),
52
53
  response.status
53
54
  );
54
55
  }
55
56
  });
56
57
  }
58
+ async getIssueComments(pullRequestId) {
59
+ const bodies = [];
60
+ let page = 1;
61
+ while (true) {
62
+ const url = `${this.baseUrl}/repos/${this.owner}/${this.repo}/issues/${pullRequestId}/comments?per_page=100&page=${page}`;
63
+ const response = await fetchWithTimeout(url, { headers: this.headers });
64
+ if (!response.ok) break;
65
+ const comments = await response.json();
66
+ for (const c of comments) bodies.push(c.body);
67
+ if (comments.length < 100) break;
68
+ page++;
69
+ }
70
+ return bodies;
71
+ }
57
72
  async getPRState(pullRequestId) {
58
73
  const url = `${this.baseUrl}/repos/${this.owner}/${this.repo}/pulls/${pullRequestId}`;
59
74
  const response = await fetchWithTimeout(url, { headers: this.headers });
@@ -81,7 +96,7 @@ var GitHubClient = class {
81
96
  if (!response.ok) {
82
97
  const text = await response.text();
83
98
  throw new RetryableError(
84
- `GitHub API error (${response.status}): ${text}`,
99
+ parseApiError(response.status, text, "GitHub"),
85
100
  response.status
86
101
  );
87
102
  }
@@ -97,7 +112,7 @@ var GitHubClient = class {
97
112
  const response = await fetchWithTimeout(url, { headers: this.headers });
98
113
  if (!response.ok) {
99
114
  const text = await response.text();
100
- throw new RetryableError(`GitHub API error (${response.status}): ${text}`, response.status);
115
+ throw new RetryableError(parseApiError(response.status, text, "GitHub"), response.status);
101
116
  }
102
117
  return response.json();
103
118
  });
@@ -125,7 +140,7 @@ ${file.patch}`;
125
140
  if (!response.ok) {
126
141
  const text = await response.text();
127
142
  throw new RetryableError(
128
- `GitHub API error (${response.status}): ${text}`,
143
+ parseApiError(response.status, text, "GitHub"),
129
144
  response.status
130
145
  );
131
146
  }
@@ -154,7 +169,7 @@ ${file.patch}`;
154
169
  if (!response.ok) {
155
170
  const text = await response.text();
156
171
  throw new RetryableError(
157
- `GitHub API error (${response.status}): ${text}`,
172
+ parseApiError(response.status, text, "GitHub"),
158
173
  response.status
159
174
  );
160
175
  }
@@ -171,7 +186,7 @@ ${file.patch}`;
171
186
  if (!response.ok) {
172
187
  const text = await response.text();
173
188
  throw new RetryableError(
174
- `GitHub API error (${response.status}): ${text}`,
189
+ parseApiError(response.status, text, "GitHub"),
175
190
  response.status
176
191
  );
177
192
  }
@@ -188,7 +203,7 @@ ${file.patch}`;
188
203
  if (!response.ok) {
189
204
  const text = await response.text();
190
205
  throw new RetryableError(
191
- `GitHub API error (${response.status}): ${text}`,
206
+ parseApiError(response.status, text, "GitHub"),
192
207
  response.status
193
208
  );
194
209
  }
@@ -221,7 +236,7 @@ ${file.patch}`;
221
236
  if (!response.ok && response.status !== 422) {
222
237
  const text = await response.text();
223
238
  throw new RetryableError(
224
- `GitHub API error (${response.status}): ${text}`,
239
+ parseApiError(response.status, text, "GitHub"),
225
240
  response.status
226
241
  );
227
242
  }
@@ -258,7 +273,7 @@ ${file.patch}`;
258
273
  if (!response.ok) {
259
274
  const text = await response.text();
260
275
  throw new RetryableError(
261
- `GitHub API error (${response.status}): ${text}`,
276
+ parseApiError(response.status, text, "GitHub"),
262
277
  response.status
263
278
  );
264
279
  }
@@ -2,8 +2,9 @@
2
2
  import {
3
3
  RetryableError,
4
4
  fetchWithTimeout,
5
+ parseApiError,
5
6
  withRetry
6
- } from "./chunk-AHCFDKK4.js";
7
+ } from "./chunk-AFLVYFZ2.js";
7
8
 
8
9
  // src/scm/bitbucket.ts
9
10
  var BitbucketClient = class {
@@ -55,14 +56,14 @@ var BitbucketClient = class {
55
56
  if (!retryResponse.ok) {
56
57
  const retryText = await retryResponse.text();
57
58
  throw new RetryableError(
58
- `Bitbucket API error (${retryResponse.status}): ${retryText}`,
59
+ parseApiError(retryResponse.status, retryText, "Bitbucket"),
59
60
  retryResponse.status
60
61
  );
61
62
  }
62
63
  return;
63
64
  }
64
65
  throw new RetryableError(
65
- `Bitbucket API error (${response.status}): ${text}`,
66
+ parseApiError(response.status, text, "Bitbucket"),
66
67
  response.status
67
68
  );
68
69
  }
@@ -82,12 +83,24 @@ var BitbucketClient = class {
82
83
  if (!response.ok) {
83
84
  const text = await response.text();
84
85
  throw new RetryableError(
85
- `Bitbucket API error (${response.status}): ${text}`,
86
+ parseApiError(response.status, text, "Bitbucket"),
86
87
  response.status
87
88
  );
88
89
  }
89
90
  });
90
91
  }
92
+ async getIssueComments(pullRequestId) {
93
+ const bodies = [];
94
+ let url = `${this.baseUrl}/repositories/${this.workspace}/${this.repoSlug}/pullrequests/${pullRequestId}/comments?pagelen=100`;
95
+ while (url) {
96
+ const response = await fetchWithTimeout(url, { headers: this.headers });
97
+ if (!response.ok) break;
98
+ const data = await response.json();
99
+ for (const c of data.values) bodies.push(c.content.raw);
100
+ url = data.next;
101
+ }
102
+ return bodies;
103
+ }
91
104
  async getFileContent(filePath, pullRequestId) {
92
105
  const sourceHash = await this.getSourceHash(pullRequestId);
93
106
  const encodedPath = filePath.split("/").map(encodeURIComponent).join("/");
@@ -99,7 +112,7 @@ var BitbucketClient = class {
99
112
  if (!response.ok) {
100
113
  const text = await response.text();
101
114
  throw new RetryableError(
102
- `Bitbucket API error (${response.status}): ${text}`,
115
+ parseApiError(response.status, text, "Bitbucket"),
103
116
  response.status
104
117
  );
105
118
  }
@@ -131,7 +144,7 @@ var BitbucketClient = class {
131
144
  if (!response.ok) {
132
145
  const text = await response.text();
133
146
  throw new RetryableError(
134
- `Bitbucket API error (${response.status}): ${text}`,
147
+ parseApiError(response.status, text, "Bitbucket"),
135
148
  response.status
136
149
  );
137
150
  }
@@ -147,7 +160,7 @@ var BitbucketClient = class {
147
160
  const response = await fetchWithTimeout(nextUrl, { headers: this.headers });
148
161
  if (!response.ok) {
149
162
  const text = await response.text();
150
- throw new RetryableError(`Bitbucket API error (${response.status}): ${text}`, response.status);
163
+ throw new RetryableError(parseApiError(response.status, text, "Bitbucket"), response.status);
151
164
  }
152
165
  return response.json();
153
166
  });
@@ -166,7 +179,7 @@ var BitbucketClient = class {
166
179
  const response = await fetchWithTimeout(url, { headers: this.headers }, 15e3);
167
180
  if (!response.ok) {
168
181
  const text = await response.text();
169
- throw new RetryableError(`Bitbucket API error (${response.status}): ${text}`, response.status);
182
+ throw new RetryableError(parseApiError(response.status, text, "Bitbucket"), response.status);
170
183
  }
171
184
  return response.text();
172
185
  });
@@ -189,7 +202,7 @@ var BitbucketClient = class {
189
202
  if (!response.ok) {
190
203
  const text = await response.text();
191
204
  throw new RetryableError(
192
- `Bitbucket API error (${response.status}): ${text}`,
205
+ parseApiError(response.status, text, "Bitbucket"),
193
206
  response.status
194
207
  );
195
208
  }
@@ -218,7 +231,7 @@ var BitbucketClient = class {
218
231
  if (!response.ok) {
219
232
  const text = await response.text();
220
233
  throw new RetryableError(
221
- `Bitbucket API error (${response.status}): ${text}`,
234
+ parseApiError(response.status, text, "Bitbucket"),
222
235
  response.status
223
236
  );
224
237
  }