codemaxxing 0.1.8 → 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 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>) => Promise<"yes" | "no" | "always">;
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 { FILE_TOOLS, executeTool } from "./tools/files.js";
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
- const systemPrompt = await getSystemPrompt(context);
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
- const decision = await this.options.onToolApproval(toolCall.name, args);
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
+ }