codeep 1.3.41 → 2.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.md +208 -0
- package/dist/acp/commands.js +770 -7
- package/dist/acp/protocol.d.ts +11 -2
- package/dist/acp/server.js +179 -11
- package/dist/acp/session.d.ts +3 -0
- package/dist/acp/session.js +5 -0
- package/dist/api/index.js +39 -6
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +46 -1
- package/dist/config/providers.js +76 -1
- package/dist/renderer/App.d.ts +12 -0
- package/dist/renderer/App.js +96 -4
- package/dist/renderer/agentExecution.js +5 -0
- package/dist/renderer/commands.js +348 -2
- package/dist/renderer/components/Login.d.ts +1 -0
- package/dist/renderer/components/Login.js +24 -9
- package/dist/renderer/handlers.d.ts +11 -1
- package/dist/renderer/handlers.js +30 -0
- package/dist/renderer/main.js +73 -0
- package/dist/utils/agent.d.ts +17 -0
- package/dist/utils/agent.js +91 -7
- package/dist/utils/agentChat.d.ts +10 -2
- package/dist/utils/agentChat.js +48 -9
- package/dist/utils/agentStream.js +6 -2
- package/dist/utils/checkpoints.d.ts +93 -0
- package/dist/utils/checkpoints.js +205 -0
- package/dist/utils/context.d.ts +24 -0
- package/dist/utils/context.js +57 -0
- package/dist/utils/customCommands.d.ts +62 -0
- package/dist/utils/customCommands.js +201 -0
- package/dist/utils/hooks.d.ts +97 -0
- package/dist/utils/hooks.js +223 -0
- package/dist/utils/mcpClient.d.ts +229 -0
- package/dist/utils/mcpClient.js +497 -0
- package/dist/utils/mcpConfig.d.ts +55 -0
- package/dist/utils/mcpConfig.js +177 -0
- package/dist/utils/mcpMarketplace.d.ts +49 -0
- package/dist/utils/mcpMarketplace.js +175 -0
- package/dist/utils/mcpRegistry.d.ts +129 -0
- package/dist/utils/mcpRegistry.js +427 -0
- package/dist/utils/mcpSamplingBridge.d.ts +32 -0
- package/dist/utils/mcpSamplingBridge.js +88 -0
- package/dist/utils/mcpStreamableHttp.d.ts +65 -0
- package/dist/utils/mcpStreamableHttp.js +207 -0
- package/dist/utils/openrouterPrefs.d.ts +36 -0
- package/dist/utils/openrouterPrefs.js +83 -0
- package/dist/utils/skillBundles.d.ts +84 -0
- package/dist/utils/skillBundles.js +257 -0
- package/dist/utils/skillBundlesCloud.d.ts +66 -0
- package/dist/utils/skillBundlesCloud.js +196 -0
- package/dist/utils/tokenTracker.d.ts +14 -2
- package/dist/utils/tokenTracker.js +59 -45
- package/dist/utils/toolExecution.d.ts +17 -1
- package/dist/utils/toolExecution.js +184 -6
- package/dist/utils/tools.d.ts +22 -6
- package/dist/utils/tools.js +83 -8
- package/package.json +3 -2
- package/bin/codeep-macos-arm64 +0 -0
- package/bin/codeep-macos-x64 +0 -0
package/dist/utils/agent.js
CHANGED
|
@@ -165,10 +165,66 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
165
165
|
const protocol = config.get('protocol');
|
|
166
166
|
const providerId = config.get('provider');
|
|
167
167
|
const useNativeTools = supportsNativeTools(providerId, protocol);
|
|
168
|
+
// Fetch the MCP tool catalog once per agent run. The session id keys into
|
|
169
|
+
// mcpRegistry; if no MCP servers are registered (or mcpSessionId is unset,
|
|
170
|
+
// e.g. TUI mode) we get back an empty array and the agent behaves as
|
|
171
|
+
// before. We do this before building the system prompt so the fallback
|
|
172
|
+
// text path can include MCP tools in its catalog too.
|
|
173
|
+
//
|
|
174
|
+
// We also append per-server "virtual" tools that wrap resource_list /
|
|
175
|
+
// resource_read / prompt_list / prompt_get so the agent can discover and
|
|
176
|
+
// pull MCP resources & prompts without the user having to type `/mcp
|
|
177
|
+
// read <uri>` manually. Servers that don't expose resources or prompts
|
|
178
|
+
// get no virtual tools — the wrappers are only emitted where useful.
|
|
179
|
+
let mcpToolDefs = [];
|
|
180
|
+
if (opts.mcpSessionId) {
|
|
181
|
+
try {
|
|
182
|
+
const { getSessionTools, getSessionVirtualTools } = await import('./mcpRegistry.js');
|
|
183
|
+
const [registered, virtuals] = await Promise.all([
|
|
184
|
+
getSessionTools(opts.mcpSessionId),
|
|
185
|
+
getSessionVirtualTools(opts.mcpSessionId),
|
|
186
|
+
]);
|
|
187
|
+
mcpToolDefs = [...registered, ...virtuals].map(t => ({
|
|
188
|
+
name: t.agentName,
|
|
189
|
+
description: t.description,
|
|
190
|
+
inputSchema: t.inputSchema,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Don't let a registry blip kill the whole agent run.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Skill bundles — structured `.codeep/skills/<name>/SKILL.md` directories
|
|
198
|
+
// the agent can discover and invoke via the `invoke_skill` tool. We just
|
|
199
|
+
// add the tool def here; the catalog block is appended to systemPrompt
|
|
200
|
+
// below alongside project rules / progress / etc. so we don't clobber
|
|
201
|
+
// those.
|
|
202
|
+
let skillCatalogBlock = '';
|
|
203
|
+
try {
|
|
204
|
+
const { loadSkillBundles, formatBundlesForSysprompt } = await import('./skillBundles.js');
|
|
205
|
+
const bundles = loadSkillBundles(projectContext.root);
|
|
206
|
+
if (bundles.length > 0) {
|
|
207
|
+
mcpToolDefs.push({
|
|
208
|
+
name: 'invoke_skill',
|
|
209
|
+
description: 'Invoke a Codeep skill bundle (curated workflow). Returns the SKILL.md body — follow its instructions step by step. Use when the user\'s request matches a skill\'s purpose.',
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
name: { type: 'string', description: 'Skill name from the catalog (e.g. "deploy").' },
|
|
214
|
+
},
|
|
215
|
+
required: ['name'],
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
skillCatalogBlock = formatBundlesForSysprompt(bundles);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Skill loading failure shouldn't fail the whole agent run.
|
|
223
|
+
}
|
|
168
224
|
// Build system prompt - use fallback format if native tools not supported
|
|
169
225
|
let systemPrompt = useNativeTools
|
|
170
226
|
? getAgentSystemPrompt(projectContext)
|
|
171
|
-
: getFallbackSystemPrompt(projectContext);
|
|
227
|
+
: getFallbackSystemPrompt(projectContext, mcpToolDefs);
|
|
172
228
|
// Inject project rules (from .codeep/rules.md or CODEEP.md)
|
|
173
229
|
const projectRules = loadProjectRules(projectContext.root);
|
|
174
230
|
if (projectRules) {
|
|
@@ -191,6 +247,12 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
191
247
|
if (chatHistoryStr) {
|
|
192
248
|
systemPrompt += chatHistoryStr;
|
|
193
249
|
}
|
|
250
|
+
// Skill bundles catalog goes last — closest to the user prompt so the
|
|
251
|
+
// model is most likely to remember the available skills when matching
|
|
252
|
+
// intent. Empty string when there are none.
|
|
253
|
+
if (skillCatalogBlock) {
|
|
254
|
+
systemPrompt += '\n\n' + skillCatalogBlock;
|
|
255
|
+
}
|
|
194
256
|
// Initial user message with optional task plan
|
|
195
257
|
let initialPrompt = prompt;
|
|
196
258
|
if (taskPlan) {
|
|
@@ -282,13 +344,35 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
282
344
|
// Calculate dynamic timeout based on task complexity
|
|
283
345
|
const dynamicTimeout = calculateDynamicTimeout(iteration, baseTimeout);
|
|
284
346
|
debug(`Using timeout: ${dynamicTimeout}ms (base: ${baseTimeout}ms)`);
|
|
347
|
+
// Refresh MCP tool list if a server flagged its catalog as changed
|
|
348
|
+
// (e.g. via `tools/list_changed` notification, or after an
|
|
349
|
+
// auto-restart). This keeps the agent in sync mid-run instead of
|
|
350
|
+
// requiring a session restart to see new tools.
|
|
351
|
+
if (opts.mcpSessionId) {
|
|
352
|
+
try {
|
|
353
|
+
const { consumeSessionCatalogChanges, getSessionTools } = await import('./mcpRegistry.js');
|
|
354
|
+
const dirty = consumeSessionCatalogChanges(opts.mcpSessionId);
|
|
355
|
+
if (dirty.has('tools')) {
|
|
356
|
+
const refreshed = await getSessionTools(opts.mcpSessionId);
|
|
357
|
+
mcpToolDefs = refreshed.map(t => ({
|
|
358
|
+
name: t.agentName,
|
|
359
|
+
description: t.description,
|
|
360
|
+
inputSchema: t.inputSchema,
|
|
361
|
+
}));
|
|
362
|
+
debug(`MCP tool catalog refreshed mid-run: ${mcpToolDefs.length} tool(s)`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Don't let a refresh hiccup break the iteration.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
285
369
|
// Get AI response with retry logic for timeouts
|
|
286
370
|
let chatResponse = null;
|
|
287
371
|
let retryCount = 0;
|
|
288
372
|
while (true) {
|
|
289
373
|
try {
|
|
290
|
-
chatResponse = await agentChat(messages, systemPrompt, opts.onChunk, opts.abortSignal, dynamicTimeout * (1 + retryCount * 0.5) // Increase timeout on retry
|
|
291
|
-
);
|
|
374
|
+
chatResponse = await agentChat(messages, systemPrompt, opts.onChunk, opts.abortSignal, dynamicTimeout * (1 + retryCount * 0.5), // Increase timeout on retry
|
|
375
|
+
mcpToolDefs);
|
|
292
376
|
consecutiveTimeouts = 0; // Reset consecutive count on success
|
|
293
377
|
consecutiveRateLimits = 0;
|
|
294
378
|
break;
|
|
@@ -559,12 +643,12 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
559
643
|
catch (err) {
|
|
560
644
|
debug('onExecuteCommand callback threw, falling back to local execution:', err);
|
|
561
645
|
// Fallback to local execution if callback throws
|
|
562
|
-
toolResult = await executeTool(toolCall, cwd);
|
|
646
|
+
toolResult = await executeTool(toolCall, cwd, opts.fs, opts.mcpSessionId);
|
|
563
647
|
}
|
|
564
648
|
}
|
|
565
649
|
}
|
|
566
650
|
else {
|
|
567
|
-
toolResult = await executeTool(toolCall, projectContext.root || process.cwd());
|
|
651
|
+
toolResult = await executeTool(toolCall, projectContext.root || process.cwd(), opts.fs, opts.mcpSessionId);
|
|
568
652
|
}
|
|
569
653
|
opts.onToolResult?.(toolResult, toolCall);
|
|
570
654
|
// Log action
|
|
@@ -737,7 +821,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
737
821
|
}
|
|
738
822
|
// Get AI response to fix errors
|
|
739
823
|
try {
|
|
740
|
-
const fixResponse = await agentChat(messages, systemPrompt, opts.onChunk, opts.abortSignal);
|
|
824
|
+
const fixResponse = await agentChat(messages, systemPrompt, opts.onChunk, opts.abortSignal, undefined, mcpToolDefs);
|
|
741
825
|
const { content: fixContent, toolCalls: fixToolCalls } = fixResponse;
|
|
742
826
|
if (fixToolCalls.length === 0) {
|
|
743
827
|
// Agent gave up or thinks it's fixed
|
|
@@ -749,7 +833,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
749
833
|
const fixResults = [];
|
|
750
834
|
for (const toolCall of fixToolCalls) {
|
|
751
835
|
opts.onToolCall?.(toolCall);
|
|
752
|
-
const toolResult = await executeTool(toolCall, projectContext.root || process.cwd());
|
|
836
|
+
const toolResult = await executeTool(toolCall, projectContext.root || process.cwd(), opts.fs, opts.mcpSessionId);
|
|
753
837
|
opts.onToolResult?.(toolResult, toolCall);
|
|
754
838
|
const actionLog = createActionLog(toolCall, toolResult);
|
|
755
839
|
actions.push(actionLog);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { ProjectContext } from './project';
|
|
15
15
|
import { Message } from '../config/index';
|
|
16
|
+
import { AdditionalToolDef } from './tools';
|
|
16
17
|
import type { AgentChatResponse } from './agentStream';
|
|
17
18
|
export type { AgentChatResponse };
|
|
18
19
|
/**
|
|
@@ -52,12 +53,19 @@ export declare function formatChatHistoryForAgent(history?: Array<{
|
|
|
52
53
|
content: string;
|
|
53
54
|
}>, maxChars?: number): string;
|
|
54
55
|
export declare function getAgentSystemPrompt(projectContext: ProjectContext): string;
|
|
55
|
-
export declare function getFallbackSystemPrompt(projectContext: ProjectContext): string;
|
|
56
|
+
export declare function getFallbackSystemPrompt(projectContext: ProjectContext, additionalTools?: AdditionalToolDef[]): string;
|
|
56
57
|
/**
|
|
57
58
|
* Make a chat API call for agent mode with native tool support.
|
|
58
59
|
* Falls back to agentChatFallback() if provider doesn't support tools.
|
|
59
60
|
*/
|
|
60
|
-
export declare function agentChat(messages: Message[], systemPrompt: string, onChunk?: (chunk: string) => void, abortSignal?: AbortSignal, dynamicTimeout?: number
|
|
61
|
+
export declare function agentChat(messages: Message[], systemPrompt: string, onChunk?: (chunk: string) => void, abortSignal?: AbortSignal, dynamicTimeout?: number,
|
|
62
|
+
/**
|
|
63
|
+
* Extra tool definitions appended to the catalog (currently used to
|
|
64
|
+
* surface MCP-registered tools as first-class entries the model can
|
|
65
|
+
* invoke). Optional — built-in tools work the same whether this is
|
|
66
|
+
* omitted or an empty array.
|
|
67
|
+
*/
|
|
68
|
+
additionalTools?: AdditionalToolDef[]): Promise<AgentChatResponse>;
|
|
61
69
|
/**
|
|
62
70
|
* Fallback chat without native tools (text-based tool format)
|
|
63
71
|
*/
|
package/dist/utils/agentChat.js
CHANGED
|
@@ -20,6 +20,7 @@ import { getProviderBaseUrl, getProviderAuthHeader, supportsNativeTools, getEffe
|
|
|
20
20
|
import { recordTokenUsage, extractOpenAIUsage, extractAnthropicUsage } from './tokenTracker.js';
|
|
21
21
|
import { parseOpenAIToolCalls, parseAnthropicToolCalls, parseToolCalls } from './toolParsing.js';
|
|
22
22
|
import { formatToolDefinitions, getOpenAITools, getAnthropicTools } from './tools.js';
|
|
23
|
+
import { readOpenRouterPreferences } from './openrouterPrefs.js';
|
|
23
24
|
import { handleStream, handleOpenAIAgentStream, handleAnthropicAgentStream } from './agentStream.js';
|
|
24
25
|
import { logger } from './logger.js';
|
|
25
26
|
const debug = (...args) => {
|
|
@@ -212,14 +213,21 @@ ${projectContext.structure ? `\n## Project Structure\n${projectContext.structure
|
|
|
212
213
|
return intelligence ? `\n\n${generateContextFromIntelligence(intelligence)}` : '';
|
|
213
214
|
})()}`;
|
|
214
215
|
}
|
|
215
|
-
export function getFallbackSystemPrompt(projectContext) {
|
|
216
|
-
return getAgentSystemPrompt(projectContext) + '\n\n' + formatToolDefinitions();
|
|
216
|
+
export function getFallbackSystemPrompt(projectContext, additionalTools) {
|
|
217
|
+
return getAgentSystemPrompt(projectContext) + '\n\n' + formatToolDefinitions(additionalTools);
|
|
217
218
|
}
|
|
218
219
|
/**
|
|
219
220
|
* Make a chat API call for agent mode with native tool support.
|
|
220
221
|
* Falls back to agentChatFallback() if provider doesn't support tools.
|
|
221
222
|
*/
|
|
222
|
-
export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout
|
|
223
|
+
export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout,
|
|
224
|
+
/**
|
|
225
|
+
* Extra tool definitions appended to the catalog (currently used to
|
|
226
|
+
* surface MCP-registered tools as first-class entries the model can
|
|
227
|
+
* invoke). Optional — built-in tools work the same whether this is
|
|
228
|
+
* omitted or an empty array.
|
|
229
|
+
*/
|
|
230
|
+
additionalTools) {
|
|
223
231
|
const protocol = config.get('protocol');
|
|
224
232
|
const model = config.get('model');
|
|
225
233
|
const providerId = config.get('provider');
|
|
@@ -255,6 +263,14 @@ export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dy
|
|
|
255
263
|
}
|
|
256
264
|
if (protocol === 'anthropic')
|
|
257
265
|
headers['anthropic-version'] = '2023-06-01';
|
|
266
|
+
// OpenRouter branding — surfaces "Codeep" in the OpenRouter dashboard
|
|
267
|
+
// attribution so users (and OpenRouter itself, for any partnership
|
|
268
|
+
// tracking) see which app generated the traffic. Spec is informal; both
|
|
269
|
+
// headers are documented at openrouter.ai/docs#headers.
|
|
270
|
+
if (providerId === 'openrouter') {
|
|
271
|
+
headers['HTTP-Referer'] = 'https://codeep.dev';
|
|
272
|
+
headers['X-Title'] = 'Codeep';
|
|
273
|
+
}
|
|
258
274
|
try {
|
|
259
275
|
let endpoint;
|
|
260
276
|
let body;
|
|
@@ -264,18 +280,30 @@ export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dy
|
|
|
264
280
|
const maxTok = getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384));
|
|
265
281
|
const tokParam = usesMaxCompletionTokens(providerId) ? { max_completion_tokens: maxTok } : { max_tokens: maxTok };
|
|
266
282
|
endpoint = `${baseUrl}/chat/completions`;
|
|
283
|
+
// OpenRouter-specific extras: request `usage` block in the response
|
|
284
|
+
// body so we get per-call cost (skips our local pricing lookup), and
|
|
285
|
+
// optionally a provider-routing preferences object the user set via
|
|
286
|
+
// `/openrouter prefer …`.
|
|
287
|
+
const openRouterExtras = {};
|
|
288
|
+
if (providerId === 'openrouter') {
|
|
289
|
+
openRouterExtras.usage = { include: true };
|
|
290
|
+
const prefs = readOpenRouterPreferences();
|
|
291
|
+
if (prefs)
|
|
292
|
+
openRouterExtras.provider = prefs;
|
|
293
|
+
}
|
|
267
294
|
body = {
|
|
268
295
|
model, messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
269
|
-
tools: getOpenAITools(), tool_choice: 'auto', stream: useStreaming,
|
|
296
|
+
tools: getOpenAITools(additionalTools), tool_choice: 'auto', stream: useStreaming,
|
|
270
297
|
...tempParam, ...tokParam,
|
|
271
298
|
...(useStreaming && providerId === 'openai' ? { stream_options: { include_usage: true } } : {}),
|
|
299
|
+
...openRouterExtras,
|
|
272
300
|
};
|
|
273
301
|
}
|
|
274
302
|
else {
|
|
275
303
|
endpoint = `${baseUrl}/v1/messages`;
|
|
276
304
|
body = {
|
|
277
305
|
model, system: systemPrompt, messages,
|
|
278
|
-
tools: getAnthropicTools(), stream: useStreaming,
|
|
306
|
+
tools: getAnthropicTools(additionalTools), stream: useStreaming,
|
|
279
307
|
...tempParam, max_tokens: getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384)),
|
|
280
308
|
};
|
|
281
309
|
}
|
|
@@ -298,8 +326,15 @@ export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dy
|
|
|
298
326
|
const data = await response.json();
|
|
299
327
|
const usageExtractor = protocol === 'openai' ? extractOpenAIUsage : extractAnthropicUsage;
|
|
300
328
|
const usage = usageExtractor(data);
|
|
301
|
-
if (usage)
|
|
302
|
-
|
|
329
|
+
if (usage) {
|
|
330
|
+
// OpenRouter returns the authoritative per-call cost in
|
|
331
|
+
// `usage.cost` (USD). Use it instead of our local pricing table
|
|
332
|
+
// since the catalog has 100+ models we don't track ourselves.
|
|
333
|
+
const reportedCost = providerId === 'openrouter' && typeof data?.usage?.cost === 'number'
|
|
334
|
+
? data.usage.cost
|
|
335
|
+
: undefined;
|
|
336
|
+
recordTokenUsage(usage, model, providerId, reportedCost);
|
|
337
|
+
}
|
|
303
338
|
if (protocol === 'openai') {
|
|
304
339
|
const message = data.choices?.[0]?.message;
|
|
305
340
|
const content = message?.content || '';
|
|
@@ -426,8 +461,12 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
|
|
|
426
461
|
const data = await response.json();
|
|
427
462
|
const fallbackUsageExtractor = protocol === 'openai' ? extractOpenAIUsage : extractAnthropicUsage;
|
|
428
463
|
const fallbackUsage = fallbackUsageExtractor(data);
|
|
429
|
-
if (fallbackUsage)
|
|
430
|
-
|
|
464
|
+
if (fallbackUsage) {
|
|
465
|
+
const reportedCost = providerId === 'openrouter' && typeof data?.usage?.cost === 'number'
|
|
466
|
+
? data.usage.cost
|
|
467
|
+
: undefined;
|
|
468
|
+
recordTokenUsage(fallbackUsage, model, providerId, reportedCost);
|
|
469
|
+
}
|
|
431
470
|
content = protocol === 'openai' ? (data.choices?.[0]?.message?.content || '') : (data.content?.[0]?.text || '');
|
|
432
471
|
}
|
|
433
472
|
const toolCalls = parseToolCalls(content);
|
|
@@ -120,8 +120,12 @@ export async function handleOpenAIAgentStream(body, onChunk, model, providerId)
|
|
|
120
120
|
}
|
|
121
121
|
if (usageData) {
|
|
122
122
|
const usage = extractOpenAIUsage(usageData);
|
|
123
|
-
if (usage)
|
|
124
|
-
|
|
123
|
+
if (usage) {
|
|
124
|
+
const reportedCost = providerId === 'openrouter' && typeof usageData?.usage?.cost === 'number'
|
|
125
|
+
? usageData.usage.cost
|
|
126
|
+
: undefined;
|
|
127
|
+
recordTokenUsage(usage, model, providerId, reportedCost);
|
|
128
|
+
}
|
|
125
129
|
}
|
|
126
130
|
const rawToolCalls = Array.from(toolCallMap.values()).map(tc => ({
|
|
127
131
|
id: tc.id,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session checkpoints — `/checkpoint` and `/rewind`.
|
|
3
|
+
*
|
|
4
|
+
* A checkpoint is a snapshot of the agent conversation at a point in time:
|
|
5
|
+
* session history, provider/model state, and the list of files the agent has
|
|
6
|
+
* touched in this session. It does NOT snapshot file content — that's git's
|
|
7
|
+
* job, and trying to do it ourselves means either a 100KB-per-file ceiling
|
|
8
|
+
* with surprise truncation, or a real-time disk hog. We surface a hint at
|
|
9
|
+
* rewind time telling the user how to restore files via git.
|
|
10
|
+
*
|
|
11
|
+
* Use cases this is for:
|
|
12
|
+
* - User suspects the agent is going off the rails after N steps and
|
|
13
|
+
* wants to roll the conversation back to before that turn.
|
|
14
|
+
* - User is about to start a risky multi-step refactor and wants a
|
|
15
|
+
* named bookmark to return to if it gets messy.
|
|
16
|
+
*
|
|
17
|
+
* Use cases this is NOT for:
|
|
18
|
+
* - Replacing git. If you only need file rollback, `git restore` /
|
|
19
|
+
* `git stash` is faster and reliable.
|
|
20
|
+
* - Cross-session persistence beyond the workspace's `.codeep/checkpoints/`
|
|
21
|
+
* folder — checkpoints are per-project, not global.
|
|
22
|
+
*
|
|
23
|
+
* On-disk shape (`.codeep/checkpoints/<id>.json`):
|
|
24
|
+
* {
|
|
25
|
+
* "id": "ck-2026-05-18-abc123",
|
|
26
|
+
* "name": "before big refactor",
|
|
27
|
+
* "createdAt": "...",
|
|
28
|
+
* "sessionId": "session-2026-05-18-...",
|
|
29
|
+
* "provider": "z.ai",
|
|
30
|
+
* "model": "glm-5.1",
|
|
31
|
+
* "messages": [ ... ],
|
|
32
|
+
* "filesTouched": ["src/a.ts", "src/b.ts"],
|
|
33
|
+
* "gitHead": "abcdef0" // optional, recorded only if cwd is a git repo
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
import type { Message } from '../config/index.js';
|
|
37
|
+
export interface Checkpoint {
|
|
38
|
+
id: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
provider: string;
|
|
43
|
+
model: string;
|
|
44
|
+
messages: Message[];
|
|
45
|
+
filesTouched: string[];
|
|
46
|
+
gitHead?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Lightweight metadata for `/checkpoints` list — avoids loading full message arrays. */
|
|
49
|
+
export interface CheckpointMeta {
|
|
50
|
+
id: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
messageCount: number;
|
|
55
|
+
filesTouchedCount: number;
|
|
56
|
+
gitHead?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a new checkpoint snapshot of the current session state.
|
|
60
|
+
* Returns the persisted checkpoint object.
|
|
61
|
+
*/
|
|
62
|
+
export declare function createCheckpoint(opts: {
|
|
63
|
+
workspaceRoot: string;
|
|
64
|
+
sessionId: string;
|
|
65
|
+
provider: string;
|
|
66
|
+
model: string;
|
|
67
|
+
messages: Message[];
|
|
68
|
+
filesTouched: string[];
|
|
69
|
+
name?: string;
|
|
70
|
+
}): Checkpoint;
|
|
71
|
+
/**
|
|
72
|
+
* Load a single checkpoint by id. Returns null if not found or unreadable.
|
|
73
|
+
*/
|
|
74
|
+
export declare function loadCheckpoint(workspaceRoot: string, id: string): Checkpoint | null;
|
|
75
|
+
/**
|
|
76
|
+
* List checkpoints in the workspace, newest first. Returns metadata only —
|
|
77
|
+
* the full `messages` array is not loaded so this is cheap for `/checkpoints`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function listCheckpoints(workspaceRoot: string): CheckpointMeta[];
|
|
80
|
+
/**
|
|
81
|
+
* Delete a checkpoint by id. Returns true if a file was removed.
|
|
82
|
+
*/
|
|
83
|
+
export declare function deleteCheckpoint(workspaceRoot: string, id: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Format a checkpoint list as a Markdown block for `/checkpoints` output.
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatCheckpointList(metas: CheckpointMeta[]): string;
|
|
88
|
+
/**
|
|
89
|
+
* Build the user-facing hint shown after a successful /rewind. Tells the
|
|
90
|
+
* user how to bring their files back to checkpoint state — checkpoints
|
|
91
|
+
* don't snapshot file content, so this is the only restore path.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildRewindGitHint(cp: Checkpoint): string;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session checkpoints — `/checkpoint` and `/rewind`.
|
|
3
|
+
*
|
|
4
|
+
* A checkpoint is a snapshot of the agent conversation at a point in time:
|
|
5
|
+
* session history, provider/model state, and the list of files the agent has
|
|
6
|
+
* touched in this session. It does NOT snapshot file content — that's git's
|
|
7
|
+
* job, and trying to do it ourselves means either a 100KB-per-file ceiling
|
|
8
|
+
* with surprise truncation, or a real-time disk hog. We surface a hint at
|
|
9
|
+
* rewind time telling the user how to restore files via git.
|
|
10
|
+
*
|
|
11
|
+
* Use cases this is for:
|
|
12
|
+
* - User suspects the agent is going off the rails after N steps and
|
|
13
|
+
* wants to roll the conversation back to before that turn.
|
|
14
|
+
* - User is about to start a risky multi-step refactor and wants a
|
|
15
|
+
* named bookmark to return to if it gets messy.
|
|
16
|
+
*
|
|
17
|
+
* Use cases this is NOT for:
|
|
18
|
+
* - Replacing git. If you only need file rollback, `git restore` /
|
|
19
|
+
* `git stash` is faster and reliable.
|
|
20
|
+
* - Cross-session persistence beyond the workspace's `.codeep/checkpoints/`
|
|
21
|
+
* folder — checkpoints are per-project, not global.
|
|
22
|
+
*
|
|
23
|
+
* On-disk shape (`.codeep/checkpoints/<id>.json`):
|
|
24
|
+
* {
|
|
25
|
+
* "id": "ck-2026-05-18-abc123",
|
|
26
|
+
* "name": "before big refactor",
|
|
27
|
+
* "createdAt": "...",
|
|
28
|
+
* "sessionId": "session-2026-05-18-...",
|
|
29
|
+
* "provider": "z.ai",
|
|
30
|
+
* "model": "glm-5.1",
|
|
31
|
+
* "messages": [ ... ],
|
|
32
|
+
* "filesTouched": ["src/a.ts", "src/b.ts"],
|
|
33
|
+
* "gitHead": "abcdef0" // optional, recorded only if cwd is a git repo
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs';
|
|
37
|
+
import { join } from 'path';
|
|
38
|
+
import { randomUUID } from 'crypto';
|
|
39
|
+
import { execSync } from 'child_process';
|
|
40
|
+
function getCheckpointsDir(workspaceRoot) {
|
|
41
|
+
const dir = join(workspaceRoot, '.codeep', 'checkpoints');
|
|
42
|
+
if (!existsSync(dir))
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
function readGitHead(workspaceRoot) {
|
|
47
|
+
try {
|
|
48
|
+
// Short SHA is enough for human display; full SHA available via git if needed.
|
|
49
|
+
const out = execSync('git rev-parse --short HEAD', {
|
|
50
|
+
cwd: workspaceRoot,
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
53
|
+
timeout: 2000,
|
|
54
|
+
}).trim();
|
|
55
|
+
return out || undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generate a unique, human-recognizable checkpoint id.
|
|
63
|
+
* Format: `ck-YYYY-MM-DD-<8-char-uuid>`
|
|
64
|
+
*/
|
|
65
|
+
function generateCheckpointId() {
|
|
66
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
67
|
+
return `ck-${date}-${randomUUID().slice(0, 8)}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a new checkpoint snapshot of the current session state.
|
|
71
|
+
* Returns the persisted checkpoint object.
|
|
72
|
+
*/
|
|
73
|
+
export function createCheckpoint(opts) {
|
|
74
|
+
const checkpoint = {
|
|
75
|
+
id: generateCheckpointId(),
|
|
76
|
+
name: opts.name,
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
sessionId: opts.sessionId,
|
|
79
|
+
provider: opts.provider,
|
|
80
|
+
model: opts.model,
|
|
81
|
+
messages: opts.messages,
|
|
82
|
+
filesTouched: opts.filesTouched,
|
|
83
|
+
gitHead: readGitHead(opts.workspaceRoot),
|
|
84
|
+
};
|
|
85
|
+
const dir = getCheckpointsDir(opts.workspaceRoot);
|
|
86
|
+
writeFileSync(join(dir, `${checkpoint.id}.json`), JSON.stringify(checkpoint, null, 2));
|
|
87
|
+
return checkpoint;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Load a single checkpoint by id. Returns null if not found or unreadable.
|
|
91
|
+
*/
|
|
92
|
+
export function loadCheckpoint(workspaceRoot, id) {
|
|
93
|
+
// Defensive: reject ids that look like path traversal attempts. Real ids are
|
|
94
|
+
// `ck-YYYY-MM-DD-<hex>` so the regex is tight enough to catch typos too.
|
|
95
|
+
if (!/^ck-\d{4}-\d{2}-\d{2}-[a-f0-9]{8}$/.test(id))
|
|
96
|
+
return null;
|
|
97
|
+
const file = join(getCheckpointsDir(workspaceRoot), `${id}.json`);
|
|
98
|
+
if (!existsSync(file))
|
|
99
|
+
return null;
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* List checkpoints in the workspace, newest first. Returns metadata only —
|
|
109
|
+
* the full `messages` array is not loaded so this is cheap for `/checkpoints`.
|
|
110
|
+
*/
|
|
111
|
+
export function listCheckpoints(workspaceRoot) {
|
|
112
|
+
const dir = getCheckpointsDir(workspaceRoot);
|
|
113
|
+
let files;
|
|
114
|
+
try {
|
|
115
|
+
files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
const metas = [];
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
try {
|
|
123
|
+
const full = join(dir, file);
|
|
124
|
+
const stat = statSync(full);
|
|
125
|
+
const cp = JSON.parse(readFileSync(full, 'utf-8'));
|
|
126
|
+
metas.push({
|
|
127
|
+
id: cp.id,
|
|
128
|
+
name: cp.name,
|
|
129
|
+
createdAt: cp.createdAt || stat.mtime.toISOString(),
|
|
130
|
+
sessionId: cp.sessionId,
|
|
131
|
+
messageCount: cp.messages?.length ?? 0,
|
|
132
|
+
filesTouchedCount: cp.filesTouched?.length ?? 0,
|
|
133
|
+
gitHead: cp.gitHead,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Skip corrupt entries — don't let one bad checkpoint block the list.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Newest first by createdAt.
|
|
141
|
+
metas.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
|
142
|
+
return metas;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Delete a checkpoint by id. Returns true if a file was removed.
|
|
146
|
+
*/
|
|
147
|
+
export function deleteCheckpoint(workspaceRoot, id) {
|
|
148
|
+
if (!/^ck-\d{4}-\d{2}-\d{2}-[a-f0-9]{8}$/.test(id))
|
|
149
|
+
return false;
|
|
150
|
+
const file = join(getCheckpointsDir(workspaceRoot), `${id}.json`);
|
|
151
|
+
if (!existsSync(file))
|
|
152
|
+
return false;
|
|
153
|
+
try {
|
|
154
|
+
unlinkSync(file);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Format a checkpoint list as a Markdown block for `/checkpoints` output.
|
|
163
|
+
*/
|
|
164
|
+
export function formatCheckpointList(metas) {
|
|
165
|
+
if (metas.length === 0) {
|
|
166
|
+
return [
|
|
167
|
+
'_No checkpoints yet._',
|
|
168
|
+
'',
|
|
169
|
+
'Create one with `/checkpoint [name]` before risky refactors so you can `/rewind` if things go sideways.',
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|
|
172
|
+
const lines = ['## Checkpoints', ''];
|
|
173
|
+
for (const m of metas) {
|
|
174
|
+
const label = m.name ? `**${m.name}** (\`${m.id}\`)` : `\`${m.id}\``;
|
|
175
|
+
const gitFragment = m.gitHead ? ` · git \`${m.gitHead}\`` : '';
|
|
176
|
+
const date = m.createdAt.slice(0, 19).replace('T', ' ');
|
|
177
|
+
lines.push(`- ${label}`, ` ${date} · ${m.messageCount} message${m.messageCount === 1 ? '' : 's'} · ${m.filesTouchedCount} file${m.filesTouchedCount === 1 ? '' : 's'} touched${gitFragment}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push('', 'Use `/rewind <id>` to restore a checkpoint, or `/checkpoint delete <id>` to drop one.');
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Build the user-facing hint shown after a successful /rewind. Tells the
|
|
184
|
+
* user how to bring their files back to checkpoint state — checkpoints
|
|
185
|
+
* don't snapshot file content, so this is the only restore path.
|
|
186
|
+
*/
|
|
187
|
+
export function buildRewindGitHint(cp) {
|
|
188
|
+
if (cp.gitHead) {
|
|
189
|
+
return [
|
|
190
|
+
`**Files were NOT restored** — only the conversation. To bring files back to the state at checkpoint time:`,
|
|
191
|
+
'',
|
|
192
|
+
'```bash',
|
|
193
|
+
`git stash # save current uncommitted changes if you want to keep them`,
|
|
194
|
+
`git checkout ${cp.gitHead} -- ${cp.filesTouched.length > 0 ? cp.filesTouched.map(f => `'${f}'`).join(' ') : '.'}`,
|
|
195
|
+
'```',
|
|
196
|
+
].join('\n');
|
|
197
|
+
}
|
|
198
|
+
return [
|
|
199
|
+
'**Files were NOT restored** — only the conversation.',
|
|
200
|
+
cp.filesTouched.length > 0
|
|
201
|
+
? `Files the agent touched between checkpoint and now: ${cp.filesTouched.map(f => `\`${f}\``).join(', ')}.`
|
|
202
|
+
: '',
|
|
203
|
+
'Use `git` (or your editor\'s undo) to revert any file changes you don\'t want.',
|
|
204
|
+
].filter(Boolean).join('\n');
|
|
205
|
+
}
|
package/dist/utils/context.d.ts
CHANGED
|
@@ -27,6 +27,30 @@ export declare function clearContext(projectPath: string): boolean;
|
|
|
27
27
|
* Get all saved contexts
|
|
28
28
|
*/
|
|
29
29
|
export declare function getAllContexts(): ConversationContext[];
|
|
30
|
+
/**
|
|
31
|
+
* AI-powered compaction of a conversation history.
|
|
32
|
+
*
|
|
33
|
+
* Used by the `/compact` slash command. Sends the older portion of the
|
|
34
|
+
* conversation to the active provider with a summarization prompt, then
|
|
35
|
+
* replaces those messages with a single system message containing the
|
|
36
|
+
* summary. Keeps the last `keepRecent` messages verbatim so the
|
|
37
|
+
* conversation can continue without losing the most recent context.
|
|
38
|
+
*
|
|
39
|
+
* Returns the same `history` (untouched) if there isn't enough to
|
|
40
|
+
* meaningfully compact.
|
|
41
|
+
*/
|
|
42
|
+
export declare function compactHistory(history: Message[], options?: {
|
|
43
|
+
keepRecent?: number;
|
|
44
|
+
projectContext?: import('./project').ProjectContext | null;
|
|
45
|
+
/** Cap on how long the summarization API call can take, in ms. Defaults to 60s. */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
/** External abort signal (e.g. user pressed /stop). Combined with the timeout. */
|
|
48
|
+
abortSignal?: AbortSignal;
|
|
49
|
+
}): Promise<{
|
|
50
|
+
compacted: Message[];
|
|
51
|
+
replaced: number;
|
|
52
|
+
summary: string;
|
|
53
|
+
}>;
|
|
30
54
|
/**
|
|
31
55
|
* Summarize messages for context persistence
|
|
32
56
|
* Keeps recent messages and summarizes older ones
|