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/utils.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for Claude Telegram Bot.
|
|
3
|
+
*
|
|
4
|
+
* Audit logging, voice transcription, typing indicator.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Context } from "grammy";
|
|
8
|
+
import type { Chat } from "grammy/types";
|
|
9
|
+
import OpenAI from "openai";
|
|
10
|
+
import {
|
|
11
|
+
AUDIT_LOG_JSON,
|
|
12
|
+
AUDIT_LOG_PATH,
|
|
13
|
+
OPENAI_API_KEY,
|
|
14
|
+
TRANSCRIPTION_AVAILABLE,
|
|
15
|
+
TRANSCRIPTION_PROMPT,
|
|
16
|
+
} from "./config";
|
|
17
|
+
import type { AuditEvent } from "./types";
|
|
18
|
+
|
|
19
|
+
// ============== OpenAI Client ==============
|
|
20
|
+
|
|
21
|
+
let openaiClient: OpenAI | null = null;
|
|
22
|
+
if (OPENAI_API_KEY && TRANSCRIPTION_AVAILABLE) {
|
|
23
|
+
openaiClient = new OpenAI({ apiKey: OPENAI_API_KEY });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============== Audit Logging ==============
|
|
27
|
+
|
|
28
|
+
async function writeAuditLog(event: AuditEvent): Promise<void> {
|
|
29
|
+
try {
|
|
30
|
+
let content: string;
|
|
31
|
+
if (AUDIT_LOG_JSON) {
|
|
32
|
+
content = JSON.stringify(event) + "\n";
|
|
33
|
+
} else {
|
|
34
|
+
// Plain text format for readability
|
|
35
|
+
const lines = ["\n" + "=".repeat(60)];
|
|
36
|
+
for (const [key, value] of Object.entries(event)) {
|
|
37
|
+
let displayValue = value;
|
|
38
|
+
if (
|
|
39
|
+
(key === "content" || key === "response") &&
|
|
40
|
+
String(value).length > 500
|
|
41
|
+
) {
|
|
42
|
+
displayValue = String(value).slice(0, 500) + "...";
|
|
43
|
+
}
|
|
44
|
+
lines.push(`${key}: ${displayValue}`);
|
|
45
|
+
}
|
|
46
|
+
content = lines.join("\n") + "\n";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Append to audit log file
|
|
50
|
+
const fs = await import("fs/promises");
|
|
51
|
+
await fs.appendFile(AUDIT_LOG_PATH, content);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error("Failed to write audit log:", error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function auditLog(
|
|
58
|
+
userId: number,
|
|
59
|
+
username: string,
|
|
60
|
+
messageType: string,
|
|
61
|
+
content: string,
|
|
62
|
+
response = "",
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
const event: AuditEvent = {
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
event: "message",
|
|
67
|
+
user_id: userId,
|
|
68
|
+
username,
|
|
69
|
+
message_type: messageType,
|
|
70
|
+
content,
|
|
71
|
+
};
|
|
72
|
+
if (response) {
|
|
73
|
+
event.response = response;
|
|
74
|
+
}
|
|
75
|
+
await writeAuditLog(event);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function auditLogAuth(
|
|
79
|
+
userId: number,
|
|
80
|
+
username: string,
|
|
81
|
+
authorized: boolean,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
await writeAuditLog({
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
event: "auth",
|
|
86
|
+
user_id: userId,
|
|
87
|
+
username,
|
|
88
|
+
authorized,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function auditLogTool(
|
|
93
|
+
userId: number,
|
|
94
|
+
username: string,
|
|
95
|
+
toolName: string,
|
|
96
|
+
toolInput: Record<string, unknown>,
|
|
97
|
+
blocked = false,
|
|
98
|
+
reason = "",
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const event: AuditEvent = {
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
event: "tool_use",
|
|
103
|
+
user_id: userId,
|
|
104
|
+
username,
|
|
105
|
+
tool_name: toolName,
|
|
106
|
+
tool_input: toolInput,
|
|
107
|
+
blocked,
|
|
108
|
+
};
|
|
109
|
+
if (blocked && reason) {
|
|
110
|
+
event.reason = reason;
|
|
111
|
+
}
|
|
112
|
+
await writeAuditLog(event);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function auditLogError(
|
|
116
|
+
userId: number,
|
|
117
|
+
username: string,
|
|
118
|
+
error: string,
|
|
119
|
+
context = "",
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const event: AuditEvent = {
|
|
122
|
+
timestamp: new Date().toISOString(),
|
|
123
|
+
event: "error",
|
|
124
|
+
user_id: userId,
|
|
125
|
+
username,
|
|
126
|
+
error,
|
|
127
|
+
};
|
|
128
|
+
if (context) {
|
|
129
|
+
event.context = context;
|
|
130
|
+
}
|
|
131
|
+
await writeAuditLog(event);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function auditLogRateLimit(
|
|
135
|
+
userId: number,
|
|
136
|
+
username: string,
|
|
137
|
+
retryAfter: number,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
await writeAuditLog({
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
event: "rate_limit",
|
|
142
|
+
user_id: userId,
|
|
143
|
+
username,
|
|
144
|
+
retry_after: retryAfter,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============== Voice Transcription ==============
|
|
149
|
+
|
|
150
|
+
export async function transcribeVoice(
|
|
151
|
+
filePath: string,
|
|
152
|
+
): Promise<string | null> {
|
|
153
|
+
if (!openaiClient) {
|
|
154
|
+
console.warn("OpenAI client not available for transcription");
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const file = Bun.file(filePath);
|
|
160
|
+
const transcript = await openaiClient.audio.transcriptions.create({
|
|
161
|
+
model: "gpt-4o-transcribe",
|
|
162
|
+
file: file,
|
|
163
|
+
prompt: TRANSCRIPTION_PROMPT,
|
|
164
|
+
});
|
|
165
|
+
return transcript.text;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error("Transcription failed:", error);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============== Typing Indicator ==============
|
|
173
|
+
|
|
174
|
+
export interface TypingController {
|
|
175
|
+
stop: () => void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function startTypingIndicator(ctx: Context): TypingController {
|
|
179
|
+
let running = true;
|
|
180
|
+
|
|
181
|
+
const loop = async () => {
|
|
182
|
+
while (running) {
|
|
183
|
+
try {
|
|
184
|
+
await ctx.replyWithChatAction("typing");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// Stop loop if context is no longer valid
|
|
187
|
+
if (
|
|
188
|
+
String(error).includes("chat not found") ||
|
|
189
|
+
String(error).includes("bot was blocked")
|
|
190
|
+
) {
|
|
191
|
+
running = false;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
console.debug("Typing indicator failed:", error);
|
|
195
|
+
}
|
|
196
|
+
await Bun.sleep(4000);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Start the loop with proper error handling
|
|
201
|
+
loop().catch((error) => {
|
|
202
|
+
console.debug("Typing indicator loop error:", error);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
stop: () => {
|
|
207
|
+
running = false;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============== Message Interrupt ==============
|
|
213
|
+
|
|
214
|
+
// Import session lazily to avoid circular dependency
|
|
215
|
+
let sessionModule: {
|
|
216
|
+
session: {
|
|
217
|
+
isRunning: boolean;
|
|
218
|
+
stop: () => Promise<"stopped" | "pending" | false>;
|
|
219
|
+
markInterrupt: () => void;
|
|
220
|
+
clearStopRequested: () => void;
|
|
221
|
+
};
|
|
222
|
+
} | null = null;
|
|
223
|
+
|
|
224
|
+
export async function checkInterrupt(text: string): Promise<string> {
|
|
225
|
+
if (!text || !text.startsWith("!")) {
|
|
226
|
+
return text;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Lazy import to avoid circular dependency
|
|
230
|
+
if (!sessionModule) {
|
|
231
|
+
sessionModule = await import("./session");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const strippedText = text.slice(1).trimStart();
|
|
235
|
+
|
|
236
|
+
if (sessionModule.session.isRunning) {
|
|
237
|
+
console.log("! prefix - interrupting current query");
|
|
238
|
+
sessionModule.session.markInterrupt();
|
|
239
|
+
await sessionModule.session.stop();
|
|
240
|
+
await Bun.sleep(100);
|
|
241
|
+
// Clear stopRequested so the new message can proceed
|
|
242
|
+
sessionModule.session.clearStopRequested();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return strippedText;
|
|
246
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|