@gonzih/cc-tg 0.3.6 → 0.3.8

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/dist/index.js DELETED
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * cc-tg — Claude Code Telegram bot
4
- *
5
- * Usage:
6
- * npx @gonzih/cc-tg
7
- *
8
- * Required env:
9
- * TELEGRAM_BOT_TOKEN — from @BotFather
10
- * CLAUDE_CODE_TOKEN — your Claude Code OAuth token (or ANTHROPIC_API_KEY)
11
- *
12
- * Optional env:
13
- * ALLOWED_USER_IDS — comma-separated Telegram user IDs (leave empty to allow all)
14
- * GROUP_CHAT_IDS — comma-separated Telegram group/supergroup chat IDs (leave empty to allow all groups)
15
- * CWD — working directory for Claude Code (default: process.cwd())
16
- */
17
- import { createServer, createConnection } from "net";
18
- import { unlinkSync, readFileSync } from "fs";
19
- import { tmpdir } from "os";
20
- import os from "os";
21
- import { join, dirname } from "path";
22
- import { fileURLToPath } from "url";
23
- import { CcTgBot } from "./bot.js";
24
- import { Registry, startControlServer } from "@gonzih/agent-ops";
25
- import { Redis } from "ioredis";
26
- const __filename = fileURLToPath(import.meta.url);
27
- const __dirname = dirname(__filename);
28
- const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
29
- // Make lock socket unique per bot token so multiple users on the same machine don't collide
30
- const _tokenHash = Buffer.from(process.env.TELEGRAM_BOT_TOKEN ?? "default").toString("base64").replace(/[^a-z0-9]/gi, "").slice(0, 16);
31
- const LOCK_SOCKET = join(tmpdir(), `cc-tg-${_tokenHash}.sock`);
32
- function acquireLock() {
33
- return new Promise((resolve) => {
34
- const server = createServer();
35
- server.listen(LOCK_SOCKET, () => {
36
- // Bound successfully — we own the lock. Socket auto-released on any exit incl. SIGKILL.
37
- resolve(true);
38
- });
39
- server.on("error", (err) => {
40
- if (err.code !== "EADDRINUSE") {
41
- resolve(true); // unrelated error, proceed
42
- return;
43
- }
44
- // Socket path exists — probe if anything is actually listening
45
- const probe = createConnection(LOCK_SOCKET);
46
- probe.on("connect", () => {
47
- probe.destroy();
48
- console.error("[cc-tg] Another instance is already running. Exiting.");
49
- resolve(false);
50
- });
51
- probe.on("error", () => {
52
- // Nothing listening — stale socket, remove and retry
53
- try {
54
- unlinkSync(LOCK_SOCKET);
55
- }
56
- catch { }
57
- const retry = createServer();
58
- retry.listen(LOCK_SOCKET, () => resolve(true));
59
- retry.on("error", () => resolve(true)); // give up on lock, just start
60
- });
61
- });
62
- });
63
- }
64
- const lockAcquired = await acquireLock();
65
- if (!lockAcquired) {
66
- process.exit(1);
67
- }
68
- function required(name) {
69
- const val = process.env[name];
70
- if (!val) {
71
- console.error(`
72
- ERROR: ${name} is not set.
73
-
74
- cc-tg requires:
75
- TELEGRAM_BOT_TOKEN — get one from @BotFather on Telegram
76
- CLAUDE_CODE_TOKEN — your Claude Code OAuth token
77
-
78
- Set them and run again:
79
- TELEGRAM_BOT_TOKEN=xxx CLAUDE_CODE_TOKEN=yyy npx @gonzih/cc-tg
80
-
81
- Or add to your shell profile / .env file.
82
- `);
83
- process.exit(1);
84
- }
85
- return val;
86
- }
87
- const telegramToken = required("TELEGRAM_BOT_TOKEN");
88
- // Accept CLAUDE_CODE_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or ANTHROPIC_API_KEY
89
- const claudeToken = process.env.CLAUDE_CODE_TOKEN ??
90
- process.env.CLAUDE_CODE_OAUTH_TOKEN ??
91
- process.env.ANTHROPIC_API_KEY;
92
- if (!claudeToken) {
93
- console.error(`
94
- ERROR: No Claude token set. Set one of: CLAUDE_CODE_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or ANTHROPIC_API_KEY.
95
-
96
- Set one and run again:
97
- TELEGRAM_BOT_TOKEN=xxx CLAUDE_CODE_TOKEN=yyy npx @gonzih/cc-tg
98
- `);
99
- process.exit(1);
100
- }
101
- const allowedUserIds = process.env.ALLOWED_USER_IDS
102
- ? process.env.ALLOWED_USER_IDS.split(",").map((s) => parseInt(s.trim(), 10)).filter(Boolean)
103
- : [];
104
- const groupChatIds = process.env.GROUP_CHAT_IDS
105
- ? process.env.GROUP_CHAT_IDS.split(",").map((s) => parseInt(s.trim(), 10)).filter(Boolean)
106
- : [];
107
- const cwd = process.env.CWD ?? process.cwd();
108
- const bot = new CcTgBot({
109
- telegramToken,
110
- claudeToken,
111
- cwd,
112
- allowedUserIds,
113
- groupChatIds,
114
- });
115
- // agent-ops: optional self-registration + HTTP control endpoint
116
- if (process.env.CC_AGENT_OPS_PORT) {
117
- const botInfo = await bot.getMe();
118
- const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
119
- const registry = new Registry(redis);
120
- const namespace = process.env.CC_AGENT_NAMESPACE || "default";
121
- await registry.register({
122
- namespace,
123
- hostname: os.hostname(),
124
- user: os.userInfo().username,
125
- pid: String(process.pid),
126
- version: pkg.version,
127
- cwd: process.env.CWD || process.cwd(),
128
- control_port: process.env.CC_AGENT_OPS_PORT,
129
- bot_username: botInfo.username ?? "",
130
- started_at: new Date().toISOString(),
131
- });
132
- setInterval(() => registry.heartbeat(namespace), 60_000);
133
- startControlServer(Number(process.env.CC_AGENT_OPS_PORT), {
134
- namespace,
135
- version: pkg.version,
136
- logFile: process.env.CC_AGENT_LOG_FILE || process.env.LOG_FILE,
137
- });
138
- console.log(`[ops] control server on port ${process.env.CC_AGENT_OPS_PORT}`);
139
- }
140
- process.on("SIGINT", () => {
141
- console.log("\nShutting down...");
142
- bot.stop();
143
- process.exit(0);
144
- });
145
- process.on("SIGTERM", () => {
146
- bot.stop();
147
- process.exit(0);
148
- });
@@ -1,7 +0,0 @@
1
- export interface UsageLimitSignal {
2
- detected: boolean;
3
- reason: 'usage_exhausted' | 'rate_limit';
4
- retryAfterMs: number;
5
- humanMessage: string;
6
- }
7
- export declare function detectUsageLimit(text: string): UsageLimitSignal;
@@ -1,29 +0,0 @@
1
- export function detectUsageLimit(text) {
2
- const lower = text.toLowerCase();
3
- if (lower.includes('extra usage') ||
4
- lower.includes('usage has been disabled') ||
5
- lower.includes('billing_error') ||
6
- lower.includes('usage limit')) {
7
- const wake = nextHourBoundary() + 5 * 60 * 1000;
8
- return {
9
- detected: true,
10
- reason: 'usage_exhausted',
11
- retryAfterMs: wake - Date.now(),
12
- humanMessage: `⏸ Claude usage limit reached. Will auto-resume at ${new Date(wake).toUTCString()}. I'll message you when it's back.`,
13
- };
14
- }
15
- if (lower.includes('rate limit') || lower.includes('overloaded')) {
16
- return {
17
- detected: true,
18
- reason: 'rate_limit',
19
- retryAfterMs: 2 * 60 * 1000,
20
- humanMessage: `⏸ Rate limited. Retrying in 2 minutes...`,
21
- };
22
- }
23
- return { detected: false, reason: 'rate_limit', retryAfterMs: 0, humanMessage: '' };
24
- }
25
- function nextHourBoundary() {
26
- const d = new Date();
27
- d.setHours(d.getHours() + 1, 0, 0, 0);
28
- return d.getTime();
29
- }
package/dist/voice.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * Voice message transcription via whisper.cpp.
3
- * Flow: Telegram OGG → ffmpeg convert to 16kHz WAV → whisper-cpp → text
4
- */
5
- /**
6
- * Transcribe a voice message from a Telegram file URL.
7
- * Returns the transcribed text, or throws if whisper/ffmpeg not available.
8
- */
9
- export declare function transcribeVoice(fileUrl: string): Promise<string>;
10
- /**
11
- * Check if voice transcription is available on this system.
12
- */
13
- export declare function isVoiceAvailable(): boolean;
package/dist/voice.js DELETED
@@ -1,124 +0,0 @@
1
- /**
2
- * Voice message transcription via whisper.cpp.
3
- * Flow: Telegram OGG → ffmpeg convert to 16kHz WAV → whisper-cpp → text
4
- */
5
- import { execFile } from "child_process";
6
- import { promisify } from "util";
7
- import { existsSync } from "fs";
8
- import { unlink } from "fs/promises";
9
- import { tmpdir } from "os";
10
- import { join } from "path";
11
- import https from "https";
12
- import http from "http";
13
- import { createWriteStream } from "fs";
14
- const execFileAsync = promisify(execFile);
15
- // Whisper model — small.en is fast and accurate enough for commands
16
- // Falls back to base.en if small not found
17
- const WHISPER_MODELS = [
18
- "/opt/homebrew/share/whisper-cpp/ggml-small.en.bin",
19
- "/opt/homebrew/share/whisper-cpp/ggml-small.bin",
20
- "/opt/homebrew/share/whisper-cpp/ggml-base.en.bin",
21
- "/opt/homebrew/share/whisper-cpp/ggml-base.bin",
22
- // user-local
23
- `${process.env.HOME}/.local/share/whisper-cpp/ggml-small.en.bin`,
24
- `${process.env.HOME}/.local/share/whisper-cpp/ggml-base.en.bin`,
25
- ];
26
- const WHISPER_BIN_CANDIDATES = [
27
- "/opt/homebrew/bin/whisper-cli", // whisper-cpp brew formula installs as whisper-cli
28
- "/opt/homebrew/bin/whisper-cpp",
29
- "/usr/local/bin/whisper-cli",
30
- "/usr/local/bin/whisper-cpp",
31
- "/opt/homebrew/bin/whisper",
32
- ];
33
- const FFMPEG_CANDIDATES = [
34
- "/opt/homebrew/bin/ffmpeg",
35
- "/usr/local/bin/ffmpeg",
36
- "/usr/bin/ffmpeg",
37
- ];
38
- function findBin(candidates) {
39
- for (const p of candidates) {
40
- if (existsSync(p))
41
- return p;
42
- }
43
- return null;
44
- }
45
- function findModel() {
46
- for (const p of WHISPER_MODELS) {
47
- if (existsSync(p))
48
- return p;
49
- }
50
- return null;
51
- }
52
- function downloadFile(url, dest) {
53
- return new Promise((resolve, reject) => {
54
- const file = createWriteStream(dest);
55
- const getter = url.startsWith("https") ? https : http;
56
- getter.get(url, (res) => {
57
- if (res.statusCode !== 200) {
58
- reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
59
- return;
60
- }
61
- res.pipe(file);
62
- file.on("finish", () => file.close(() => resolve()));
63
- file.on("error", reject);
64
- }).on("error", reject);
65
- });
66
- }
67
- /**
68
- * Transcribe a voice message from a Telegram file URL.
69
- * Returns the transcribed text, or throws if whisper/ffmpeg not available.
70
- */
71
- export async function transcribeVoice(fileUrl) {
72
- const whisperBin = findBin(WHISPER_BIN_CANDIDATES);
73
- if (!whisperBin)
74
- throw new Error("whisper-cpp not found — install with: brew install whisper-cpp");
75
- const ffmpegBin = findBin(FFMPEG_CANDIDATES);
76
- if (!ffmpegBin)
77
- throw new Error("ffmpeg not found — install with: brew install ffmpeg");
78
- const model = findModel();
79
- if (!model)
80
- throw new Error("No whisper model found — run: whisper-cpp-download-ggml-model small.en");
81
- const tmp = join(tmpdir(), `cc-tg-voice-${Date.now()}`);
82
- const oggPath = `${tmp}.ogg`;
83
- const wavPath = `${tmp}.wav`;
84
- try {
85
- // 1. Download OGG from Telegram
86
- await downloadFile(fileUrl, oggPath);
87
- // 2. Convert OGG → 16kHz mono WAV (whisper requirement)
88
- await execFileAsync(ffmpegBin, [
89
- "-y", "-i", oggPath,
90
- "-ar", "16000",
91
- "-ac", "1",
92
- "-c:a", "pcm_s16le",
93
- wavPath,
94
- ]);
95
- // 3. Run whisper-cpp
96
- const { stdout } = await execFileAsync(whisperBin, [
97
- "-m", model,
98
- "-f", wavPath,
99
- "--no-timestamps",
100
- "-l", "auto",
101
- "--output-txt",
102
- ]);
103
- // whisper outputs to stdout — strip leading/trailing whitespace and [BLANK_AUDIO] artifacts
104
- const text = stdout
105
- .replace(/\[BLANK_AUDIO\]/gi, "")
106
- .replace(/\[.*?\]/g, "") // remove timestamp artifacts
107
- .trim();
108
- return text || "[empty transcription]";
109
- }
110
- finally {
111
- // Cleanup temp files
112
- await unlink(oggPath).catch(() => { });
113
- await unlink(wavPath).catch(() => { });
114
- await unlink(`${wavPath}.txt`).catch(() => { });
115
- }
116
- }
117
- /**
118
- * Check if voice transcription is available on this system.
119
- */
120
- export function isVoiceAvailable() {
121
- return (findBin(WHISPER_BIN_CANDIDATES) !== null &&
122
- findBin(FFMPEG_CANDIDATES) !== null &&
123
- findModel() !== null);
124
- }