codeep 1.2.69 → 1.2.70

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/dist/api/index.js CHANGED
@@ -293,7 +293,7 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
293
293
  const timeoutId = setTimeout(() => { timedOut = true; controller.abort(); }, timeout);
294
294
  // Listen to external abort signal if provided (user cancel)
295
295
  if (abortSignal) {
296
- abortSignal.addEventListener('abort', () => controller.abort());
296
+ abortSignal.addEventListener('abort', () => controller.abort(), { once: true });
297
297
  }
298
298
  // Build headers based on auth type
299
299
  const headers = {
@@ -413,7 +413,7 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
413
413
  const timeoutId = setTimeout(() => { timedOut = true; controller.abort(); }, timeout);
414
414
  // Listen to external abort signal if provided (user cancel)
415
415
  if (abortSignal) {
416
- abortSignal.addEventListener('abort', () => controller.abort());
416
+ abortSignal.addEventListener('abort', () => controller.abort(), { once: true });
417
417
  }
418
418
  // Build headers based on auth type
419
419
  const headers = {
@@ -9,6 +9,16 @@ import { chat } from '../api/index.js';
9
9
  import { runAgent } from '../utils/agent.js';
10
10
  import { config, autoSaveSession } from '../config/index.js';
11
11
  import { getGitStatus } from '../utils/git.js';
12
+ function getActionType(toolName) {
13
+ return toolName.includes('write') ? 'write' :
14
+ toolName.includes('edit') ? 'edit' :
15
+ toolName.includes('read') ? 'read' :
16
+ toolName.includes('delete') ? 'delete' :
17
+ toolName.includes('list') ? 'list' :
18
+ toolName.includes('search') || toolName.includes('grep') ? 'search' :
19
+ toolName.includes('mkdir') ? 'mkdir' :
20
+ toolName.includes('fetch') ? 'fetch' : 'command';
21
+ }
12
22
  // ─── Dangerous tool detection ────────────────────────────────────────────────
13
23
  const DANGEROUS_TOOLS = ['write', 'edit', 'delete', 'command', 'execute', 'shell', 'rm', 'mv'];
14
24
  export function isDangerousTool(toolName, parameters) {
@@ -154,14 +164,7 @@ export async function executeAgentTask(task, dryRun, ctx) {
154
164
  const target = tool.parameters.path ||
155
165
  tool.parameters.command ||
156
166
  tool.parameters.pattern || '';
157
- const actionType = toolName.includes('write') ? 'write' :
158
- toolName.includes('edit') ? 'edit' :
159
- toolName.includes('read') ? 'read' :
160
- toolName.includes('delete') ? 'delete' :
161
- toolName.includes('list') ? 'list' :
162
- toolName.includes('search') || toolName.includes('grep') ? 'search' :
163
- toolName.includes('mkdir') ? 'mkdir' :
164
- toolName.includes('fetch') ? 'fetch' : 'command';
167
+ const actionType = getActionType(toolName);
165
168
  const shortTarget = target.length > 50 ? '...' + target.slice(-47) : target;
166
169
  app.setAgentThinking(`${actionType}: ${shortTarget}`);
167
170
  if (actionType === 'write' && tool.parameters.content) {
@@ -247,14 +250,7 @@ export async function executeAgentTask(task, dryRun, ctx) {
247
250
  onToolResult: (result, toolCall) => {
248
251
  const toolName = toolCall.tool.toLowerCase();
249
252
  const target = toolCall.parameters.path || toolCall.parameters.command || '';
250
- const actionType = toolName.includes('write') ? 'write' :
251
- toolName.includes('edit') ? 'edit' :
252
- toolName.includes('read') ? 'read' :
253
- toolName.includes('delete') ? 'delete' :
254
- toolName.includes('list') ? 'list' :
255
- toolName.includes('search') || toolName.includes('grep') ? 'search' :
256
- toolName.includes('mkdir') ? 'mkdir' :
257
- toolName.includes('fetch') ? 'fetch' : 'command';
253
+ const actionType = getActionType(toolName);
258
254
  app.updateAgentProgress(0, {
259
255
  type: actionType,
260
256
  target,
@@ -324,11 +320,13 @@ export async function executeAgentTask(task, dryRun, ctx) {
324
320
  app.addMessage({ role: 'assistant', content: 'Agent stopped by user.' });
325
321
  }
326
322
  else {
327
- const failLines = [];
328
- if (result.finalResponse)
329
- failLines.push(result.finalResponse);
330
- failLines.push(`**Agent stopped**: ${result.error || 'Unknown error'}`);
331
- app.addMessage({ role: 'assistant', content: failLines.join('\n\n') });
323
+ // Show the agent's summary if available, with error details below
324
+ if (result.finalResponse) {
325
+ app.addMessage({ role: 'assistant', content: result.finalResponse });
326
+ }
327
+ else {
328
+ app.addMessage({ role: 'assistant', content: `Agent could not complete the task: ${result.error || 'Unknown error'}` });
329
+ }
332
330
  }
333
331
  autoSaveSession(app.getMessages(), ctx.projectPath);
334
332
  }
@@ -16,7 +16,7 @@ export { loadProjectRules, formatChatHistoryForAgent };
16
16
  * Calculate dynamic timeout based on task complexity
17
17
  * Complex tasks (creating pages, multiple files) need more time
18
18
  */
19
- function calculateDynamicTimeout(prompt, iteration, baseTimeout) {
19
+ function calculateDynamicTimeout(iteration, baseTimeout) {
20
20
  // Simple approach: just use base timeout with small multiplier for later iterations
21
21
  // Complex calculations were causing more problems than they solved
22
22
  let multiplier = 1.0;
@@ -224,11 +224,11 @@ export async function runAgent(prompt, projectContext, options = {}) {
224
224
  if (compressed !== messages) {
225
225
  messages.length = 0;
226
226
  messages.push(...compressed);
227
- opts.onIteration?.(iteration, `Context compressed (${compressed.length} messages kept)`);
227
+ opts.onIteration?.(iteration, `Context compressed to save memory — continuing with last ${compressed.length} messages`);
228
228
  }
229
229
  debug(`Starting iteration ${iteration}/${opts.maxIterations}, actions: ${actions.length}`);
230
230
  // Calculate dynamic timeout based on task complexity
231
- const dynamicTimeout = calculateDynamicTimeout(prompt, iteration, baseTimeout);
231
+ const dynamicTimeout = calculateDynamicTimeout(iteration, baseTimeout);
232
232
  debug(`Using timeout: ${dynamicTimeout}ms (base: ${baseTimeout}ms)`);
233
233
  // Get AI response with retry logic for timeouts
234
234
  let chatResponse = null;
@@ -283,23 +283,37 @@ export async function runAgent(prompt, projectContext, options = {}) {
283
283
  await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
284
284
  continue;
285
285
  }
286
- // Retry on transient errors: rate-limit, server errors, network failures
286
+ // All non-abort errors are retryable retry with backoff
287
+ retryCount++;
287
288
  const isRateLimit = err.message.includes('429');
288
289
  const isServerError = err.message.includes('500') || err.message.includes('502') || err.message.includes('503') || err.message.includes('529');
289
- const isNetworkError = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'EPIPE', 'EAI_AGAIN'].some(code => err.message.includes(code));
290
- if (isRateLimit || isServerError || isNetworkError) {
291
- retryCount++;
292
- const waitSec = Math.min(5 * retryCount, 30); // 5s, 10s, 15s max 30s
293
- const code = isRateLimit ? '429' : isServerError ? '5xx' : 'network';
294
- debug(`${code} error (retry ${retryCount}/${maxTimeoutRetries}), waiting ${waitSec}s`);
295
- opts.onIteration?.(iteration, `Server error (${code}), retrying in ${waitSec}s... (${retryCount}/${maxTimeoutRetries})`);
296
- if (retryCount >= maxTimeoutRetries) {
297
- throw error; // Give up after max retries
290
+ const code = isRateLimit ? '429' : isServerError ? '5xx' : 'error';
291
+ const waitSec = Math.min(5 * retryCount, 30);
292
+ debug(`${code} (retry ${retryCount}/${maxTimeoutRetries}): ${err.message}`);
293
+ opts.onIteration?.(iteration, `API ${code}, retrying in ${waitSec}s... (${retryCount}/${maxTimeoutRetries})`);
294
+ if (retryCount >= maxTimeoutRetries) {
295
+ // Don't throw skip this iteration like timeouts do
296
+ consecutiveTimeouts++;
297
+ if (consecutiveTimeouts >= maxConsecutiveTimeouts) {
298
+ result = {
299
+ success: false,
300
+ iterations: iteration,
301
+ actions,
302
+ finalResponse: actions.length > 0
303
+ ? `Agent made progress (${actions.length} actions) but API errors prevented completion. You can continue by running the agent again.`
304
+ : 'Agent could not complete the task due to repeated API errors. Check your API key and network connection.',
305
+ error: `API failed after ${maxTimeoutRetries} retries: ${err.message}`,
306
+ };
307
+ return result;
298
308
  }
299
- await new Promise(resolve => setTimeout(resolve, waitSec * 1000));
300
- continue;
309
+ messages.push({
310
+ role: 'user',
311
+ content: 'The previous request failed. Please continue with the task.'
312
+ });
313
+ break; // Break retry loop, continue main loop
301
314
  }
302
- throw error;
315
+ await new Promise(resolve => setTimeout(resolve, waitSec * 1000));
316
+ continue;
303
317
  }
304
318
  }
305
319
  // If we broke out due to max retries without a response, continue to next iteration
@@ -326,7 +340,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
326
340
  .replace(/<arg_key>[\s\S]*?<\/arg_value>/gi, '')
327
341
  .replace(/Tool parameters:[\s\S]*?(?=\n\n|$)/gi, '')
328
342
  .replace(/\{'path'[\s\S]*?\}/g, '')
329
- .replace(/```[\s\S]*?```/g, '')
343
+ .replace(/```(?:json|tool_call)?\s*\{[\s\S]*?\}\s*```/g, '') // Only strip tool-call-like code blocks
330
344
  .trim();
331
345
  // Check if model indicates it wants to continue (incomplete response)
332
346
  const continueIndicators = [
@@ -426,11 +440,14 @@ export async function runAgent(prompt, projectContext, options = {}) {
426
440
  else {
427
441
  toolResults.push(`Tool ${toolCall.tool} failed:\n${toolResult.error || 'Unknown error'}`);
428
442
  }
429
- // Invalidate read cache when a file is written/edited
443
+ // Invalidate read cache when files may have changed
430
444
  if ((toolCall.tool === 'write_file' || toolCall.tool === 'edit_file') && toolResult.success) {
431
445
  const filePath = toolCall.parameters.path || '';
432
446
  readCache.delete(filePath);
433
447
  }
448
+ else if (toolCall.tool === 'execute_command' && toolResult.success) {
449
+ readCache.clear(); // Commands can modify arbitrary files
450
+ }
434
451
  }
435
452
  // Add tool results to messages
436
453
  messages.push({
@@ -543,7 +560,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
543
560
  const actionLog = createActionLog(toolCall, toolResult);
544
561
  actions.push(actionLog);
545
562
  if (toolResult.success) {
546
- fixResults.push(`Tool ${toolCall.tool} succeeded:\n${toolResult.output}`);
563
+ const truncated = truncateToolResult(toolResult.output, toolCall.tool);
564
+ fixResults.push(`Tool ${toolCall.tool} succeeded:\n${truncated}`);
547
565
  }
548
566
  else {
549
567
  fixResults.push(`Tool ${toolCall.tool} failed:\n${toolResult.error || 'Unknown error'}`);
@@ -154,7 +154,7 @@ export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dy
154
154
  let isTimeout = false;
155
155
  const timeout = setTimeout(() => { isTimeout = true; controller.abort(); }, timeoutMs);
156
156
  if (abortSignal) {
157
- abortSignal.addEventListener('abort', () => { isTimeout = false; controller.abort(); });
157
+ abortSignal.addEventListener('abort', () => { isTimeout = false; controller.abort(); }, { once: true });
158
158
  }
159
159
  const headers = { 'Content-Type': 'application/json' };
160
160
  if (authHeader === 'Bearer') {
@@ -268,7 +268,7 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
268
268
  let isTimeout = false;
269
269
  const timeout = setTimeout(() => { isTimeout = true; controller.abort(); }, timeoutMs);
270
270
  if (abortSignal) {
271
- abortSignal.addEventListener('abort', () => { isTimeout = false; controller.abort(); });
271
+ abortSignal.addEventListener('abort', () => { isTimeout = false; controller.abort(); }, { once: true });
272
272
  }
273
273
  const headers = { 'Content-Type': 'application/json' };
274
274
  if (authHeader === 'Bearer') {
@@ -112,8 +112,8 @@ export function validateCommand(command, args, options) {
112
112
  }
113
113
  // Special validation for rm command
114
114
  if (command === 'rm') {
115
- const hasRecursive = args.some(a => a.includes('r'));
116
- const hasForce = args.some(a => a.includes('f'));
115
+ const hasRecursive = args.some(a => a.startsWith('-') && a.includes('r'));
116
+ const hasForce = args.some(a => a.startsWith('-') && a.includes('f'));
117
117
  if (hasRecursive && hasForce) {
118
118
  // rm -rf requires extra validation
119
119
  const paths = args.filter(a => !a.startsWith('-'));
@@ -176,8 +176,8 @@ export async function executeTool(toolCall, projectRoot) {
176
176
  return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
177
177
  }
178
178
  if (content === undefined || content === null) {
179
- debug('write_file: content was undefined, using placeholder');
180
- content = '<!-- Content was not provided -->\n';
179
+ debug('write_file failed: content was undefined');
180
+ return { success: false, output: '', error: 'File content was empty or truncated by the API. Try writing a smaller file or splitting into multiple writes.', tool, parameters };
181
181
  }
182
182
  const validation = validatePath(path, projectRoot);
183
183
  if (!validation.valid)
@@ -390,25 +390,26 @@ export async function runLintVerification(projectRoot, timeout = 60000) {
390
390
  export async function runAllVerifications(projectRoot, options = {}) {
391
391
  const opts = { ...DEFAULT_OPTIONS, ...options };
392
392
  const results = [];
393
- // Run typecheck first (fastest feedback)
394
- if (opts.runTypecheck) {
395
- const result = await runTypecheckVerification(projectRoot, opts.timeout);
396
- if (result)
397
- results.push(result);
393
+ // Run typecheck and lint in parallel (independent checks)
394
+ const parallel = [];
395
+ if (opts.runTypecheck)
396
+ parallel.push(runTypecheckVerification(projectRoot, opts.timeout));
397
+ if (opts.runLint)
398
+ parallel.push(runLintVerification(projectRoot, opts.timeout));
399
+ if (parallel.length > 0) {
400
+ const parallelResults = await Promise.all(parallel);
401
+ for (const r of parallelResults) {
402
+ if (r)
403
+ results.push(r);
404
+ }
398
405
  }
399
- // Run build
406
+ // Run build after typecheck/lint (may depend on them)
400
407
  if (opts.runBuild) {
401
408
  const result = await runBuildVerification(projectRoot, opts.timeout);
402
409
  if (result)
403
410
  results.push(result);
404
411
  }
405
- // Run lint
406
- if (opts.runLint) {
407
- const result = await runLintVerification(projectRoot, opts.timeout);
408
- if (result)
409
- results.push(result);
410
- }
411
- // Run tests last (slowest)
412
+ // Run tests last (slowest, depends on build)
412
413
  if (opts.runTest) {
413
414
  const result = await runTestVerification(projectRoot, opts.timeout);
414
415
  if (result)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.2.69",
3
+ "version": "1.2.70",
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",