ctb 1.1.0 → 1.3.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 +50 -11
- package/docs/plans/2026-02-01-performance-reliability-improvements.md +1038 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +12 -0
- package/src/__tests__/errors.test.ts +60 -0
- package/src/__tests__/events.test.ts +43 -0
- package/src/__tests__/session.test.ts +472 -0
- package/src/__tests__/telegram-api.test.ts +80 -0
- package/src/bot.ts +27 -3
- package/src/cli.ts +94 -0
- package/src/errors.ts +58 -0
- package/src/events.ts +57 -0
- package/src/handlers/commands.ts +383 -50
- package/src/handlers/index.ts +8 -1
- package/src/handlers/streaming.ts +23 -27
- package/src/handlers/text.ts +2 -1
- package/src/index.ts +53 -18
- package/src/session.ts +207 -7
- package/src/telegram-api.ts +195 -0
- package/src/types.ts +34 -33
- package/src/utils.ts +4 -21
package/src/bot.ts
CHANGED
|
@@ -18,17 +18,24 @@ import {
|
|
|
18
18
|
handleBookmarks,
|
|
19
19
|
handleCallback,
|
|
20
20
|
handleCd,
|
|
21
|
+
handleCompact,
|
|
22
|
+
handleCost,
|
|
21
23
|
handleDocument,
|
|
24
|
+
handleFile,
|
|
25
|
+
handleModel,
|
|
22
26
|
handleNew,
|
|
23
27
|
handlePhoto,
|
|
24
|
-
|
|
28
|
+
handlePlan,
|
|
25
29
|
handleRestart,
|
|
26
30
|
handleResume,
|
|
27
31
|
handleRetry,
|
|
32
|
+
handleSkill,
|
|
28
33
|
handleStart,
|
|
29
34
|
handleStatus,
|
|
30
35
|
handleStop,
|
|
31
36
|
handleText,
|
|
37
|
+
handleThink,
|
|
38
|
+
handleUndo,
|
|
32
39
|
handleVoice,
|
|
33
40
|
} from "./handlers";
|
|
34
41
|
|
|
@@ -61,12 +68,22 @@ bot.use(
|
|
|
61
68
|
bot.command("start", handleStart);
|
|
62
69
|
bot.command("new", handleNew);
|
|
63
70
|
bot.command("stop", handleStop);
|
|
71
|
+
bot.command("c", handleStop);
|
|
72
|
+
bot.command("kill", handleStop);
|
|
73
|
+
bot.command("dc", handleStop);
|
|
64
74
|
bot.command("status", handleStatus);
|
|
65
75
|
bot.command("resume", handleResume);
|
|
66
76
|
bot.command("restart", handleRestart);
|
|
67
77
|
bot.command("retry", handleRetry);
|
|
68
78
|
bot.command("cd", handleCd);
|
|
69
|
-
bot.command("
|
|
79
|
+
bot.command("skill", handleSkill);
|
|
80
|
+
bot.command("file", handleFile);
|
|
81
|
+
bot.command("model", handleModel);
|
|
82
|
+
bot.command("cost", handleCost);
|
|
83
|
+
bot.command("think", handleThink);
|
|
84
|
+
bot.command("plan", handlePlan);
|
|
85
|
+
bot.command("compact", handleCompact);
|
|
86
|
+
bot.command("undo", handleUndo);
|
|
70
87
|
bot.command("bookmarks", handleBookmarks);
|
|
71
88
|
|
|
72
89
|
// ============== Message Handlers ==============
|
|
@@ -113,8 +130,15 @@ await bot.api.setMyCommands([
|
|
|
113
130
|
{ command: "resume", description: "Resume last session" },
|
|
114
131
|
{ command: "stop", description: "Interrupt current query" },
|
|
115
132
|
{ command: "status", description: "Check what Claude is doing" },
|
|
133
|
+
{ command: "model", description: "Switch model (sonnet/opus/haiku)" },
|
|
134
|
+
{ command: "cost", description: "Show token usage and cost" },
|
|
135
|
+
{ command: "think", description: "Force extended thinking" },
|
|
136
|
+
{ command: "plan", description: "Toggle planning mode" },
|
|
137
|
+
{ command: "compact", description: "Trigger context compaction" },
|
|
138
|
+
{ command: "undo", description: "Revert file changes" },
|
|
116
139
|
{ command: "cd", description: "Change working directory" },
|
|
117
|
-
{ command: "
|
|
140
|
+
{ command: "skill", description: "Invoke a Claude Code skill" },
|
|
141
|
+
{ command: "file", description: "Download a file" },
|
|
118
142
|
{ command: "bookmarks", description: "Manage directory bookmarks" },
|
|
119
143
|
{ command: "retry", description: "Retry last message" },
|
|
120
144
|
{ command: "restart", description: "Restart the bot" },
|
package/src/cli.ts
CHANGED
|
@@ -23,6 +23,7 @@ interface CliOptions {
|
|
|
23
23
|
dir?: string;
|
|
24
24
|
help?: boolean;
|
|
25
25
|
version?: boolean;
|
|
26
|
+
tut?: boolean;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function parseArgs(args: string[]): CliOptions {
|
|
@@ -33,6 +34,8 @@ function parseArgs(args: string[]): CliOptions {
|
|
|
33
34
|
options.help = true;
|
|
34
35
|
} else if (arg === "--version" || arg === "-v") {
|
|
35
36
|
options.version = true;
|
|
37
|
+
} else if (arg === "tut" || arg === "tutorial") {
|
|
38
|
+
options.tut = true;
|
|
36
39
|
} else if (arg.startsWith("--token=")) {
|
|
37
40
|
options.token = arg.slice(8);
|
|
38
41
|
} else if (arg.startsWith("--users=")) {
|
|
@@ -53,6 +56,7 @@ Run a Telegram bot that controls Claude Code in your project directory.
|
|
|
53
56
|
|
|
54
57
|
USAGE:
|
|
55
58
|
ctb [options]
|
|
59
|
+
ctb tut Show setup tutorial
|
|
56
60
|
|
|
57
61
|
OPTIONS:
|
|
58
62
|
--help, -h Show this help message
|
|
@@ -75,6 +79,91 @@ Multiple instances can run simultaneously in different directories.
|
|
|
75
79
|
`);
|
|
76
80
|
}
|
|
77
81
|
|
|
82
|
+
function showTutorial(): void {
|
|
83
|
+
console.log(`
|
|
84
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
85
|
+
║ CTB Setup Tutorial ║
|
|
86
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
87
|
+
|
|
88
|
+
Follow these steps to set up your Claude Telegram Bot:
|
|
89
|
+
|
|
90
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
91
|
+
STEP 1: Create a Telegram Bot
|
|
92
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
93
|
+
|
|
94
|
+
1. Open Telegram and search for @BotFather
|
|
95
|
+
2. Send /newbot
|
|
96
|
+
3. Follow the prompts:
|
|
97
|
+
- Choose a name (e.g., "My Claude Bot")
|
|
98
|
+
- Choose a username (must end in "bot", e.g., "my_claude_bot")
|
|
99
|
+
4. Copy the token that looks like:
|
|
100
|
+
1234567890:ABCdefGHIjklMNOpqrsTUVwxyz
|
|
101
|
+
|
|
102
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
103
|
+
STEP 2: Get Your Telegram User ID
|
|
104
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
105
|
+
|
|
106
|
+
1. Open Telegram and search for @userinfobot
|
|
107
|
+
2. Send any message to it
|
|
108
|
+
3. It will reply with your user ID (a number like 123456789)
|
|
109
|
+
4. Copy this number
|
|
110
|
+
|
|
111
|
+
Tip: Add multiple user IDs separated by commas for team access
|
|
112
|
+
|
|
113
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
114
|
+
STEP 3: Configure the Bot
|
|
115
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
116
|
+
|
|
117
|
+
Option A: Interactive setup (easiest)
|
|
118
|
+
Just run: ctb
|
|
119
|
+
It will prompt you for the token and user IDs.
|
|
120
|
+
|
|
121
|
+
Option B: Create a .env file
|
|
122
|
+
Create a file named .env in your project directory:
|
|
123
|
+
|
|
124
|
+
TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz
|
|
125
|
+
TELEGRAM_ALLOWED_USERS=123456789,987654321
|
|
126
|
+
|
|
127
|
+
Option C: Use command-line arguments
|
|
128
|
+
ctb --token=YOUR_TOKEN --users=YOUR_USER_ID
|
|
129
|
+
|
|
130
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
131
|
+
STEP 4: Set Up Bot Commands (Optional)
|
|
132
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
133
|
+
|
|
134
|
+
1. Go back to @BotFather
|
|
135
|
+
2. Send /setcommands
|
|
136
|
+
3. Select your bot
|
|
137
|
+
4. Paste this command list:
|
|
138
|
+
|
|
139
|
+
start - Show status and user ID
|
|
140
|
+
new - Start a fresh session
|
|
141
|
+
resume - Resume last session
|
|
142
|
+
stop - Interrupt current query
|
|
143
|
+
status - Check what Claude is doing
|
|
144
|
+
undo - Revert file changes
|
|
145
|
+
cd - Change working directory
|
|
146
|
+
file - Download a file
|
|
147
|
+
bookmarks - Manage directory bookmarks
|
|
148
|
+
retry - Retry last message
|
|
149
|
+
restart - Restart the bot
|
|
150
|
+
|
|
151
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
152
|
+
STEP 5: Start the Bot
|
|
153
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
154
|
+
|
|
155
|
+
cd ~/your-project
|
|
156
|
+
ctb
|
|
157
|
+
|
|
158
|
+
The bot will start and show "Bot started: @your_bot_username"
|
|
159
|
+
Open Telegram and message your bot to start using Claude!
|
|
160
|
+
|
|
161
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
162
|
+
Need help? https://github.com/htlin/claude-telegram-bot
|
|
163
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
|
|
78
167
|
async function prompt(question: string): Promise<string> {
|
|
79
168
|
const rl = createInterface({
|
|
80
169
|
input: process.stdin,
|
|
@@ -226,6 +315,11 @@ async function main(): Promise<void> {
|
|
|
226
315
|
process.exit(0);
|
|
227
316
|
}
|
|
228
317
|
|
|
318
|
+
if (options.tut) {
|
|
319
|
+
showTutorial();
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
|
|
229
323
|
// Determine working directory
|
|
230
324
|
const workingDir = options.dir ? resolve(options.dir) : process.cwd();
|
|
231
325
|
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-friendly error message formatting.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
interface ErrorPattern {
|
|
6
|
+
pattern: RegExp;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ERROR_PATTERNS: ErrorPattern[] = [
|
|
11
|
+
{
|
|
12
|
+
pattern: /timeout/i,
|
|
13
|
+
message:
|
|
14
|
+
"The operation took too long. Try a simpler request or break it into smaller steps.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
pattern: /too many requests|rate limit|retry after/i,
|
|
18
|
+
message: "Claude is busy right now. Please wait a moment and try again.",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /etimedout|econnreset|enotfound/i,
|
|
22
|
+
message: "Connection issue. Please check your network and try again.",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /cancelled|aborted/i,
|
|
26
|
+
message: "Request was cancelled.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /unsafe command|blocked/i,
|
|
30
|
+
message: "That operation isn't allowed for safety reasons.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pattern: /file access|outside allowed paths/i,
|
|
34
|
+
message: "Claude can't access that file location.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
pattern: /authentication|unauthorized|401/i,
|
|
38
|
+
message: "Authentication issue. Please check your credentials.",
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert technical errors to user-friendly messages.
|
|
44
|
+
*/
|
|
45
|
+
export function formatUserError(error: Error): string {
|
|
46
|
+
const errorStr = error.message || String(error);
|
|
47
|
+
|
|
48
|
+
for (const { pattern, message } of ERROR_PATTERNS) {
|
|
49
|
+
if (pattern.test(errorStr)) {
|
|
50
|
+
return message;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Generic fallback with truncation
|
|
55
|
+
const truncated =
|
|
56
|
+
errorStr.length > 200 ? errorStr.slice(0, 200) + "..." : errorStr;
|
|
57
|
+
return `Error: ${truncated || "An unexpected error occurred"}`;
|
|
58
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight event emitter for decoupling modules.
|
|
3
|
+
* Eliminates circular dependencies between session and utils.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
type EventCallback<T> = (data: T) => void;
|
|
7
|
+
|
|
8
|
+
interface BotEventsMap {
|
|
9
|
+
sessionRunning: boolean;
|
|
10
|
+
stopRequested: undefined;
|
|
11
|
+
interruptRequested: undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class BotEventEmitter {
|
|
15
|
+
private listeners = new Map<
|
|
16
|
+
keyof BotEventsMap,
|
|
17
|
+
Set<EventCallback<unknown>>
|
|
18
|
+
>();
|
|
19
|
+
private sessionRunning = false;
|
|
20
|
+
|
|
21
|
+
on<K extends keyof BotEventsMap>(
|
|
22
|
+
event: K,
|
|
23
|
+
callback: EventCallback<BotEventsMap[K]>,
|
|
24
|
+
): () => void {
|
|
25
|
+
if (!this.listeners.has(event)) {
|
|
26
|
+
this.listeners.set(event, new Set());
|
|
27
|
+
}
|
|
28
|
+
this.listeners.get(event)?.add(callback as EventCallback<unknown>);
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
this.listeners.get(event)?.delete(callback as EventCallback<unknown>);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
emit<K extends keyof BotEventsMap>(event: K, data: BotEventsMap[K]): void {
|
|
36
|
+
if (event === "sessionRunning") {
|
|
37
|
+
this.sessionRunning = data as boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const callbacks = this.listeners.get(event);
|
|
41
|
+
if (callbacks) {
|
|
42
|
+
for (const callback of callbacks) {
|
|
43
|
+
try {
|
|
44
|
+
callback(data);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`Event handler error for ${event}:`, error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getSessionState(): boolean {
|
|
53
|
+
return this.sessionRunning;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const botEvents = new BotEventEmitter();
|