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/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
- handlePreview,
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("preview", handlePreview);
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: "preview", description: "Download a file" },
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();