ctb 1.0.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/.env.example +76 -0
- package/CLAUDE.md +116 -0
- package/LICENSE +21 -0
- package/Makefile +142 -0
- package/README.md +268 -0
- package/SECURITY.md +177 -0
- package/ask_user_mcp/server.ts +115 -0
- package/assets/demo-research.gif +0 -0
- package/assets/demo-video-summary.gif +0 -0
- package/assets/demo-workout.gif +0 -0
- package/assets/demo.gif +0 -0
- package/bun.lock +266 -0
- package/bunfig.toml +2 -0
- package/docs/personal-assistant-guide.md +549 -0
- package/launchagent/com.claude-telegram-ts.plist.template +76 -0
- package/launchagent/start.sh +14 -0
- package/mcp-config.example.ts +42 -0
- package/package.json +46 -0
- package/src/__tests__/formatting.test.ts +118 -0
- package/src/__tests__/security.test.ts +124 -0
- package/src/__tests__/setup.ts +8 -0
- package/src/bookmarks.ts +106 -0
- package/src/bot.ts +151 -0
- package/src/cli.ts +278 -0
- package/src/config.ts +254 -0
- package/src/formatting.ts +309 -0
- package/src/handlers/callback.ts +248 -0
- package/src/handlers/commands.ts +392 -0
- package/src/handlers/document.ts +585 -0
- package/src/handlers/index.ts +21 -0
- package/src/handlers/media-group.ts +205 -0
- package/src/handlers/photo.ts +215 -0
- package/src/handlers/streaming.ts +231 -0
- package/src/handlers/text.ts +128 -0
- package/src/handlers/voice.ts +138 -0
- package/src/index.ts +150 -0
- package/src/security.ts +209 -0
- package/src/session.ts +565 -0
- package/src/types.ts +77 -0
- package/src/utils.ts +246 -0
- package/tsconfig.json +29 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for ctb (Claude Telegram Bot)
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ctb # Start bot using .env in current directory
|
|
7
|
+
* ctb --help # Show usage
|
|
8
|
+
* ctb --version # Show version
|
|
9
|
+
* ctb --token=xxx # Override TELEGRAM_BOT_TOKEN
|
|
10
|
+
* ctb --users=123 # Override TELEGRAM_ALLOWED_USERS
|
|
11
|
+
* ctb --dir=/path # Override working directory
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { createInterface } from "node:readline";
|
|
17
|
+
|
|
18
|
+
const VERSION = "1.0.0";
|
|
19
|
+
|
|
20
|
+
interface CliOptions {
|
|
21
|
+
token?: string;
|
|
22
|
+
users?: string;
|
|
23
|
+
dir?: string;
|
|
24
|
+
help?: boolean;
|
|
25
|
+
version?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(args: string[]): CliOptions {
|
|
29
|
+
const options: CliOptions = {};
|
|
30
|
+
|
|
31
|
+
for (const arg of args) {
|
|
32
|
+
if (arg === "--help" || arg === "-h") {
|
|
33
|
+
options.help = true;
|
|
34
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
35
|
+
options.version = true;
|
|
36
|
+
} else if (arg.startsWith("--token=")) {
|
|
37
|
+
options.token = arg.slice(8);
|
|
38
|
+
} else if (arg.startsWith("--users=")) {
|
|
39
|
+
options.users = arg.slice(8);
|
|
40
|
+
} else if (arg.startsWith("--dir=")) {
|
|
41
|
+
options.dir = arg.slice(6);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function showHelp(): void {
|
|
49
|
+
console.log(`
|
|
50
|
+
ctb - Claude Telegram Bot
|
|
51
|
+
|
|
52
|
+
Run a Telegram bot that controls Claude Code in your project directory.
|
|
53
|
+
|
|
54
|
+
USAGE:
|
|
55
|
+
ctb [options]
|
|
56
|
+
|
|
57
|
+
OPTIONS:
|
|
58
|
+
--help, -h Show this help message
|
|
59
|
+
--version, -v Show version
|
|
60
|
+
--token=TOKEN Override TELEGRAM_BOT_TOKEN from .env
|
|
61
|
+
--users=IDS Override TELEGRAM_ALLOWED_USERS (comma-separated)
|
|
62
|
+
--dir=PATH Override working directory (default: current directory)
|
|
63
|
+
|
|
64
|
+
ENVIRONMENT:
|
|
65
|
+
Reads .env from current directory. Required variables:
|
|
66
|
+
TELEGRAM_BOT_TOKEN - Bot token from @BotFather
|
|
67
|
+
TELEGRAM_ALLOWED_USERS - Comma-separated Telegram user IDs
|
|
68
|
+
|
|
69
|
+
EXAMPLES:
|
|
70
|
+
cd ~/my-project && ctb # Start bot for this project
|
|
71
|
+
ctb --dir=/path/to/project # Start bot for specific directory
|
|
72
|
+
ctb --token=xxx --users=123,456 # Override env vars
|
|
73
|
+
|
|
74
|
+
Multiple instances can run simultaneously in different directories.
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function prompt(question: string): Promise<string> {
|
|
79
|
+
const rl = createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
rl.question(question, (answer) => {
|
|
86
|
+
rl.close();
|
|
87
|
+
resolve(answer.trim());
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadEnvFile(dir: string): Record<string, string> {
|
|
93
|
+
const envPath = resolve(dir, ".env");
|
|
94
|
+
const env: Record<string, string> = {};
|
|
95
|
+
|
|
96
|
+
if (!existsSync(envPath)) {
|
|
97
|
+
return env;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const content = readFileSync(envPath, "utf-8");
|
|
101
|
+
for (const line of content.split("\n")) {
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
104
|
+
|
|
105
|
+
const eqIndex = trimmed.indexOf("=");
|
|
106
|
+
if (eqIndex === -1) continue;
|
|
107
|
+
|
|
108
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
109
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
110
|
+
|
|
111
|
+
// Remove quotes if present
|
|
112
|
+
if (
|
|
113
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
114
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
115
|
+
) {
|
|
116
|
+
value = value.slice(1, -1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
env[key] = value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return env;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function saveEnvFile(dir: string, env: Record<string, string>): void {
|
|
126
|
+
const envPath = resolve(dir, ".env");
|
|
127
|
+
const lines: string[] = [];
|
|
128
|
+
|
|
129
|
+
// Preserve existing content
|
|
130
|
+
if (existsSync(envPath)) {
|
|
131
|
+
const existing = readFileSync(envPath, "utf-8");
|
|
132
|
+
const existingKeys = new Set<string>();
|
|
133
|
+
|
|
134
|
+
for (const line of existing.split("\n")) {
|
|
135
|
+
const trimmed = line.trim();
|
|
136
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
137
|
+
lines.push(line);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const eqIndex = trimmed.indexOf("=");
|
|
142
|
+
if (eqIndex !== -1) {
|
|
143
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
144
|
+
existingKeys.add(key);
|
|
145
|
+
// Use new value if provided, otherwise keep original
|
|
146
|
+
if (key in env) {
|
|
147
|
+
lines.push(`${key}=${env[key]}`);
|
|
148
|
+
} else {
|
|
149
|
+
lines.push(line);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
lines.push(line);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add new keys not in existing file
|
|
157
|
+
for (const [key, value] of Object.entries(env)) {
|
|
158
|
+
if (!existingKeys.has(key)) {
|
|
159
|
+
lines.push(`${key}=${value}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// New file
|
|
164
|
+
for (const [key, value] of Object.entries(env)) {
|
|
165
|
+
lines.push(`${key}=${value}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
writeFileSync(envPath, `${lines.join("\n")}\n`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function interactiveSetup(
|
|
173
|
+
dir: string,
|
|
174
|
+
existingEnv: Record<string, string>,
|
|
175
|
+
): Promise<{ token: string; users: string }> {
|
|
176
|
+
console.log(`\nNo .env found or missing required variables in ${dir}\n`);
|
|
177
|
+
|
|
178
|
+
let token = existingEnv.TELEGRAM_BOT_TOKEN || "";
|
|
179
|
+
let users = existingEnv.TELEGRAM_ALLOWED_USERS || "";
|
|
180
|
+
|
|
181
|
+
if (!token) {
|
|
182
|
+
console.log("Get a bot token from @BotFather on Telegram");
|
|
183
|
+
token = await prompt("Enter TELEGRAM_BOT_TOKEN: ");
|
|
184
|
+
if (!token) {
|
|
185
|
+
console.error("Token is required");
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!users) {
|
|
191
|
+
console.log(
|
|
192
|
+
"\nEnter your Telegram user ID(s). Find yours by messaging @userinfobot",
|
|
193
|
+
);
|
|
194
|
+
users = await prompt("Enter TELEGRAM_ALLOWED_USERS (comma-separated): ");
|
|
195
|
+
if (!users) {
|
|
196
|
+
console.error("At least one user ID is required");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Ask to save
|
|
202
|
+
const save = await prompt("\nSave to .env? (Y/n): ");
|
|
203
|
+
if (save.toLowerCase() !== "n") {
|
|
204
|
+
saveEnvFile(dir, {
|
|
205
|
+
...existingEnv,
|
|
206
|
+
TELEGRAM_BOT_TOKEN: token,
|
|
207
|
+
TELEGRAM_ALLOWED_USERS: users,
|
|
208
|
+
});
|
|
209
|
+
console.log(`Saved to ${resolve(dir, ".env")}\n`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { token, users };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function main(): Promise<void> {
|
|
216
|
+
const args = process.argv.slice(2);
|
|
217
|
+
const options = parseArgs(args);
|
|
218
|
+
|
|
219
|
+
if (options.help) {
|
|
220
|
+
showHelp();
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (options.version) {
|
|
225
|
+
console.log(`ctb version ${VERSION}`);
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Determine working directory
|
|
230
|
+
const workingDir = options.dir ? resolve(options.dir) : process.cwd();
|
|
231
|
+
|
|
232
|
+
// Load .env from working directory
|
|
233
|
+
const envFile = loadEnvFile(workingDir);
|
|
234
|
+
|
|
235
|
+
// Merge: CLI args > .env file > process.env
|
|
236
|
+
let token =
|
|
237
|
+
options.token ||
|
|
238
|
+
envFile.TELEGRAM_BOT_TOKEN ||
|
|
239
|
+
process.env.TELEGRAM_BOT_TOKEN ||
|
|
240
|
+
"";
|
|
241
|
+
let users =
|
|
242
|
+
options.users ||
|
|
243
|
+
envFile.TELEGRAM_ALLOWED_USERS ||
|
|
244
|
+
process.env.TELEGRAM_ALLOWED_USERS ||
|
|
245
|
+
"";
|
|
246
|
+
|
|
247
|
+
// Interactive setup if missing required vars
|
|
248
|
+
if (!token || !users) {
|
|
249
|
+
const setup = await interactiveSetup(workingDir, envFile);
|
|
250
|
+
token = token || setup.token;
|
|
251
|
+
users = users || setup.users;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Set environment variables for the bot
|
|
255
|
+
process.env.TELEGRAM_BOT_TOKEN = token;
|
|
256
|
+
process.env.TELEGRAM_ALLOWED_USERS = users;
|
|
257
|
+
process.env.CLAUDE_WORKING_DIR = workingDir;
|
|
258
|
+
|
|
259
|
+
// Pass through other env vars from .env file
|
|
260
|
+
for (const [key, value] of Object.entries(envFile)) {
|
|
261
|
+
if (!(key in process.env)) {
|
|
262
|
+
process.env[key] = value;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Set CTB_INSTANCE_DIR for session isolation
|
|
267
|
+
process.env.CTB_INSTANCE_DIR = workingDir;
|
|
268
|
+
|
|
269
|
+
console.log(`\nStarting ctb in ${workingDir}...\n`);
|
|
270
|
+
|
|
271
|
+
// Import and start the bot
|
|
272
|
+
await import("./bot.js");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
main().catch((error) => {
|
|
276
|
+
console.error("Fatal error:", error);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for Claude Telegram Bot.
|
|
3
|
+
*
|
|
4
|
+
* All environment variables, paths, constants, and safety settings.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import type { McpServerConfig } from "./types";
|
|
10
|
+
|
|
11
|
+
// ============== Environment Setup ==============
|
|
12
|
+
|
|
13
|
+
const HOME = homedir();
|
|
14
|
+
|
|
15
|
+
// Ensure necessary paths are available for Claude's bash commands
|
|
16
|
+
// LaunchAgents don't inherit the full shell environment
|
|
17
|
+
const EXTRA_PATHS = [
|
|
18
|
+
`${HOME}/.local/bin`,
|
|
19
|
+
`${HOME}/.bun/bin`,
|
|
20
|
+
"/opt/homebrew/bin",
|
|
21
|
+
"/opt/homebrew/sbin",
|
|
22
|
+
"/usr/local/bin",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const currentPath = process.env.PATH || "";
|
|
26
|
+
const pathParts = currentPath.split(":");
|
|
27
|
+
for (const extraPath of EXTRA_PATHS) {
|
|
28
|
+
if (!pathParts.includes(extraPath)) {
|
|
29
|
+
pathParts.unshift(extraPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
process.env.PATH = pathParts.join(":");
|
|
33
|
+
|
|
34
|
+
// ============== Core Configuration ==============
|
|
35
|
+
|
|
36
|
+
export const TELEGRAM_TOKEN = process.env.TELEGRAM_BOT_TOKEN || "";
|
|
37
|
+
export const ALLOWED_USERS: number[] = (
|
|
38
|
+
process.env.TELEGRAM_ALLOWED_USERS || ""
|
|
39
|
+
)
|
|
40
|
+
.split(",")
|
|
41
|
+
.filter((x) => x.trim())
|
|
42
|
+
.map((x) => parseInt(x.trim(), 10))
|
|
43
|
+
.filter((x) => !Number.isNaN(x));
|
|
44
|
+
|
|
45
|
+
export const WORKING_DIR = process.env.CLAUDE_WORKING_DIR || HOME;
|
|
46
|
+
export const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
|
|
47
|
+
|
|
48
|
+
// ============== Claude CLI Path ==============
|
|
49
|
+
|
|
50
|
+
// Auto-detect from PATH, or use environment override
|
|
51
|
+
function findClaudeCli(): string {
|
|
52
|
+
const envPath = process.env.CLAUDE_CLI_PATH;
|
|
53
|
+
if (envPath) return envPath;
|
|
54
|
+
|
|
55
|
+
// Try to find claude in PATH using Bun.which
|
|
56
|
+
const whichResult = Bun.which("claude");
|
|
57
|
+
if (whichResult) return whichResult;
|
|
58
|
+
|
|
59
|
+
// Final fallback
|
|
60
|
+
return "/usr/local/bin/claude";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const CLAUDE_CLI_PATH = findClaudeCli();
|
|
64
|
+
|
|
65
|
+
// ============== MCP Configuration ==============
|
|
66
|
+
|
|
67
|
+
// MCP servers loaded from mcp-config.ts
|
|
68
|
+
let MCP_SERVERS: Record<string, McpServerConfig> = {};
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Dynamic import of MCP config
|
|
72
|
+
const mcpConfigPath = resolve(dirname(import.meta.dir), "mcp-config.ts");
|
|
73
|
+
const mcpModule = await import(mcpConfigPath).catch(() => null);
|
|
74
|
+
if (mcpModule?.MCP_SERVERS) {
|
|
75
|
+
MCP_SERVERS = mcpModule.MCP_SERVERS;
|
|
76
|
+
console.log(
|
|
77
|
+
`Loaded ${Object.keys(MCP_SERVERS).length} MCP servers from mcp-config.ts`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
console.log("No mcp-config.ts found - running without MCPs");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { MCP_SERVERS };
|
|
85
|
+
|
|
86
|
+
// ============== Security Configuration ==============
|
|
87
|
+
|
|
88
|
+
// Allowed directories for file operations
|
|
89
|
+
const defaultAllowedPaths = [
|
|
90
|
+
WORKING_DIR,
|
|
91
|
+
`${HOME}/Documents`,
|
|
92
|
+
`${HOME}/Downloads`,
|
|
93
|
+
`${HOME}/Desktop`,
|
|
94
|
+
`${HOME}/.claude`, // Claude Code data (plans, settings)
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const allowedPathsStr = process.env.ALLOWED_PATHS || "";
|
|
98
|
+
export const ALLOWED_PATHS: string[] = allowedPathsStr
|
|
99
|
+
? allowedPathsStr
|
|
100
|
+
.split(",")
|
|
101
|
+
.map((p) => p.trim())
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
: defaultAllowedPaths;
|
|
104
|
+
|
|
105
|
+
// Build safety prompt dynamically from ALLOWED_PATHS
|
|
106
|
+
function buildSafetyPrompt(allowedPaths: string[]): string {
|
|
107
|
+
const pathsList = allowedPaths
|
|
108
|
+
.map((p) => ` - ${p} (and subdirectories)`)
|
|
109
|
+
.join("\n");
|
|
110
|
+
|
|
111
|
+
return `
|
|
112
|
+
CRITICAL SAFETY RULES FOR TELEGRAM BOT:
|
|
113
|
+
|
|
114
|
+
1. NEVER delete, remove, or overwrite files without EXPLICIT confirmation from the user.
|
|
115
|
+
- If user asks to delete something, respond: "Are you sure you want to delete [file]? Reply 'yes delete it' to confirm."
|
|
116
|
+
- Only proceed with deletion if user replies with explicit confirmation like "yes delete it", "confirm delete"
|
|
117
|
+
- This applies to: rm, trash, unlink, shred, or any file deletion
|
|
118
|
+
|
|
119
|
+
2. You can ONLY access files in these directories:
|
|
120
|
+
${pathsList}
|
|
121
|
+
- REFUSE any file operations outside these paths
|
|
122
|
+
|
|
123
|
+
3. NEVER run dangerous commands like:
|
|
124
|
+
- rm -rf (recursive force delete)
|
|
125
|
+
- Any command that affects files outside allowed directories
|
|
126
|
+
- Commands that could damage the system
|
|
127
|
+
|
|
128
|
+
4. For any destructive or irreversible action, ALWAYS ask for confirmation first.
|
|
129
|
+
|
|
130
|
+
You are running via Telegram, so the user cannot easily undo mistakes. Be extra careful!
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const SAFETY_PROMPT = buildSafetyPrompt(ALLOWED_PATHS);
|
|
135
|
+
|
|
136
|
+
// Dangerous command patterns to block
|
|
137
|
+
export const BLOCKED_PATTERNS = [
|
|
138
|
+
"rm -rf /",
|
|
139
|
+
"rm -rf ~",
|
|
140
|
+
"rm -rf $HOME",
|
|
141
|
+
"sudo rm",
|
|
142
|
+
":(){ :|:& };:", // Fork bomb
|
|
143
|
+
"> /dev/sd",
|
|
144
|
+
"mkfs.",
|
|
145
|
+
"dd if=",
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
// Query timeout (3 minutes)
|
|
149
|
+
export const QUERY_TIMEOUT_MS = 180_000;
|
|
150
|
+
|
|
151
|
+
// ============== Voice Transcription ==============
|
|
152
|
+
|
|
153
|
+
const BASE_TRANSCRIPTION_PROMPT = `Transcribe this voice message accurately.
|
|
154
|
+
The speaker may use multiple languages (English, and possibly others).
|
|
155
|
+
Focus on accuracy for proper nouns, technical terms, and commands.`;
|
|
156
|
+
|
|
157
|
+
const TRANSCRIPTION_CONTEXT = process.env.TRANSCRIPTION_CONTEXT || "";
|
|
158
|
+
|
|
159
|
+
export const TRANSCRIPTION_PROMPT = TRANSCRIPTION_CONTEXT
|
|
160
|
+
? `${BASE_TRANSCRIPTION_PROMPT}\n\nAdditional context:\n${TRANSCRIPTION_CONTEXT}`
|
|
161
|
+
: BASE_TRANSCRIPTION_PROMPT;
|
|
162
|
+
|
|
163
|
+
export const TRANSCRIPTION_AVAILABLE = !!OPENAI_API_KEY;
|
|
164
|
+
|
|
165
|
+
// ============== Thinking Keywords ==============
|
|
166
|
+
|
|
167
|
+
const thinkingKeywordsStr =
|
|
168
|
+
process.env.THINKING_KEYWORDS || "think,pensa,ragiona";
|
|
169
|
+
const thinkingDeepKeywordsStr =
|
|
170
|
+
process.env.THINKING_DEEP_KEYWORDS || "ultrathink,think hard,pensa bene";
|
|
171
|
+
|
|
172
|
+
export const THINKING_KEYWORDS = thinkingKeywordsStr
|
|
173
|
+
.split(",")
|
|
174
|
+
.map((k) => k.trim().toLowerCase());
|
|
175
|
+
export const THINKING_DEEP_KEYWORDS = thinkingDeepKeywordsStr
|
|
176
|
+
.split(",")
|
|
177
|
+
.map((k) => k.trim().toLowerCase());
|
|
178
|
+
|
|
179
|
+
// ============== Media Group Settings ==============
|
|
180
|
+
|
|
181
|
+
export const MEDIA_GROUP_TIMEOUT = 1000; // ms to wait for more photos in a group
|
|
182
|
+
|
|
183
|
+
// ============== Telegram Message Limits ==============
|
|
184
|
+
|
|
185
|
+
export const TELEGRAM_MESSAGE_LIMIT = 4096; // Max characters per message
|
|
186
|
+
export const TELEGRAM_SAFE_LIMIT = 4000; // Safe limit with buffer for formatting
|
|
187
|
+
export const STREAMING_THROTTLE_MS = 500; // Throttle streaming updates
|
|
188
|
+
export const BUTTON_LABEL_MAX_LENGTH = 30; // Max chars for inline button labels
|
|
189
|
+
|
|
190
|
+
// ============== Audit Logging ==============
|
|
191
|
+
|
|
192
|
+
export const AUDIT_LOG_PATH =
|
|
193
|
+
process.env.AUDIT_LOG_PATH || "/tmp/claude-telegram-audit.log";
|
|
194
|
+
export const AUDIT_LOG_JSON =
|
|
195
|
+
(process.env.AUDIT_LOG_JSON || "false").toLowerCase() === "true";
|
|
196
|
+
|
|
197
|
+
// ============== Rate Limiting ==============
|
|
198
|
+
|
|
199
|
+
export const RATE_LIMIT_ENABLED =
|
|
200
|
+
(process.env.RATE_LIMIT_ENABLED || "true").toLowerCase() === "true";
|
|
201
|
+
export const RATE_LIMIT_REQUESTS = parseInt(
|
|
202
|
+
process.env.RATE_LIMIT_REQUESTS || "20",
|
|
203
|
+
10,
|
|
204
|
+
);
|
|
205
|
+
export const RATE_LIMIT_WINDOW = parseInt(
|
|
206
|
+
process.env.RATE_LIMIT_WINDOW || "60",
|
|
207
|
+
10,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// ============== File Paths ==============
|
|
211
|
+
|
|
212
|
+
// Generate a short hash for instance isolation
|
|
213
|
+
function hashDir(dir: string): string {
|
|
214
|
+
let hash = 0;
|
|
215
|
+
for (let i = 0; i < dir.length; i++) {
|
|
216
|
+
const char = dir.charCodeAt(i);
|
|
217
|
+
hash = ((hash << 5) - hash + char) | 0;
|
|
218
|
+
}
|
|
219
|
+
return Math.abs(hash).toString(36).slice(0, 8);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Instance directory for session isolation (set by CLI or defaults to working dir)
|
|
223
|
+
const INSTANCE_DIR = process.env.CTB_INSTANCE_DIR || WORKING_DIR;
|
|
224
|
+
const INSTANCE_HASH = hashDir(INSTANCE_DIR);
|
|
225
|
+
const INSTANCE_TEMP_DIR = `/tmp/ctb-${INSTANCE_HASH}`;
|
|
226
|
+
|
|
227
|
+
export const SESSION_FILE = `${INSTANCE_TEMP_DIR}/session.json`;
|
|
228
|
+
export const RESTART_FILE = `${INSTANCE_TEMP_DIR}/restart.json`;
|
|
229
|
+
export const TEMP_DIR = `${INSTANCE_TEMP_DIR}/downloads`;
|
|
230
|
+
|
|
231
|
+
// Temp paths that are always allowed for bot operations
|
|
232
|
+
export const TEMP_PATHS = ["/tmp/", "/private/tmp/", "/var/folders/"];
|
|
233
|
+
|
|
234
|
+
// Ensure temp directories exist
|
|
235
|
+
await Bun.write(`${INSTANCE_TEMP_DIR}/.keep`, "");
|
|
236
|
+
await Bun.write(`${TEMP_DIR}/.keep`, "");
|
|
237
|
+
|
|
238
|
+
// ============== Validation ==============
|
|
239
|
+
|
|
240
|
+
if (!TELEGRAM_TOKEN) {
|
|
241
|
+
console.error("ERROR: TELEGRAM_BOT_TOKEN environment variable is required");
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (ALLOWED_USERS.length === 0) {
|
|
246
|
+
console.error(
|
|
247
|
+
"ERROR: TELEGRAM_ALLOWED_USERS environment variable is required",
|
|
248
|
+
);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(
|
|
253
|
+
`Config loaded: ${ALLOWED_USERS.length} allowed users, working dir: ${WORKING_DIR}`,
|
|
254
|
+
);
|