codemaxxing 0.1.7 → 0.1.9
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 +43 -0
- package/dist/agent.d.ts +31 -1
- package/dist/agent.js +357 -5
- package/dist/config.d.ts +3 -5
- package/dist/config.js +23 -0
- package/dist/index.js +208 -48
- package/dist/tools/files.d.ts +8 -0
- package/dist/tools/files.js +137 -0
- package/dist/utils/sessions.d.ts +7 -0
- package/dist/utils/sessions.js +26 -1
- package/package.json +2 -1
- package/src/agent.ts +380 -6
- package/src/config.ts +28 -1
- package/src/index.tsx +278 -53
- package/src/tools/files.ts +127 -0
- package/src/utils/sessions.ts +33 -1
package/README.md
CHANGED
|
@@ -40,6 +40,12 @@ curl -fsSL -o %TEMP%\install-codemaxxing.bat https://raw.githubusercontent.com/M
|
|
|
40
40
|
npm install -g codemaxxing
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## Updating
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm update -g codemaxxing
|
|
47
|
+
```
|
|
48
|
+
|
|
43
49
|
## Quick Start
|
|
44
50
|
|
|
45
51
|
### 1. Start Your LLM
|
|
@@ -61,6 +67,39 @@ That's it. Codemaxxing auto-detects LM Studio and connects. Start coding.
|
|
|
61
67
|
|
|
62
68
|
---
|
|
63
69
|
|
|
70
|
+
## Authentication
|
|
71
|
+
|
|
72
|
+
**One command to connect any provider:**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
codemaxxing login
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Interactive setup walks you through it. Or use `/login` inside the TUI.
|
|
79
|
+
|
|
80
|
+
**Supported auth methods:**
|
|
81
|
+
|
|
82
|
+
| Provider | Methods |
|
|
83
|
+
|----------|---------|
|
|
84
|
+
| **OpenRouter** | OAuth (browser login) or API key — one login, 200+ models |
|
|
85
|
+
| **Anthropic** | Link your Claude subscription (via Claude Code) or API key |
|
|
86
|
+
| **OpenAI** | Import from Codex CLI or API key |
|
|
87
|
+
| **Qwen** | Import from Qwen CLI or API key |
|
|
88
|
+
| **GitHub Copilot** | Device flow (browser) |
|
|
89
|
+
| **Google Gemini** | API key |
|
|
90
|
+
| **Any provider** | API key + custom base URL |
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
codemaxxing login # Interactive provider picker
|
|
94
|
+
codemaxxing auth list # See saved credentials
|
|
95
|
+
codemaxxing auth remove <name> # Delete a credential
|
|
96
|
+
codemaxxing auth openrouter # Direct OpenRouter OAuth
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Credentials stored securely in `~/.codemaxxing/auth.json` (owner-only permissions).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
64
103
|
## Advanced Setup
|
|
65
104
|
|
|
66
105
|
**With a remote provider (OpenAI, OpenRouter, etc.):**
|
|
@@ -106,6 +145,9 @@ Switch models mid-session without restarting:
|
|
|
106
145
|
- `/model gpt-4o` — switch to a different model
|
|
107
146
|
- `/models` — list available models from your provider
|
|
108
147
|
|
|
148
|
+
### 🔐 Authentication
|
|
149
|
+
One command to connect any LLM provider. OpenRouter OAuth (browser login for 200+ models), Anthropic subscription linking, Codex/Qwen CLI import, GitHub Copilot device flow, or manual API keys. Use `codemaxxing login` or `/login` in-session.
|
|
150
|
+
|
|
109
151
|
### 📋 Smart Paste
|
|
110
152
|
Paste large code blocks without breaking the UI. Multi-line pastes collapse into `[Pasted text #1 +N lines]` badges (like Claude Code).
|
|
111
153
|
|
|
@@ -117,6 +159,7 @@ Type `/` for autocomplete suggestions. Arrow keys to navigate, Tab or Enter to s
|
|
|
117
159
|
| Command | Description |
|
|
118
160
|
|---------|-------------|
|
|
119
161
|
| `/help` | Show all commands |
|
|
162
|
+
| `/login` | Interactive auth setup |
|
|
120
163
|
| `/model <name>` | Switch model mid-session |
|
|
121
164
|
| `/models` | List available models |
|
|
122
165
|
| `/map` | Show repository map |
|
package/dist/agent.d.ts
CHANGED
|
@@ -8,12 +8,16 @@ export interface AgentOptions {
|
|
|
8
8
|
onToolCall?: (name: string, args: Record<string, unknown>) => void;
|
|
9
9
|
onToolResult?: (name: string, result: string) => void;
|
|
10
10
|
onThinking?: (text: string) => void;
|
|
11
|
-
onToolApproval?: (name: string, args: Record<string, unknown
|
|
11
|
+
onToolApproval?: (name: string, args: Record<string, unknown>, diff?: string) => Promise<"yes" | "no" | "always">;
|
|
12
12
|
onGitCommit?: (message: string) => void;
|
|
13
|
+
onContextCompressed?: (oldTokens: number, newTokens: number) => void;
|
|
14
|
+
contextCompressionThreshold?: number;
|
|
13
15
|
}
|
|
14
16
|
export declare class CodingAgent {
|
|
15
17
|
private options;
|
|
16
18
|
private client;
|
|
19
|
+
private anthropicClient;
|
|
20
|
+
private providerType;
|
|
17
21
|
private messages;
|
|
18
22
|
private tools;
|
|
19
23
|
private cwd;
|
|
@@ -25,6 +29,11 @@ export declare class CodingAgent {
|
|
|
25
29
|
private autoCommitEnabled;
|
|
26
30
|
private repoMap;
|
|
27
31
|
private sessionId;
|
|
32
|
+
private totalPromptTokens;
|
|
33
|
+
private totalCompletionTokens;
|
|
34
|
+
private totalCost;
|
|
35
|
+
private systemPrompt;
|
|
36
|
+
private compressionThreshold;
|
|
28
37
|
constructor(options: AgentOptions);
|
|
29
38
|
/**
|
|
30
39
|
* Initialize the agent — call this after constructor to build async context
|
|
@@ -49,6 +58,18 @@ export declare class CodingAgent {
|
|
|
49
58
|
* and loops until the model responds with text (no more tool calls).
|
|
50
59
|
*/
|
|
51
60
|
chat(userMessage: string): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Convert OpenAI-format tools to Anthropic tool format
|
|
63
|
+
*/
|
|
64
|
+
private getAnthropicTools;
|
|
65
|
+
/**
|
|
66
|
+
* Convert messages to Anthropic format (separate system from conversation)
|
|
67
|
+
*/
|
|
68
|
+
private getAnthropicMessages;
|
|
69
|
+
/**
|
|
70
|
+
* Anthropic-native streaming chat
|
|
71
|
+
*/
|
|
72
|
+
private chatAnthropic;
|
|
52
73
|
/**
|
|
53
74
|
* Switch to a different model mid-session
|
|
54
75
|
*/
|
|
@@ -61,5 +82,14 @@ export declare class CodingAgent {
|
|
|
61
82
|
* Estimate token count across all messages (~4 chars per token)
|
|
62
83
|
*/
|
|
63
84
|
estimateTokens(): number;
|
|
85
|
+
/**
|
|
86
|
+
* Check if context needs compression and compress if threshold exceeded
|
|
87
|
+
*/
|
|
88
|
+
private maybeCompressContext;
|
|
89
|
+
getCostInfo(): {
|
|
90
|
+
promptTokens: number;
|
|
91
|
+
completionTokens: number;
|
|
92
|
+
totalCost: number;
|
|
93
|
+
};
|
|
64
94
|
reset(): void;
|
|
65
95
|
}
|
package/dist/agent.js
CHANGED
|
@@ -1,13 +1,60 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import { FILE_TOOLS, executeTool, generateDiff, getExistingContent } from "./tools/files.js";
|
|
3
4
|
import { buildProjectContext, getSystemPrompt } from "./utils/context.js";
|
|
4
5
|
import { isGitRepo, autoCommit } from "./utils/git.js";
|
|
5
|
-
import { createSession, saveMessage, updateTokenEstimate, loadMessages } from "./utils/sessions.js";
|
|
6
|
+
import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
|
|
6
7
|
// Tools that can modify your project — require approval
|
|
7
8
|
const DANGEROUS_TOOLS = new Set(["write_file", "run_command"]);
|
|
9
|
+
// Cost per 1M tokens (input/output) for common models
|
|
10
|
+
const MODEL_COSTS = {
|
|
11
|
+
// OpenAI
|
|
12
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
13
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
14
|
+
"gpt-4-turbo": { input: 10, output: 30 },
|
|
15
|
+
"gpt-4": { input: 30, output: 60 },
|
|
16
|
+
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
17
|
+
"o1": { input: 15, output: 60 },
|
|
18
|
+
"o1-mini": { input: 3, output: 12 },
|
|
19
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
20
|
+
// Anthropic
|
|
21
|
+
"claude-3-5-sonnet-20241022": { input: 3, output: 15 },
|
|
22
|
+
"claude-3-5-sonnet": { input: 3, output: 15 },
|
|
23
|
+
"claude-sonnet-4-20250514": { input: 3, output: 15 },
|
|
24
|
+
"claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
|
|
25
|
+
"claude-3-opus-20240229": { input: 15, output: 75 },
|
|
26
|
+
"claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
|
|
27
|
+
// Qwen (typically free/cheap on local, but OpenRouter pricing)
|
|
28
|
+
"qwen/qwen-2.5-coder-32b-instruct": { input: 0.2, output: 0.2 },
|
|
29
|
+
"qwen/qwen-2.5-72b-instruct": { input: 0.35, output: 0.4 },
|
|
30
|
+
// DeepSeek
|
|
31
|
+
"deepseek/deepseek-chat": { input: 0.14, output: 0.28 },
|
|
32
|
+
"deepseek/deepseek-coder": { input: 0.14, output: 0.28 },
|
|
33
|
+
// Llama
|
|
34
|
+
"meta-llama/llama-3.1-70b-instruct": { input: 0.52, output: 0.75 },
|
|
35
|
+
"meta-llama/llama-3.1-8b-instruct": { input: 0.055, output: 0.055 },
|
|
36
|
+
// Google
|
|
37
|
+
"google/gemini-pro-1.5": { input: 1.25, output: 5 },
|
|
38
|
+
"google/gemini-flash-1.5": { input: 0.075, output: 0.3 },
|
|
39
|
+
};
|
|
40
|
+
function getModelCost(model) {
|
|
41
|
+
// Direct match
|
|
42
|
+
if (MODEL_COSTS[model])
|
|
43
|
+
return MODEL_COSTS[model];
|
|
44
|
+
// Partial match (model name contains a known key)
|
|
45
|
+
const lower = model.toLowerCase();
|
|
46
|
+
for (const [key, cost] of Object.entries(MODEL_COSTS)) {
|
|
47
|
+
if (lower.includes(key) || key.includes(lower))
|
|
48
|
+
return cost;
|
|
49
|
+
}
|
|
50
|
+
// Default: $0 (local/unknown models)
|
|
51
|
+
return { input: 0, output: 0 };
|
|
52
|
+
}
|
|
8
53
|
export class CodingAgent {
|
|
9
54
|
options;
|
|
10
55
|
client;
|
|
56
|
+
anthropicClient = null;
|
|
57
|
+
providerType;
|
|
11
58
|
messages = [];
|
|
12
59
|
tools = FILE_TOOLS;
|
|
13
60
|
cwd;
|
|
@@ -19,26 +66,42 @@ export class CodingAgent {
|
|
|
19
66
|
autoCommitEnabled = false;
|
|
20
67
|
repoMap = "";
|
|
21
68
|
sessionId = "";
|
|
69
|
+
totalPromptTokens = 0;
|
|
70
|
+
totalCompletionTokens = 0;
|
|
71
|
+
totalCost = 0;
|
|
72
|
+
systemPrompt = "";
|
|
73
|
+
compressionThreshold;
|
|
22
74
|
constructor(options) {
|
|
23
75
|
this.options = options;
|
|
76
|
+
this.providerType = options.provider.type || "openai";
|
|
24
77
|
this.client = new OpenAI({
|
|
25
78
|
baseURL: options.provider.baseUrl,
|
|
26
79
|
apiKey: options.provider.apiKey,
|
|
27
80
|
});
|
|
81
|
+
if (this.providerType === "anthropic") {
|
|
82
|
+
this.anthropicClient = new Anthropic({
|
|
83
|
+
apiKey: options.provider.apiKey,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
28
86
|
this.cwd = options.cwd;
|
|
29
87
|
this.maxTokens = options.maxTokens;
|
|
30
88
|
this.autoApprove = options.autoApprove;
|
|
31
89
|
this.model = options.provider.model;
|
|
90
|
+
// Default model for Anthropic
|
|
91
|
+
if (this.providerType === "anthropic" && (this.model === "auto" || !this.model)) {
|
|
92
|
+
this.model = "claude-sonnet-4-20250514";
|
|
93
|
+
}
|
|
32
94
|
this.gitEnabled = isGitRepo(this.cwd);
|
|
95
|
+
this.compressionThreshold = options.contextCompressionThreshold ?? 80000;
|
|
33
96
|
}
|
|
34
97
|
/**
|
|
35
98
|
* Initialize the agent — call this after constructor to build async context
|
|
36
99
|
*/
|
|
37
100
|
async init() {
|
|
38
101
|
const context = await buildProjectContext(this.cwd);
|
|
39
|
-
|
|
102
|
+
this.systemPrompt = await getSystemPrompt(context);
|
|
40
103
|
this.messages = [
|
|
41
|
-
{ role: "system", content: systemPrompt },
|
|
104
|
+
{ role: "system", content: this.systemPrompt },
|
|
42
105
|
];
|
|
43
106
|
// Create a new session
|
|
44
107
|
this.sessionId = createSession(this.cwd, this.model);
|
|
@@ -81,6 +144,11 @@ export class CodingAgent {
|
|
|
81
144
|
const userMsg = { role: "user", content: userMessage };
|
|
82
145
|
this.messages.push(userMsg);
|
|
83
146
|
saveMessage(this.sessionId, userMsg);
|
|
147
|
+
// Check if context needs compression before sending
|
|
148
|
+
await this.maybeCompressContext();
|
|
149
|
+
if (this.providerType === "anthropic" && this.anthropicClient) {
|
|
150
|
+
return this.chatAnthropic(userMessage);
|
|
151
|
+
}
|
|
84
152
|
let iterations = 0;
|
|
85
153
|
const MAX_ITERATIONS = 20;
|
|
86
154
|
while (iterations < MAX_ITERATIONS) {
|
|
@@ -91,13 +159,21 @@ export class CodingAgent {
|
|
|
91
159
|
tools: this.tools,
|
|
92
160
|
max_tokens: this.maxTokens,
|
|
93
161
|
stream: true,
|
|
162
|
+
stream_options: { include_usage: true },
|
|
94
163
|
});
|
|
95
164
|
// Accumulate the streamed response
|
|
96
165
|
let contentText = "";
|
|
97
166
|
let thinkingText = "";
|
|
98
167
|
let inThinking = false;
|
|
99
168
|
const toolCalls = new Map();
|
|
169
|
+
let chunkPromptTokens = 0;
|
|
170
|
+
let chunkCompletionTokens = 0;
|
|
100
171
|
for await (const chunk of stream) {
|
|
172
|
+
// Capture usage from the final chunk
|
|
173
|
+
if (chunk.usage) {
|
|
174
|
+
chunkPromptTokens = chunk.usage.prompt_tokens ?? 0;
|
|
175
|
+
chunkCompletionTokens = chunk.usage.completion_tokens ?? 0;
|
|
176
|
+
}
|
|
101
177
|
const delta = chunk.choices?.[0]?.delta;
|
|
102
178
|
if (!delta)
|
|
103
179
|
continue;
|
|
@@ -154,6 +230,15 @@ export class CodingAgent {
|
|
|
154
230
|
}
|
|
155
231
|
this.messages.push(assistantMessage);
|
|
156
232
|
saveMessage(this.sessionId, assistantMessage);
|
|
233
|
+
// Track token usage and cost
|
|
234
|
+
if (chunkPromptTokens > 0 || chunkCompletionTokens > 0) {
|
|
235
|
+
this.totalPromptTokens += chunkPromptTokens;
|
|
236
|
+
this.totalCompletionTokens += chunkCompletionTokens;
|
|
237
|
+
const costs = getModelCost(this.model);
|
|
238
|
+
this.totalCost = (this.totalPromptTokens / 1_000_000) * costs.input +
|
|
239
|
+
(this.totalCompletionTokens / 1_000_000) * costs.output;
|
|
240
|
+
updateSessionCost(this.sessionId, this.totalPromptTokens, this.totalCompletionTokens, this.totalCost);
|
|
241
|
+
}
|
|
157
242
|
// If no tool calls, we're done — return the text
|
|
158
243
|
if (toolCalls.size === 0) {
|
|
159
244
|
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
@@ -172,7 +257,15 @@ export class CodingAgent {
|
|
|
172
257
|
// Check approval for dangerous tools
|
|
173
258
|
if (DANGEROUS_TOOLS.has(toolCall.name) && !this.autoApprove && !this.alwaysApproved.has(toolCall.name)) {
|
|
174
259
|
if (this.options.onToolApproval) {
|
|
175
|
-
|
|
260
|
+
// Generate diff for write_file if file already exists
|
|
261
|
+
let diff;
|
|
262
|
+
if (toolCall.name === "write_file" && args.path && args.content) {
|
|
263
|
+
const existing = getExistingContent(String(args.path), this.cwd);
|
|
264
|
+
if (existing !== null) {
|
|
265
|
+
diff = generateDiff(existing, String(args.content), String(args.path));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const decision = await this.options.onToolApproval(toolCall.name, args, diff);
|
|
176
269
|
if (decision === "no") {
|
|
177
270
|
const denied = `Tool call "${toolCall.name}" was denied by the user.`;
|
|
178
271
|
this.options.onToolResult?.(toolCall.name, denied);
|
|
@@ -213,6 +306,185 @@ export class CodingAgent {
|
|
|
213
306
|
}
|
|
214
307
|
return "Max iterations reached. The agent may be stuck in a loop.";
|
|
215
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Convert OpenAI-format tools to Anthropic tool format
|
|
311
|
+
*/
|
|
312
|
+
getAnthropicTools() {
|
|
313
|
+
return this.tools.map((t) => ({
|
|
314
|
+
name: t.function.name,
|
|
315
|
+
description: t.function.description ?? "",
|
|
316
|
+
input_schema: t.function.parameters ?? { type: "object", properties: {} },
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Convert messages to Anthropic format (separate system from conversation)
|
|
321
|
+
*/
|
|
322
|
+
getAnthropicMessages() {
|
|
323
|
+
const msgs = [];
|
|
324
|
+
for (const msg of this.messages) {
|
|
325
|
+
if (msg.role === "system")
|
|
326
|
+
continue; // system handled separately
|
|
327
|
+
if (msg.role === "user") {
|
|
328
|
+
msgs.push({ role: "user", content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content) });
|
|
329
|
+
}
|
|
330
|
+
else if (msg.role === "assistant") {
|
|
331
|
+
const content = [];
|
|
332
|
+
if (msg.content) {
|
|
333
|
+
content.push({ type: "text", text: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content) });
|
|
334
|
+
}
|
|
335
|
+
if ("tool_calls" in msg && Array.isArray(msg.tool_calls)) {
|
|
336
|
+
for (const tc of msg.tool_calls) {
|
|
337
|
+
let input = {};
|
|
338
|
+
try {
|
|
339
|
+
input = JSON.parse(tc.function.arguments);
|
|
340
|
+
}
|
|
341
|
+
catch { }
|
|
342
|
+
content.push({
|
|
343
|
+
type: "tool_use",
|
|
344
|
+
id: tc.id,
|
|
345
|
+
name: tc.function.name,
|
|
346
|
+
input,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (content.length > 0) {
|
|
351
|
+
msgs.push({ role: "assistant", content });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else if (msg.role === "tool") {
|
|
355
|
+
const toolCallId = msg.tool_call_id;
|
|
356
|
+
const resultContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
357
|
+
// Anthropic expects tool results as user messages with tool_result content
|
|
358
|
+
msgs.push({
|
|
359
|
+
role: "user",
|
|
360
|
+
content: [{
|
|
361
|
+
type: "tool_result",
|
|
362
|
+
tool_use_id: toolCallId,
|
|
363
|
+
content: resultContent,
|
|
364
|
+
}],
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return msgs;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Anthropic-native streaming chat
|
|
372
|
+
*/
|
|
373
|
+
async chatAnthropic(_userMessage) {
|
|
374
|
+
const client = this.anthropicClient;
|
|
375
|
+
let iterations = 0;
|
|
376
|
+
const MAX_ITERATIONS = 20;
|
|
377
|
+
while (iterations < MAX_ITERATIONS) {
|
|
378
|
+
iterations++;
|
|
379
|
+
const anthropicMessages = this.getAnthropicMessages();
|
|
380
|
+
const anthropicTools = this.getAnthropicTools();
|
|
381
|
+
const stream = client.messages.stream({
|
|
382
|
+
model: this.model,
|
|
383
|
+
max_tokens: this.maxTokens,
|
|
384
|
+
system: this.systemPrompt,
|
|
385
|
+
messages: anthropicMessages,
|
|
386
|
+
tools: anthropicTools,
|
|
387
|
+
});
|
|
388
|
+
let contentText = "";
|
|
389
|
+
const toolCalls = [];
|
|
390
|
+
let currentToolId = "";
|
|
391
|
+
let currentToolName = "";
|
|
392
|
+
let currentToolInput = "";
|
|
393
|
+
stream.on("text", (text) => {
|
|
394
|
+
contentText += text;
|
|
395
|
+
this.options.onToken?.(text);
|
|
396
|
+
});
|
|
397
|
+
const finalMessage = await stream.finalMessage();
|
|
398
|
+
// Track usage
|
|
399
|
+
if (finalMessage.usage) {
|
|
400
|
+
const promptTokens = finalMessage.usage.input_tokens;
|
|
401
|
+
const completionTokens = finalMessage.usage.output_tokens;
|
|
402
|
+
this.totalPromptTokens += promptTokens;
|
|
403
|
+
this.totalCompletionTokens += completionTokens;
|
|
404
|
+
const costs = getModelCost(this.model);
|
|
405
|
+
this.totalCost = (this.totalPromptTokens / 1_000_000) * costs.input +
|
|
406
|
+
(this.totalCompletionTokens / 1_000_000) * costs.output;
|
|
407
|
+
updateSessionCost(this.sessionId, this.totalPromptTokens, this.totalCompletionTokens, this.totalCost);
|
|
408
|
+
}
|
|
409
|
+
// Extract tool uses from content blocks
|
|
410
|
+
for (const block of finalMessage.content) {
|
|
411
|
+
if (block.type === "tool_use") {
|
|
412
|
+
toolCalls.push({
|
|
413
|
+
id: block.id,
|
|
414
|
+
name: block.name,
|
|
415
|
+
input: block.input,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Build OpenAI-format assistant message for session storage
|
|
420
|
+
const assistantMessage = { role: "assistant", content: contentText || null };
|
|
421
|
+
if (toolCalls.length > 0) {
|
|
422
|
+
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
423
|
+
id: tc.id,
|
|
424
|
+
type: "function",
|
|
425
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) },
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
this.messages.push(assistantMessage);
|
|
429
|
+
saveMessage(this.sessionId, assistantMessage);
|
|
430
|
+
// If no tool calls, we're done
|
|
431
|
+
if (toolCalls.length === 0) {
|
|
432
|
+
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
433
|
+
return contentText || "(empty response)";
|
|
434
|
+
}
|
|
435
|
+
// Process tool calls
|
|
436
|
+
for (const toolCall of toolCalls) {
|
|
437
|
+
const args = toolCall.input;
|
|
438
|
+
this.options.onToolCall?.(toolCall.name, args);
|
|
439
|
+
// Check approval for dangerous tools
|
|
440
|
+
if (DANGEROUS_TOOLS.has(toolCall.name) && !this.autoApprove && !this.alwaysApproved.has(toolCall.name)) {
|
|
441
|
+
if (this.options.onToolApproval) {
|
|
442
|
+
let diff;
|
|
443
|
+
if (toolCall.name === "write_file" && args.path && args.content) {
|
|
444
|
+
const existing = getExistingContent(String(args.path), this.cwd);
|
|
445
|
+
if (existing !== null) {
|
|
446
|
+
diff = generateDiff(existing, String(args.content), String(args.path));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const decision = await this.options.onToolApproval(toolCall.name, args, diff);
|
|
450
|
+
if (decision === "no") {
|
|
451
|
+
const denied = `Tool call "${toolCall.name}" was denied by the user.`;
|
|
452
|
+
this.options.onToolResult?.(toolCall.name, denied);
|
|
453
|
+
const deniedMsg = {
|
|
454
|
+
role: "tool",
|
|
455
|
+
tool_call_id: toolCall.id,
|
|
456
|
+
content: denied,
|
|
457
|
+
};
|
|
458
|
+
this.messages.push(deniedMsg);
|
|
459
|
+
saveMessage(this.sessionId, deniedMsg);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (decision === "always") {
|
|
463
|
+
this.alwaysApproved.add(toolCall.name);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const result = await executeTool(toolCall.name, args, this.cwd);
|
|
468
|
+
this.options.onToolResult?.(toolCall.name, result);
|
|
469
|
+
// Auto-commit after successful write_file
|
|
470
|
+
if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
|
|
471
|
+
const path = String(args.path ?? "unknown");
|
|
472
|
+
const committed = autoCommit(this.cwd, path, "write");
|
|
473
|
+
if (committed) {
|
|
474
|
+
this.options.onGitCommit?.(`write ${path}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const toolMsg = {
|
|
478
|
+
role: "tool",
|
|
479
|
+
tool_call_id: toolCall.id,
|
|
480
|
+
content: result,
|
|
481
|
+
};
|
|
482
|
+
this.messages.push(toolMsg);
|
|
483
|
+
saveMessage(this.sessionId, toolMsg);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return "Max iterations reached. The agent may be stuck in a loop.";
|
|
487
|
+
}
|
|
216
488
|
/**
|
|
217
489
|
* Switch to a different model mid-session
|
|
218
490
|
*/
|
|
@@ -262,6 +534,86 @@ export class CodingAgent {
|
|
|
262
534
|
}
|
|
263
535
|
return Math.ceil(chars / 4);
|
|
264
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Check if context needs compression and compress if threshold exceeded
|
|
539
|
+
*/
|
|
540
|
+
async maybeCompressContext() {
|
|
541
|
+
const currentTokens = this.estimateTokens();
|
|
542
|
+
if (currentTokens < this.compressionThreshold)
|
|
543
|
+
return;
|
|
544
|
+
// Keep: system prompt (index 0) + last 10 messages
|
|
545
|
+
const keepCount = 10;
|
|
546
|
+
if (this.messages.length <= keepCount + 1)
|
|
547
|
+
return; // Not enough to compress
|
|
548
|
+
const systemMsg = this.messages[0];
|
|
549
|
+
const middleMessages = this.messages.slice(1, this.messages.length - keepCount);
|
|
550
|
+
const recentMessages = this.messages.slice(this.messages.length - keepCount);
|
|
551
|
+
if (middleMessages.length === 0)
|
|
552
|
+
return;
|
|
553
|
+
// Build a summary of the middle messages
|
|
554
|
+
const summaryParts = [];
|
|
555
|
+
for (const msg of middleMessages) {
|
|
556
|
+
if (msg.role === "user" && typeof msg.content === "string") {
|
|
557
|
+
summaryParts.push(`User: ${msg.content.slice(0, 200)}`);
|
|
558
|
+
}
|
|
559
|
+
else if (msg.role === "assistant" && typeof msg.content === "string" && msg.content) {
|
|
560
|
+
summaryParts.push(`Assistant: ${msg.content.slice(0, 200)}`);
|
|
561
|
+
}
|
|
562
|
+
else if (msg.role === "tool") {
|
|
563
|
+
// Skip tool messages in summary to save tokens
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Use the active model to summarize
|
|
567
|
+
const summaryPrompt = `Summarize this conversation history in 2-3 concise paragraphs. Focus on: what was discussed, what files were modified, what decisions were made, and any important context for continuing the conversation.\n\n${summaryParts.join("\n")}`;
|
|
568
|
+
try {
|
|
569
|
+
let summary;
|
|
570
|
+
if (this.providerType === "anthropic" && this.anthropicClient) {
|
|
571
|
+
const response = await this.anthropicClient.messages.create({
|
|
572
|
+
model: this.model,
|
|
573
|
+
max_tokens: 500,
|
|
574
|
+
messages: [{ role: "user", content: summaryPrompt }],
|
|
575
|
+
});
|
|
576
|
+
summary = response.content
|
|
577
|
+
.filter((b) => b.type === "text")
|
|
578
|
+
.map((b) => b.text)
|
|
579
|
+
.join("");
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
const response = await this.client.chat.completions.create({
|
|
583
|
+
model: this.model,
|
|
584
|
+
max_tokens: 500,
|
|
585
|
+
messages: [{ role: "user", content: summaryPrompt }],
|
|
586
|
+
});
|
|
587
|
+
summary = response.choices[0]?.message?.content ?? "Previous conversation context.";
|
|
588
|
+
}
|
|
589
|
+
const compressedMsg = {
|
|
590
|
+
role: "assistant",
|
|
591
|
+
content: `[Context compressed: ${summary}]`,
|
|
592
|
+
};
|
|
593
|
+
const oldTokens = currentTokens;
|
|
594
|
+
this.messages = [systemMsg, compressedMsg, ...recentMessages];
|
|
595
|
+
const newTokens = this.estimateTokens();
|
|
596
|
+
this.options.onContextCompressed?.(oldTokens, newTokens);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
// If summarization fails, just truncate without summary
|
|
600
|
+
const compressedMsg = {
|
|
601
|
+
role: "assistant",
|
|
602
|
+
content: "[Context compressed: Earlier conversation history was removed to stay within token limits.]",
|
|
603
|
+
};
|
|
604
|
+
const oldTokens = currentTokens;
|
|
605
|
+
this.messages = [systemMsg, compressedMsg, ...recentMessages];
|
|
606
|
+
const newTokens = this.estimateTokens();
|
|
607
|
+
this.options.onContextCompressed?.(oldTokens, newTokens);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
getCostInfo() {
|
|
611
|
+
return {
|
|
612
|
+
promptTokens: this.totalPromptTokens,
|
|
613
|
+
completionTokens: this.totalCompletionTokens,
|
|
614
|
+
totalCost: this.totalCost,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
265
617
|
reset() {
|
|
266
618
|
const systemMsg = this.messages[0];
|
|
267
619
|
this.messages = [systemMsg];
|
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface ProviderConfig {
|
|
|
2
2
|
baseUrl: string;
|
|
3
3
|
apiKey: string;
|
|
4
4
|
model: string;
|
|
5
|
+
type?: "openai" | "anthropic";
|
|
5
6
|
}
|
|
6
7
|
export interface ProviderProfile extends ProviderConfig {
|
|
7
8
|
name: string;
|
|
@@ -13,6 +14,7 @@ export interface CodemaxxingConfig {
|
|
|
13
14
|
autoApprove: boolean;
|
|
14
15
|
contextFiles: number;
|
|
15
16
|
maxTokens: number;
|
|
17
|
+
contextCompressionThreshold?: number;
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
export interface CLIArgs {
|
|
@@ -43,8 +45,4 @@ export declare function listModels(baseUrl: string, apiKey: string): Promise<str
|
|
|
43
45
|
* Resolve provider configuration from auth store or config file
|
|
44
46
|
* Priority: CLI args > auth store > config file > auto-detect
|
|
45
47
|
*/
|
|
46
|
-
export declare function resolveProvider(providerId: string, cliArgs: CLIArgs):
|
|
47
|
-
baseUrl: string;
|
|
48
|
-
apiKey: string;
|
|
49
|
-
model: string;
|
|
50
|
-
} | null;
|
|
48
|
+
export declare function resolveProvider(providerId: string, cliArgs: CLIArgs): ProviderConfig | null;
|
package/dist/config.js
CHANGED
|
@@ -102,6 +102,14 @@ export function applyOverrides(config, args) {
|
|
|
102
102
|
if (args.provider && config.providers?.[args.provider]) {
|
|
103
103
|
const profile = config.providers[args.provider];
|
|
104
104
|
result.provider = { ...profile };
|
|
105
|
+
// Also check auth store for this provider
|
|
106
|
+
const authCred = getCredential(args.provider);
|
|
107
|
+
if (authCred) {
|
|
108
|
+
result.provider.baseUrl = authCred.baseUrl;
|
|
109
|
+
result.provider.apiKey = authCred.apiKey;
|
|
110
|
+
}
|
|
111
|
+
// Detect provider type
|
|
112
|
+
result.provider.type = detectProviderType(args.provider, result.provider.baseUrl);
|
|
105
113
|
}
|
|
106
114
|
// CLI flags override everything
|
|
107
115
|
if (args.model)
|
|
@@ -110,6 +118,10 @@ export function applyOverrides(config, args) {
|
|
|
110
118
|
result.provider.apiKey = args.apiKey;
|
|
111
119
|
if (args.baseUrl)
|
|
112
120
|
result.provider.baseUrl = args.baseUrl;
|
|
121
|
+
// Auto-detect type from baseUrl if not set
|
|
122
|
+
if (!result.provider.type && result.provider.baseUrl) {
|
|
123
|
+
result.provider.type = detectProviderType(args.provider || "", result.provider.baseUrl);
|
|
124
|
+
}
|
|
113
125
|
return result;
|
|
114
126
|
}
|
|
115
127
|
export function getConfigPath() {
|
|
@@ -185,6 +197,7 @@ export function resolveProvider(providerId, cliArgs) {
|
|
|
185
197
|
baseUrl: authCred.baseUrl,
|
|
186
198
|
apiKey: authCred.apiKey,
|
|
187
199
|
model: cliArgs.model || "auto",
|
|
200
|
+
type: detectProviderType(providerId, authCred.baseUrl),
|
|
188
201
|
};
|
|
189
202
|
}
|
|
190
203
|
// Fall back to config file
|
|
@@ -195,7 +208,17 @@ export function resolveProvider(providerId, cliArgs) {
|
|
|
195
208
|
baseUrl: provider.baseUrl,
|
|
196
209
|
apiKey: cliArgs.apiKey || provider.apiKey,
|
|
197
210
|
model: cliArgs.model || provider.model,
|
|
211
|
+
type: detectProviderType(providerId, provider.baseUrl),
|
|
198
212
|
};
|
|
199
213
|
}
|
|
200
214
|
return null;
|
|
201
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Detect provider type from ID or base URL
|
|
218
|
+
*/
|
|
219
|
+
function detectProviderType(providerId, baseUrl) {
|
|
220
|
+
if (providerId === "anthropic" || baseUrl.includes("anthropic.com")) {
|
|
221
|
+
return "anthropic";
|
|
222
|
+
}
|
|
223
|
+
return "openai";
|
|
224
|
+
}
|