codeep 2.22.0 → 2.24.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.
@@ -259,7 +259,7 @@ export async function summarizeEarlierHistory(history, maxChars = 16000) {
259
259
  if (dropped.length === 0)
260
260
  return '';
261
261
  const key = createHash('sha256')
262
- .update(dropped.map(m => `${m.role}:${m.content}`).join(''))
262
+ .update(dropped.map(m => `${m.role}:${m.content}`).join('\u0000'))
263
263
  .digest('hex');
264
264
  const cached = earlierSummaryCache.get(key);
265
265
  if (cached)
@@ -507,7 +507,12 @@ additionalTools, runtime) {
507
507
  if (errorText.includes('tools') || errorText.includes('function') || response.status === 400) {
508
508
  return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout, additionalTools, runtime);
509
509
  }
510
- throw new Error(`API error: ${response.status} - ${errorText}`);
510
+ // ApiError, not Error: runAgent's retry loop refuses to retry a 4xx by
511
+ // checking `err instanceof ApiError && err.status`. A bare Error made
512
+ // that guard silently inapplicable, so an expired key was retried once
513
+ // per iteration until the budget ran out and the run reported "Exceeded
514
+ // maximum of N iterations" — the one explanation unrelated to the cause.
515
+ throw new ApiError(`API error: ${response.status} - ${errorText}`, response.status);
511
516
  }
512
517
  if (useStreaming && response.body) {
513
518
  if (protocol === 'openai')
@@ -658,7 +663,9 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
658
663
  });
659
664
  if (!response.ok) {
660
665
  const error = await response.text();
661
- throw new Error(`API error: ${response.status} - ${error}`);
666
+ // Same reasoning as the native-tools path: the status has to survive, or
667
+ // the caller cannot tell "your key is wrong" from "the network hiccuped".
668
+ throw new ApiError(`API error: ${response.status} - ${error}`, response.status);
662
669
  }
663
670
  let content;
664
671
  if (onChunk && response.body) {
@@ -55,11 +55,14 @@ const CODE_PATTERNS = [
55
55
  // Performance issues
56
56
  {
57
57
  id: 'foreach-await',
58
- pattern: /\.forEach\s*\([^)]*\)\s*{\s*await/g,
58
+ pattern: /\.forEach\s*\(\s*async\b[^{]{0,120}\{[^{}]{0,200}?\bawait\b/g,
59
59
  category: 'performance',
60
60
  severity: 'warning',
61
61
  message: 'Sequential async operations in forEach are inefficient',
62
62
  suggestion: 'Use Promise.all() with map() for parallel execution',
63
+ // Both quantifiers are bounded and both classes are negated, so the match
64
+ // is linear in the input — this rule is a built-in and never passes
65
+ // through the custom-rule screening in utils/reviewConfig.ts.
63
66
  extensions: ['.js', '.ts', '.jsx', '.tsx'],
64
67
  },
65
68
  {
@@ -172,12 +175,27 @@ const CODE_PATTERNS = [
172
175
  extensions: ['.js', '.ts', '.jsx', '.tsx'],
173
176
  },
174
177
  ];
178
+ /**
179
+ * Collapse JavaScript's module-flavoured extensions onto `.js`.
180
+ *
181
+ * `.mjs` and `.cjs` are JavaScript — the module system differs, nothing a
182
+ * regex rule cares about does. Without this, thirteen rules that name `.js`
183
+ * silently skipped every such file, security rules included: `eval()`,
184
+ * `innerHTML` and hardcoded credentials went unreported in `.mjs` entirely.
185
+ *
186
+ * Normalising here rather than widening each rule's `extensions` array means a
187
+ * rule added tomorrow is covered by default. The arrays were the wrong place
188
+ * to fix it — thirteen copies of the same fact is how it went wrong once.
189
+ */
190
+ function normaliseExtension(ext) {
191
+ return ext === '.mjs' || ext === '.cjs' ? '.js' : ext;
192
+ }
175
193
  /**
176
194
  * Analyze a single file for issues
177
195
  */
178
196
  function analyzeFile(filePath, content, projectRoot, rules, disabled) {
179
197
  const issues = [];
180
- const ext = extname(filePath);
198
+ const ext = normaliseExtension(extname(filePath));
181
199
  const relativePath = relative(projectRoot, filePath);
182
200
  const lines = content.split('\n');
183
201
  // Skip the regex pass on very large files (the cheap line-count heuristics
@@ -299,7 +317,7 @@ function getAllSourceFiles(dir, maxFiles = 50) {
299
317
  }
300
318
  else if (entry.isFile()) {
301
319
  const ext = extname(entry.name);
302
- if (['.ts', '.tsx', '.js', '.jsx', '.py', '.php', '.go', '.rs'].includes(ext)) {
320
+ if (['.ts', '.tsx', '.js', '.mjs', '.cjs', '.jsx', '.py', '.php', '.go', '.rs'].includes(ext)) {
303
321
  files.push(fullPath);
304
322
  }
305
323
  }
@@ -1,4 +1,4 @@
1
- import { buildFixPlan, summariseFixPlan } from './reviewFix.js';
1
+ import { buildFixPlan, summariseFixPlan, describeAgentActivity } from './reviewFix.js';
2
2
  // Headless `codeep review` — a non-interactive entry point around the
3
3
  // deterministic reviewer in codeReview.ts. No API key, no TUI: it scans, prints
4
4
  // a report (markdown or JSON), and exits non-zero when issues at/above a chosen
@@ -214,22 +214,40 @@ function defaultDeps() {
214
214
  */
215
215
  async function runFixPlan(plan, context) {
216
216
  try {
217
+ // Populate the key cache before anything asks for it. `getApiKey` is
218
+ // synchronous and reads the cache alone — it does not consult the
219
+ // environment — so without this every request goes out with an empty
220
+ // bearer token and the provider answers 401. The AI review path next door
221
+ // has always done this; the fix path never did, which is why a CI run with
222
+ // a perfectly good key in the environment spent its whole iteration budget
223
+ // being rejected.
224
+ const { loadAllApiKeys, isConfigured } = await import('../config/index.js');
225
+ await loadAllApiKeys();
226
+ if (!isConfigured()) {
227
+ return `${summariseFixPlan(plan)} No API key is configured for the current provider, so the fix agent could not start.`;
228
+ }
217
229
  const { runAgent } = await import('./agent.js');
218
230
  // No cast here. `as never` on this call once hid the fact that
219
231
  // personalityOverride did not exist, which would have run the CI fix with
220
232
  // no boundary at all while the tests happily asserted otherwise.
221
233
  const result = await runAgent(plan.prompt, context, {
222
234
  personalityOverride: plan.personality,
223
- maxIterations: 12,
235
+ // The product default. 12 was picked to keep a CI run cheap and was
236
+ // simply too small: fixing one innerHTML call and running the suite ran
237
+ // out of steps, and an agent stopped mid-edit leaves a worse diff than
238
+ // one that never started. The real bounds on cost here are the size of
239
+ // the plan, which buildFixPlan caps, and the action's wall-clock.
240
+ maxIterations: 25,
224
241
  });
225
242
  const edited = new Set(result.actions
226
243
  .filter(a => a.type === 'write' || a.type === 'edit')
227
244
  .map(a => a.target));
245
+ const activity = describeAgentActivity(result.actions);
228
246
  if (!result.success) {
229
- return `${summariseFixPlan(plan)} The run did not finish: ${result.error ?? 'unknown error'}.`;
247
+ return `${summariseFixPlan(plan)} The run did not finish: ${result.error ?? 'unknown error'}. ${activity}`;
230
248
  }
231
249
  if (edited.size === 0) {
232
- return `${summariseFixPlan(plan)} Nothing was changed — the agent judged the findings not mechanically fixable.`;
250
+ return `${summariseFixPlan(plan)} Nothing was changed. ${activity}`;
233
251
  }
234
252
  return `${summariseFixPlan(plan)} Edited ${edited.size} file${edited.size === 1 ? '' : 's'}: ${[...edited].join(', ')}.`;
235
253
  }
@@ -63,3 +63,21 @@ export declare function buildFixPlan(issues: ReviewIssue[], options?: FixPlanOpt
63
63
  export declare function formatFixPrompt(issues: ReviewIssue[]): string;
64
64
  /** A one-line summary for the pull request body the action opens. */
65
65
  export declare function summariseFixPlan(plan: FixPlan): string;
66
+ /**
67
+ * What the agent actually did, in one line.
68
+ *
69
+ * A fix that changes nothing is the hardest outcome to act on, because the
70
+ * summary that reports it — "the run did not finish", "nothing was changed" —
71
+ * says what did not happen and never what did. Debugging one such run through
72
+ * CI cost two releases and forty minutes of guessing at whether the agent
73
+ * could not find the file, could not write, or was being refused a tool.
74
+ *
75
+ * Counting the action log answers that in the message itself. Failures are
76
+ * called out separately from successes, and one failing detail is quoted,
77
+ * because a refusal reason is usually the whole explanation.
78
+ */
79
+ export declare function describeAgentActivity(actions: {
80
+ type: string;
81
+ result: string;
82
+ details?: string;
83
+ }[]): string;
@@ -139,3 +139,35 @@ export function summariseFixPlan(plan) {
139
139
  ].filter(Boolean);
140
140
  return `Attempting ${parts.join(' and ')} across ${plan.files.length} file${plan.files.length === 1 ? '' : 's'}.`;
141
141
  }
142
+ /**
143
+ * What the agent actually did, in one line.
144
+ *
145
+ * A fix that changes nothing is the hardest outcome to act on, because the
146
+ * summary that reports it — "the run did not finish", "nothing was changed" —
147
+ * says what did not happen and never what did. Debugging one such run through
148
+ * CI cost two releases and forty minutes of guessing at whether the agent
149
+ * could not find the file, could not write, or was being refused a tool.
150
+ *
151
+ * Counting the action log answers that in the message itself. Failures are
152
+ * called out separately from successes, and one failing detail is quoted,
153
+ * because a refusal reason is usually the whole explanation.
154
+ */
155
+ export function describeAgentActivity(actions) {
156
+ if (actions.length === 0)
157
+ return 'It made no tool calls at all.';
158
+ const byType = new Map();
159
+ for (const action of actions) {
160
+ const tally = byType.get(action.type) ?? { ok: 0, failed: 0 };
161
+ if (action.result === 'error')
162
+ tally.failed++;
163
+ else
164
+ tally.ok++;
165
+ byType.set(action.type, tally);
166
+ }
167
+ const parts = [...byType.entries()]
168
+ .sort((a, b) => (b[1].ok + b[1].failed) - (a[1].ok + a[1].failed))
169
+ .map(([type, { ok, failed }]) => (failed ? `${ok + failed} ${type} (${failed} failed)` : `${ok} ${type}`));
170
+ const firstFailure = actions.find(a => a.result === 'error' && a.details);
171
+ const why = firstFailure ? ` First failure: ${firstFailure.details.slice(0, 200)}` : '';
172
+ return `It made ${actions.length} tool call${actions.length === 1 ? '' : 's'}: ${parts.join(', ')}.${why}`;
173
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "2.22.0";
1
+ export declare const VERSION = "2.24.0";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
2
2
  // Baked from package.json at build time so the bun-compiled binary reports
3
3
  // the right version (it has no package.json on disk to read at runtime).
4
- export const VERSION = '2.22.0';
4
+ export const VERSION = '2.24.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.22.0",
3
+ "version": "2.24.0",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",