@hifullmoon/aicommit 2.2.2 → 2.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.2.3] - 2026-09-01
8
+
9
+ ### Fixed
10
+
11
+ - Prevented accepted provider requests from being replayed after response-body interruptions, avoiding duplicate generations and potential duplicate billing.
12
+ - Honored provider retry timing safely by failing clearly when `Retry-After` exceeds the configured maximum delay instead of retrying too early.
13
+ - Stopped setup from replacing valid JSON configurations with unsupported schemas or invalid structures.
14
+
7
15
  ## [2.2.2] - 2026-08-31
8
16
 
9
17
  ### Added
@@ -170,7 +178,8 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
170
178
  - Added file-level split planning and execution with Git-state concurrency checks.
171
179
  - Added provider presets and user/project configuration boundaries.
172
180
 
173
- [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.2.2...HEAD
181
+ [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.2.3...HEAD
182
+ [2.2.3]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.3
174
183
  [2.2.2]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.2
175
184
  [2.2.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.1
176
185
  [2.2.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.0
@@ -26,7 +26,7 @@ The release workflow uses npm Trusted Publishing without a long-lived `NPM_TOKEN
26
26
  ```bash
27
27
  workdir=$(mktemp -d)
28
28
  cd "$workdir"
29
- npm install --package-lock-only @hifullmoon/aicommit@2.2.2
29
+ npm install --package-lock-only @hifullmoon/aicommit@2.2.3
30
30
  npm audit signatures
31
31
  ```
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifullmoon/aicommit",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "Safe, local-first AI commit message generator for Git workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -136,18 +136,24 @@ async function fetchWithRetry(apiUrl, init, timeout, configuredPolicy, consume)
136
136
  } catch (err) {
137
137
  const wrappedTimeout = timeoutError(err, timeout);
138
138
  if (wrappedTimeout) throw wrappedTimeout;
139
- if (!networkFailure(err) || attempt >= policy.maxAttempts) throw err;
140
- const delay = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
141
- await policy.sleep(delay);
142
- continue;
139
+ // Once the provider has accepted a generation request, replaying it is
140
+ // unsafe: the first request may already have completed and been billed
141
+ // even though its response body was interrupted locally.
142
+ throw err;
143
143
  }
144
144
  }
145
145
  if (RETRYABLE_STATUS.has(response.status) && attempt < policy.maxAttempts) {
146
146
  const requestedDelay = retryAfterMs(response.headers.get('retry-after'), policy.now);
147
- const delay = Math.min(
148
- requestedDelay ?? policy.baseDelayMs * 2 ** (attempt - 1),
149
- policy.maxDelayMs,
150
- );
147
+ if (requestedDelay !== null && requestedDelay > policy.maxDelayMs) {
148
+ await response.body?.cancel().catch(() => {});
149
+ throw new Error(
150
+ `HTTP ${response.status}: provider requested a retry after ${Math.ceil(
151
+ requestedDelay / 1000,
152
+ )}s, exceeding the configured retry.maxDelayMs limit.`,
153
+ );
154
+ }
155
+ const delay =
156
+ requestedDelay ?? Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
151
157
  await response.body?.cancel().catch(() => {});
152
158
  await policy.sleep(delay);
153
159
  continue;
package/src/setup.js CHANGED
@@ -74,9 +74,9 @@ export function mergeSetupConfig(existing, { providerName, entry, language }) {
74
74
 
75
75
  async function readExistingConfig(path) {
76
76
  if (!(await fileExists(path))) return {};
77
+ let parsed;
77
78
  try {
78
- const parsed = JSON.parse(await readFile(path, 'utf-8'));
79
- return validateUserConfig(parsed);
79
+ parsed = JSON.parse(await readFile(path, 'utf-8'));
80
80
  } catch (err) {
81
81
  const backup = `${path}.invalid-${Date.now()}.bak`;
82
82
  await copyFile(path, backup);
@@ -84,6 +84,16 @@ async function readExistingConfig(path) {
84
84
  console.log(chalk.dim(` The original file was preserved at ${backup}.`));
85
85
  return {};
86
86
  }
87
+
88
+ try {
89
+ return validateUserConfig(parsed);
90
+ } catch (err) {
91
+ throw new Error(
92
+ `Existing config ${path} is valid JSON but has an unsupported structure: ${err.message} ` +
93
+ 'Fix it or move it aside before running setup.',
94
+ { cause: err },
95
+ );
96
+ }
87
97
  }
88
98
 
89
99
  async function writeConfigAtomic(path, value) {