codeep 2.1.3 → 2.1.4

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
@@ -181,7 +181,7 @@ Codeep works as a **full AI coding agent** that autonomously:
181
181
  ### Context Persistence
182
182
  - **Save conversations** - Continue where you left off
183
183
  - **Per-project context** - Each project maintains its own history
184
- - **Automatic summarization** - Old messages are summarized to save space
184
+ - **Automatic summarization** - When prior history overflows the agent's context budget, the dropped (oldest) messages are condensed into a short recap (decisions, constraints, unfinished threads) instead of being silently truncated — so long sessions don't forget how they started. One cheap LLM call, made only on overflow and cached per session; opt out with `autoSummarizeHistory: false` (`/settings`)
185
185
 
186
186
  ### Web & MCP Tools
187
187
  - Agent can fetch documentation and web content
@@ -27,6 +27,11 @@ interface ConfigSchema {
27
27
  * small background API call (uses the active model) once per session.
28
28
  * Default true; set false to avoid any unsolicited API calls. */
29
29
  autoSessionTitle: boolean;
30
+ /** When prior chat history overflows the agent's context budget, summarize
31
+ * the dropped (oldest) messages via one LLM call instead of silently
32
+ * discarding them — so long sessions keep early decisions/constraints.
33
+ * Default true; set false to fall back to plain truncation (no extra call). */
34
+ autoSummarizeHistory: boolean;
30
35
  /** Absolute workspace roots whose project-local `.codeep/hooks/*` the user
31
36
  * has approved to run. Untrusted projects' hooks are skipped (a cloned repo
32
37
  * can't execute shell on first tool call). Granted via `/hooks trust`. */
@@ -167,6 +167,7 @@ function createConfig() {
167
167
  language: 'en',
168
168
  autoSave: true,
169
169
  autoSessionTitle: true,
170
+ autoSummarizeHistory: true,
170
171
  trustedHookProjects: [],
171
172
  currentSessionId: '',
172
173
  temperature: 0.7,
@@ -11,7 +11,7 @@ const debug = (...args) => {
11
11
  }
12
12
  };
13
13
  // Import chat layer (prompt building + API calls)
14
- import { agentChat, getAgentSystemPrompt, getFallbackSystemPrompt, loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent, } from './agentChat.js';
14
+ import { agentChat, getAgentSystemPrompt, getFallbackSystemPrompt, loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent, summarizeEarlierHistory, } from './agentChat.js';
15
15
  import { ApiError } from '../api/index.js';
16
16
  export { loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent };
17
17
  /**
@@ -242,7 +242,13 @@ export async function runAgent(prompt, projectContext, options = {}) {
242
242
  if (taskCtx) {
243
243
  systemPrompt += taskCtx;
244
244
  }
245
- // Inject prior chat session context
245
+ // Inject prior chat session context. When the history overflows the budget,
246
+ // prepend an LLM recap of the dropped (oldest) messages so long sessions
247
+ // keep early decisions/constraints, then the recent messages verbatim.
248
+ const earlierSummary = await summarizeEarlierHistory(opts.chatHistory);
249
+ if (earlierSummary) {
250
+ systemPrompt += earlierSummary;
251
+ }
246
252
  const chatHistoryStr = formatChatHistoryForAgent(opts.chatHistory);
247
253
  if (chatHistoryStr) {
248
254
  systemPrompt += chatHistoryStr;
@@ -52,6 +52,21 @@ export declare function formatChatHistoryForAgent(history?: Array<{
52
52
  role: 'user' | 'assistant';
53
53
  content: string;
54
54
  }>, maxChars?: number): string;
55
+ /**
56
+ * Summarize the OVERFLOW that `formatChatHistoryForAgent` drops. When prior
57
+ * history exceeds `maxChars`, that function keeps only the most recent messages
58
+ * and silently discards the older ones — losing early decisions/constraints on
59
+ * long sessions. This condenses those dropped messages into a short recap that
60
+ * the caller prepends *before* the recent verbatim history.
61
+ *
62
+ * Returns '' when: opted out (`autoSummarizeHistory === false`), nothing
63
+ * overflows, or the summarization call fails (graceful fallback — the recent
64
+ * history still goes in, we just don't add a recap).
65
+ */
66
+ export declare function summarizeEarlierHistory(history?: Array<{
67
+ role: 'user' | 'assistant';
68
+ content: string;
69
+ }>, maxChars?: number): Promise<string>;
55
70
  export declare function getAgentSystemPrompt(projectContext: ProjectContext): string;
56
71
  export declare function getFallbackSystemPrompt(projectContext: ProjectContext, additionalTools?: AdditionalToolDef[]): string;
57
72
  /**
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { existsSync, readFileSync, writeFileSync } from 'fs';
15
15
  import { join } from 'path';
16
+ import { createHash } from 'crypto';
16
17
  import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
17
18
  import { loadProjectIntelligence, generateContextFromIntelligence } from './projectIntelligence.js';
18
19
  import { syncProgress, generateProjectId } from './codeepCloud.js';
@@ -180,6 +181,84 @@ export function formatChatHistoryForAgent(history, maxChars = 16000) {
180
181
  const lines = selected.map(m => `**${m.role === 'user' ? 'User' : 'Assistant'}:** ${m.content}`).join('\n\n');
181
182
  return `\n\n## Prior Conversation Context\nThe following is the recent chat history from this session. Use it as background context to understand the user's intent, but focus on completing the current task.\n\n${lines}`;
182
183
  }
184
+ // Same noise filter formatChatHistoryForAgent uses — kept in sync so the two
185
+ // functions agree on which messages are "real" conversation.
186
+ function filterAgentHistory(history) {
187
+ return history.filter(m => {
188
+ const content = m.content.trimStart();
189
+ if (content.startsWith('[AGENT]') || content.startsWith('[DRY RUN]'))
190
+ return false;
191
+ if (content.startsWith('Agent completed') || content.startsWith('Agent failed') || content.startsWith('Agent stopped'))
192
+ return false;
193
+ return true;
194
+ });
195
+ }
196
+ // Cache summaries by a hash of the dropped messages, so re-running the agent in
197
+ // the same session (same overflow) doesn't re-summarize on every task.
198
+ const earlierSummaryCache = new Map();
199
+ /**
200
+ * Summarize the OVERFLOW that `formatChatHistoryForAgent` drops. When prior
201
+ * history exceeds `maxChars`, that function keeps only the most recent messages
202
+ * and silently discards the older ones — losing early decisions/constraints on
203
+ * long sessions. This condenses those dropped messages into a short recap that
204
+ * the caller prepends *before* the recent verbatim history.
205
+ *
206
+ * Returns '' when: opted out (`autoSummarizeHistory === false`), nothing
207
+ * overflows, or the summarization call fails (graceful fallback — the recent
208
+ * history still goes in, we just don't add a recap).
209
+ */
210
+ export async function summarizeEarlierHistory(history, maxChars = 16000) {
211
+ if (config.get('autoSummarizeHistory') === false)
212
+ return '';
213
+ if (!history || history.length === 0)
214
+ return '';
215
+ const filtered = filterAgentHistory(history);
216
+ if (filtered.length === 0)
217
+ return '';
218
+ // Mirror formatChatHistoryForAgent's newest→oldest budget walk to find which
219
+ // messages it KEEPS; everything older than the oldest kept message is dropped.
220
+ let totalChars = 0;
221
+ let firstKept = filtered.length;
222
+ for (let i = filtered.length - 1; i >= 0; i--) {
223
+ const entry = `${filtered[i].role === 'user' ? 'User' : 'Assistant'}: ${filtered[i].content}`;
224
+ if (totalChars + entry.length > maxChars && firstKept < filtered.length)
225
+ break;
226
+ if (entry.length > maxChars) {
227
+ firstKept = i;
228
+ break;
229
+ }
230
+ firstKept = i;
231
+ totalChars += entry.length;
232
+ }
233
+ const dropped = filtered.slice(0, firstKept);
234
+ if (dropped.length === 0)
235
+ return '';
236
+ const key = createHash('sha256')
237
+ .update(dropped.map(m => `${m.role}:${m.content}`).join(''))
238
+ .digest('hex');
239
+ const cached = earlierSummaryCache.get(key);
240
+ if (cached)
241
+ return cached;
242
+ // Compact transcript of the dropped messages, capped so the summarization
243
+ // prompt stays cheap even when a lot has overflowed.
244
+ const transcript = dropped
245
+ .map(m => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content.replace(/\s+/g, ' ').slice(0, 600)}`)
246
+ .join('\n')
247
+ .slice(0, 24000);
248
+ const system = 'You are condensing the EARLIER part of an ongoing coding session that no longer fits the context window. Summarize what happened in 3-6 sentences: concrete decisions made, constraints/requirements stated, files or APIs involved, and anything still unfinished. Past tense, no preamble, no bullet headers — just the recap.';
249
+ try {
250
+ const { chat } = await import('../api/index.js');
251
+ const summary = (await chat(transcript, [{ role: 'system', content: system }])).trim();
252
+ if (!summary)
253
+ return '';
254
+ const block = `\n\n## Earlier Conversation (summarized)\nThe earlier part of this session was condensed to fit context. Treat it as established background:\n\n${summary}`;
255
+ earlierSummaryCache.set(key, block);
256
+ return block;
257
+ }
258
+ catch {
259
+ return ''; // graceful — recent verbatim history still gets injected
260
+ }
261
+ }
183
262
  export function getAgentSystemPrompt(projectContext) {
184
263
  const root = projectContext.root || process.cwd();
185
264
  // State the real underlying model/provider so "which model are you"
@@ -74,6 +74,37 @@ const ALLOWED_COMMANDS = new Set([
74
74
  // HTTP tools
75
75
  'http', 'https',
76
76
  ]);
77
+ // Interpreter flags that execute inline code straight from the command line.
78
+ // Without this check, a whitelisted runtime (`node`, `python`, …) becomes
79
+ // arbitrary code execution — `node -e "<anything>"`, `python -c "<anything>"` —
80
+ // bypassing the command whitelist entirely. File execution (`node app.js`)
81
+ // stays allowed; only the eval flags are blocked.
82
+ const INLINE_EVAL_SHORT = {
83
+ node: ['e', 'p'], bun: ['e'], python: ['c'], python3: ['c'], php: ['r'], ruby: ['e'], perl: ['e', 'E'],
84
+ };
85
+ const INLINE_EVAL_LONG = {
86
+ node: ['--eval', '--print'], deno: ['eval'], bun: ['--eval'],
87
+ };
88
+ function hasInlineEval(command, args) {
89
+ const short = INLINE_EVAL_SHORT[command] ?? [];
90
+ const long = INLINE_EVAL_LONG[command] ?? [];
91
+ if (short.length === 0 && long.length === 0)
92
+ return false;
93
+ for (const arg of args) {
94
+ if (arg.startsWith('--')) {
95
+ if (long.includes(arg.split('=')[0]))
96
+ return true; // --eval / --print(=...)
97
+ }
98
+ else if (arg.length > 1 && arg.startsWith('-')) {
99
+ if (arg.slice(1).split('').some((l) => short.includes(l)))
100
+ return true; // -e, -c, -pe …
101
+ }
102
+ else if (long.includes(arg)) {
103
+ return true; // bare subcommand, e.g. `deno eval`
104
+ }
105
+ }
106
+ return false;
107
+ }
77
108
  /**
78
109
  * Validate if a command is safe to execute
79
110
  */
@@ -86,6 +117,11 @@ export function validateCommand(command, args, options) {
86
117
  if (!ALLOWED_COMMANDS.has(command)) {
87
118
  return { valid: false, reason: `Command '${command}' is not in the allowed list` };
88
119
  }
120
+ // Block inline-code execution that would turn a whitelisted interpreter into
121
+ // arbitrary code execution (the whitelist alone doesn't stop `node -e "…"`).
122
+ if (hasInlineEval(command, args)) {
123
+ return { valid: false, reason: `Inline code execution via '${command}' (e.g. -e/-c/--eval) is not allowed in agent mode — put the code in a file and run that, or run it yourself.` };
124
+ }
89
125
  // Check full command string against dangerous patterns
90
126
  const fullCommand = `${command} ${args.join(' ')}`;
91
127
  for (const pattern of BLOCKED_PATTERNS) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
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",