ctb 1.1.0 → 1.2.1
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 +42 -11
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +12 -0
- package/src/__tests__/session.test.ts +399 -0
- package/src/bot.ts +27 -3
- package/src/cli.ts +94 -0
- package/src/handlers/commands.ts +383 -50
- package/src/handlers/index.ts +8 -1
- package/src/index.ts +19 -2
- package/src/session.ts +140 -6
package/src/session.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import {
|
|
10
10
|
type Options,
|
|
11
|
+
type Query,
|
|
11
12
|
query,
|
|
12
13
|
type SDKMessage,
|
|
13
14
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -31,8 +32,9 @@ import type { SessionData, StatusCallback, TokenUsage } from "./types";
|
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Determine thinking token budget based on message keywords.
|
|
35
|
+
* Exported for testing.
|
|
34
36
|
*/
|
|
35
|
-
function getThinkingLevel(message: string): number {
|
|
37
|
+
export function getThinkingLevel(message: string): number {
|
|
36
38
|
const msgLower = message.toLowerCase();
|
|
37
39
|
|
|
38
40
|
// Check deep thinking triggers first (more specific)
|
|
@@ -78,6 +80,21 @@ class ClaudeSession {
|
|
|
78
80
|
lastErrorTime: Date | null = null;
|
|
79
81
|
lastUsage: TokenUsage | null = null;
|
|
80
82
|
lastMessage: string | null = null;
|
|
83
|
+
lastBotResponse: string | null = null;
|
|
84
|
+
|
|
85
|
+
// Model and mode settings
|
|
86
|
+
currentModel: "sonnet" | "opus" | "haiku" = "sonnet";
|
|
87
|
+
forceThinking: number | null = null; // Tokens for next message, then resets
|
|
88
|
+
planMode = false;
|
|
89
|
+
|
|
90
|
+
// Cumulative usage tracking
|
|
91
|
+
totalInputTokens = 0;
|
|
92
|
+
totalOutputTokens = 0;
|
|
93
|
+
totalCacheReadTokens = 0;
|
|
94
|
+
|
|
95
|
+
// File checkpointing for /undo
|
|
96
|
+
private _queryInstance: Query | null = null;
|
|
97
|
+
private _userMessageUuids: string[] = [];
|
|
81
98
|
|
|
82
99
|
// Mutable working directory (can be changed with /cd)
|
|
83
100
|
private _workingDir: string = WORKING_DIR;
|
|
@@ -192,11 +209,27 @@ class ClaudeSession {
|
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
const isNewSession = !this.isActive;
|
|
195
|
-
|
|
212
|
+
|
|
213
|
+
// Determine thinking tokens - forceThinking overrides keyword detection
|
|
214
|
+
let thinkingTokens: number;
|
|
215
|
+
if (this.forceThinking !== null) {
|
|
216
|
+
thinkingTokens = this.forceThinking;
|
|
217
|
+
this.forceThinking = null; // Reset after use
|
|
218
|
+
} else {
|
|
219
|
+
thinkingTokens = getThinkingLevel(message);
|
|
220
|
+
}
|
|
196
221
|
const thinkingLabel =
|
|
197
222
|
{ 0: "off", 10000: "normal", 50000: "deep" }[thinkingTokens] ||
|
|
198
223
|
String(thinkingTokens);
|
|
199
224
|
|
|
225
|
+
// Determine model based on currentModel setting
|
|
226
|
+
const modelMap = {
|
|
227
|
+
sonnet: "claude-sonnet-4-5",
|
|
228
|
+
opus: "claude-opus-4-5",
|
|
229
|
+
haiku: "claude-haiku-3-5",
|
|
230
|
+
};
|
|
231
|
+
const modelId = modelMap[this.currentModel];
|
|
232
|
+
|
|
200
233
|
// Inject current date/time at session start so Claude doesn't need to call a tool for it
|
|
201
234
|
let messageToSend = message;
|
|
202
235
|
if (isNewSession) {
|
|
@@ -218,16 +251,17 @@ class ClaudeSession {
|
|
|
218
251
|
|
|
219
252
|
// Build SDK V1 options - supports all features
|
|
220
253
|
const options: Options = {
|
|
221
|
-
model:
|
|
254
|
+
model: modelId,
|
|
222
255
|
cwd: this._workingDir,
|
|
223
256
|
settingSources: ["user", "project"],
|
|
224
|
-
permissionMode: "bypassPermissions",
|
|
225
|
-
allowDangerouslySkipPermissions:
|
|
257
|
+
permissionMode: this.planMode ? "plan" : "bypassPermissions",
|
|
258
|
+
allowDangerouslySkipPermissions: !this.planMode,
|
|
226
259
|
systemPrompt: SAFETY_PROMPT,
|
|
227
260
|
mcpServers: MCP_SERVERS,
|
|
228
261
|
maxThinkingTokens: thinkingTokens,
|
|
229
262
|
additionalDirectories: ALLOWED_PATHS,
|
|
230
263
|
resume: this.sessionId || undefined,
|
|
264
|
+
enableFileCheckpointing: true, // Enable /undo support
|
|
231
265
|
};
|
|
232
266
|
|
|
233
267
|
// Add Claude Code executable path if set (required for standalone builds)
|
|
@@ -281,6 +315,9 @@ class ClaudeSession {
|
|
|
281
315
|
},
|
|
282
316
|
});
|
|
283
317
|
|
|
318
|
+
// Store query instance for /undo support
|
|
319
|
+
this._queryInstance = queryInstance;
|
|
320
|
+
|
|
284
321
|
// Process streaming response
|
|
285
322
|
for await (const event of queryInstance) {
|
|
286
323
|
// Check for abort
|
|
@@ -308,6 +345,12 @@ class ClaudeSession {
|
|
|
308
345
|
this.saveSession();
|
|
309
346
|
}
|
|
310
347
|
|
|
348
|
+
// Capture user message UUIDs for /undo checkpoints
|
|
349
|
+
if (event.type === "user" && event.uuid) {
|
|
350
|
+
this._userMessageUuids.push(event.uuid);
|
|
351
|
+
console.log(`Checkpoint: user message ${event.uuid.slice(0, 8)}...`);
|
|
352
|
+
}
|
|
353
|
+
|
|
311
354
|
// Handle different message types
|
|
312
355
|
if (event.type === "assistant") {
|
|
313
356
|
for (const block of event.message.content) {
|
|
@@ -436,6 +479,10 @@ class ClaudeSession {
|
|
|
436
479
|
if ("usage" in event && event.usage) {
|
|
437
480
|
this.lastUsage = event.usage as TokenUsage;
|
|
438
481
|
const u = this.lastUsage;
|
|
482
|
+
// Accumulate totals
|
|
483
|
+
this.totalInputTokens += u.input_tokens || 0;
|
|
484
|
+
this.totalOutputTokens += u.output_tokens || 0;
|
|
485
|
+
this.totalCacheReadTokens += u.cache_read_input_tokens || 0;
|
|
439
486
|
console.log(
|
|
440
487
|
`Usage: in=${u.input_tokens} out=${u.output_tokens} cache_read=${
|
|
441
488
|
u.cache_read_input_tokens || 0
|
|
@@ -486,7 +533,9 @@ class ClaudeSession {
|
|
|
486
533
|
|
|
487
534
|
await statusCallback("done", "");
|
|
488
535
|
|
|
489
|
-
|
|
536
|
+
const finalResponse = responseParts.join("") || "No response from Claude.";
|
|
537
|
+
this.lastBotResponse = finalResponse;
|
|
538
|
+
return finalResponse;
|
|
490
539
|
}
|
|
491
540
|
|
|
492
541
|
/**
|
|
@@ -495,9 +544,91 @@ class ClaudeSession {
|
|
|
495
544
|
async kill(): Promise<void> {
|
|
496
545
|
this.sessionId = null;
|
|
497
546
|
this.lastActivity = null;
|
|
547
|
+
// Reset usage totals
|
|
548
|
+
this.totalInputTokens = 0;
|
|
549
|
+
this.totalOutputTokens = 0;
|
|
550
|
+
this.totalCacheReadTokens = 0;
|
|
551
|
+
// Reset modes
|
|
552
|
+
this.planMode = false;
|
|
553
|
+
this.forceThinking = null;
|
|
554
|
+
// Reset checkpoints
|
|
555
|
+
this._queryInstance = null;
|
|
556
|
+
this._userMessageUuids = [];
|
|
498
557
|
console.log("Session cleared");
|
|
499
558
|
}
|
|
500
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Check if undo is available (has checkpoints).
|
|
562
|
+
*/
|
|
563
|
+
get canUndo(): boolean {
|
|
564
|
+
return this._queryInstance !== null && this._userMessageUuids.length > 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Get number of available undo checkpoints.
|
|
569
|
+
*/
|
|
570
|
+
get undoCheckpoints(): number {
|
|
571
|
+
return this._userMessageUuids.length;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Undo file changes by rewinding to the last user message checkpoint.
|
|
576
|
+
* Returns [success, message].
|
|
577
|
+
*/
|
|
578
|
+
async undo(): Promise<[boolean, string]> {
|
|
579
|
+
if (!this._queryInstance) {
|
|
580
|
+
return [false, "No active session to undo"];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (this._userMessageUuids.length === 0) {
|
|
584
|
+
return [false, "No checkpoints available"];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Get and remove the last user message UUID
|
|
588
|
+
const targetUuid = this._userMessageUuids.pop();
|
|
589
|
+
if (!targetUuid) {
|
|
590
|
+
return [false, "No checkpoints available"];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
console.log(`Rewinding files to checkpoint ${targetUuid.slice(0, 8)}...`);
|
|
595
|
+
await this._queryInstance.rewindFiles(targetUuid);
|
|
596
|
+
|
|
597
|
+
const remaining = this._userMessageUuids.length;
|
|
598
|
+
return [
|
|
599
|
+
true,
|
|
600
|
+
`✅ Reverted file changes to checkpoint \`${targetUuid.slice(0, 8)}...\`\n${remaining} checkpoint${remaining !== 1 ? "s" : ""} remaining`,
|
|
601
|
+
];
|
|
602
|
+
} catch (error) {
|
|
603
|
+
// Restore the checkpoint on failure
|
|
604
|
+
this._userMessageUuids.push(targetUuid);
|
|
605
|
+
console.error(`Undo failed: ${error}`);
|
|
606
|
+
return [false, `Failed to undo: ${error}`];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Estimate cost based on current model and usage.
|
|
612
|
+
*/
|
|
613
|
+
estimateCost(): { inputCost: number; outputCost: number; total: number } {
|
|
614
|
+
// Pricing per 1M tokens (approximate as of 2024)
|
|
615
|
+
const pricing = {
|
|
616
|
+
sonnet: { input: 3, output: 15 },
|
|
617
|
+
opus: { input: 15, output: 75 },
|
|
618
|
+
haiku: { input: 0.25, output: 1.25 },
|
|
619
|
+
};
|
|
620
|
+
const rates = pricing[this.currentModel];
|
|
621
|
+
|
|
622
|
+
const inputCost = (this.totalInputTokens / 1_000_000) * rates.input;
|
|
623
|
+
const outputCost = (this.totalOutputTokens / 1_000_000) * rates.output;
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
inputCost,
|
|
627
|
+
outputCost,
|
|
628
|
+
total: inputCost + outputCost,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
501
632
|
/**
|
|
502
633
|
* Save session to disk for resume after restart.
|
|
503
634
|
*/
|
|
@@ -561,5 +692,8 @@ class ClaudeSession {
|
|
|
561
692
|
}
|
|
562
693
|
}
|
|
563
694
|
|
|
695
|
+
// Export class for testing
|
|
696
|
+
export { ClaudeSession };
|
|
697
|
+
|
|
564
698
|
// Global session instance
|
|
565
699
|
export const session = new ClaudeSession();
|