@gonzih/cc-tg 0.9.16 → 0.9.18

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.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Claude Code subprocess wrapper.
3
+ * Mirrors ce_ce's mechanism: spawn `claude` CLI with stream-json I/O,
4
+ * pipe prompts in, parse streaming JSON messages out.
5
+ */
6
+ import { EventEmitter } from "events";
7
+ export type MessageType = "system" | "assistant" | "user" | "result";
8
+ export interface ClaudeMessage {
9
+ type: MessageType;
10
+ session_id?: string;
11
+ uuid?: string;
12
+ payload: Record<string, unknown>;
13
+ raw: Record<string, unknown>;
14
+ }
15
+ export interface ClaudeOptions {
16
+ cwd?: string;
17
+ systemPrompt?: string;
18
+ /** OAuth token (sk-ant-oat01-...) or API key (sk-ant-api03-...) */
19
+ token?: string;
20
+ }
21
+ export interface UsageEvent {
22
+ inputTokens: number;
23
+ outputTokens: number;
24
+ cacheReadTokens: number;
25
+ cacheWriteTokens: number;
26
+ }
27
+ export declare interface ClaudeProcess {
28
+ on(event: "message", listener: (msg: ClaudeMessage) => void): this;
29
+ on(event: "usage", listener: (usage: UsageEvent) => void): this;
30
+ on(event: "error", listener: (err: Error) => void): this;
31
+ on(event: "exit", listener: (code: number | null) => void): this;
32
+ on(event: "stderr", listener: (data: string) => void): this;
33
+ }
34
+ export declare class ClaudeProcess extends EventEmitter {
35
+ private proc;
36
+ private buffer;
37
+ private _exited;
38
+ constructor(opts?: ClaudeOptions);
39
+ sendPrompt(text: string): void;
40
+ /**
41
+ * Send an image (with optional text caption) to Claude via stream-json content blocks.
42
+ * mediaType: image/jpeg | image/png | image/gif | image/webp
43
+ */
44
+ sendImage(base64Data: string, mediaType: string, caption?: string): void;
45
+ kill(): void;
46
+ get exited(): boolean;
47
+ private drainBuffer;
48
+ private parseMessage;
49
+ }
50
+ /**
51
+ * Extract the text content from an assistant message payload.
52
+ * Handles both simple string content and content-block arrays.
53
+ */
54
+ export declare function extractText(msg: ClaudeMessage): string;
package/dist/claude.js ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Claude Code subprocess wrapper.
3
+ * Mirrors ce_ce's mechanism: spawn `claude` CLI with stream-json I/O,
4
+ * pipe prompts in, parse streaming JSON messages out.
5
+ */
6
+ import { spawn } from "child_process";
7
+ import { EventEmitter } from "events";
8
+ import { existsSync } from "fs";
9
+ export class ClaudeProcess extends EventEmitter {
10
+ proc;
11
+ buffer = "";
12
+ _exited = false;
13
+ constructor(opts = {}) {
14
+ super();
15
+ const args = [
16
+ "--continue",
17
+ "--output-format", "stream-json",
18
+ "--input-format", "stream-json",
19
+ "--print",
20
+ "--verbose",
21
+ "--dangerously-skip-permissions",
22
+ ];
23
+ if (opts.systemPrompt) {
24
+ args.push("--system-prompt", opts.systemPrompt);
25
+ }
26
+ const env = { ...process.env };
27
+ if (opts.token) {
28
+ // API keys start with sk-ant-api — set ANTHROPIC_API_KEY only
29
+ // Everything else (OAuth sk-ant-oat, setup-token format with #, etc.)
30
+ // goes into CLAUDE_CODE_OAUTH_TOKEN
31
+ // Mixing them causes "Invalid API key" errors
32
+ if (opts.token.startsWith("sk-ant-api")) {
33
+ env.ANTHROPIC_API_KEY = opts.token;
34
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
35
+ }
36
+ else {
37
+ env.CLAUDE_CODE_OAUTH_TOKEN = opts.token;
38
+ delete env.ANTHROPIC_API_KEY;
39
+ }
40
+ }
41
+ // Resolve claude binary — check common install locations if not in PATH
42
+ const claudeBin = resolveClaude(env.PATH);
43
+ this.proc = spawn(claudeBin, args, {
44
+ cwd: opts.cwd ?? process.cwd(),
45
+ env,
46
+ stdio: ["pipe", "pipe", "pipe"],
47
+ });
48
+ this.proc.stdout.on("data", (chunk) => {
49
+ this.buffer += chunk.toString();
50
+ this.drainBuffer();
51
+ });
52
+ this.proc.stderr.on("data", (chunk) => {
53
+ this.emit("stderr", chunk.toString());
54
+ });
55
+ this.proc.on("exit", (code) => {
56
+ this._exited = true;
57
+ this.emit("exit", code);
58
+ });
59
+ this.proc.on("error", (err) => {
60
+ this.emit("error", err);
61
+ });
62
+ }
63
+ sendPrompt(text) {
64
+ if (this._exited)
65
+ throw new Error("Claude process has exited");
66
+ const payload = JSON.stringify({
67
+ type: "user",
68
+ message: { role: "user", content: text },
69
+ });
70
+ this.proc.stdin.write(payload + "\n");
71
+ }
72
+ /**
73
+ * Send an image (with optional text caption) to Claude via stream-json content blocks.
74
+ * mediaType: image/jpeg | image/png | image/gif | image/webp
75
+ */
76
+ sendImage(base64Data, mediaType, caption) {
77
+ if (this._exited)
78
+ throw new Error("Claude process has exited");
79
+ const content = [];
80
+ if (caption) {
81
+ content.push({ type: "text", text: caption });
82
+ }
83
+ content.push({
84
+ type: "image",
85
+ source: {
86
+ type: "base64",
87
+ media_type: mediaType,
88
+ data: base64Data,
89
+ },
90
+ });
91
+ const payload = JSON.stringify({
92
+ type: "user",
93
+ message: { role: "user", content },
94
+ });
95
+ this.proc.stdin.write(payload + "\n");
96
+ }
97
+ kill() {
98
+ this.proc.kill();
99
+ }
100
+ get exited() {
101
+ return this._exited;
102
+ }
103
+ drainBuffer() {
104
+ const lines = this.buffer.split("\n");
105
+ // Last element may be incomplete — keep it
106
+ this.buffer = lines.pop() ?? "";
107
+ for (const line of lines) {
108
+ if (!line.trim())
109
+ continue;
110
+ let raw;
111
+ try {
112
+ raw = JSON.parse(line);
113
+ }
114
+ catch {
115
+ // Non-JSON line (startup noise etc.) — ignore
116
+ continue;
117
+ }
118
+ // Emit usage events from Anthropic API stream events passed through by Claude CLI
119
+ if (raw.type === "message_start") {
120
+ const usage = (raw.message?.usage);
121
+ if (usage) {
122
+ this.emit("usage", {
123
+ inputTokens: usage.input_tokens ?? 0,
124
+ outputTokens: 0, // output_tokens at message_start is always 0
125
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
126
+ cacheWriteTokens: usage.cache_creation_input_tokens ?? 0,
127
+ });
128
+ }
129
+ }
130
+ else if (raw.type === "message_delta") {
131
+ const usage = raw.usage;
132
+ if (usage?.output_tokens) {
133
+ this.emit("usage", {
134
+ inputTokens: 0,
135
+ outputTokens: usage.output_tokens,
136
+ cacheReadTokens: 0,
137
+ cacheWriteTokens: 0,
138
+ });
139
+ }
140
+ }
141
+ const msg = this.parseMessage(raw);
142
+ if (msg)
143
+ this.emit("message", msg);
144
+ }
145
+ }
146
+ parseMessage(raw) {
147
+ const type = raw.type;
148
+ if (!type)
149
+ return null;
150
+ return {
151
+ type,
152
+ session_id: raw.session_id,
153
+ uuid: raw.uuid,
154
+ payload: raw,
155
+ raw,
156
+ };
157
+ }
158
+ }
159
+ /**
160
+ * Extract the text content from an assistant message payload.
161
+ * Handles both simple string content and content-block arrays.
162
+ */
163
+ export function extractText(msg) {
164
+ const message = msg.payload.message;
165
+ if (!message) {
166
+ // result message type
167
+ if (msg.type === "result") {
168
+ return msg.payload.result ?? "";
169
+ }
170
+ return "";
171
+ }
172
+ const content = message.content;
173
+ if (typeof content === "string")
174
+ return content;
175
+ if (Array.isArray(content)) {
176
+ return content
177
+ .filter((b) => b.type === "text")
178
+ .map((b) => b.text)
179
+ .join("");
180
+ }
181
+ return "";
182
+ }
183
+ /**
184
+ * Resolve the claude CLI binary path.
185
+ * Checks PATH entries + common npm global install locations.
186
+ */
187
+ function resolveClaude(pathEnv) {
188
+ // Try PATH entries first
189
+ const dirs = (pathEnv ?? process.env.PATH ?? "").split(":");
190
+ for (const dir of dirs) {
191
+ const candidate = `${dir}/claude`;
192
+ if (existsSync(candidate))
193
+ return candidate;
194
+ }
195
+ // Common fallback locations
196
+ const fallbacks = [
197
+ `${process.env.HOME}/.npm-global/bin/claude`,
198
+ "/opt/homebrew/bin/claude",
199
+ "/usr/local/bin/claude",
200
+ "/usr/bin/claude",
201
+ ];
202
+ for (const p of fallbacks) {
203
+ if (existsSync(p))
204
+ return p;
205
+ }
206
+ // Last resort — let the OS resolve it (will throw ENOENT if missing)
207
+ return "claude";
208
+ }
package/dist/cron.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cron job manager for cc-tg.
3
+ * Persists jobs to <cwd>/.cc-tg/crons.json.
4
+ * Fires prompts into Claude sessions on schedule.
5
+ */
6
+ export interface CronJob {
7
+ id: string;
8
+ chatId: number;
9
+ intervalMs: number;
10
+ prompt: string;
11
+ createdAt: string;
12
+ schedule: string;
13
+ }
14
+ type FireCallback = (chatId: number, prompt: string) => void;
15
+ export declare class CronManager {
16
+ private jobs;
17
+ private storePath;
18
+ private fire;
19
+ constructor(cwd: string, fire: FireCallback);
20
+ /** Parse "every 30m", "every 2h", "every 1d" → ms */
21
+ static parseSchedule(schedule: string): number | null;
22
+ add(chatId: number, schedule: string, prompt: string): CronJob | null;
23
+ remove(chatId: number, id: string): boolean;
24
+ clearAll(chatId: number): number;
25
+ list(chatId: number): CronJob[];
26
+ update(chatId: number, id: string, updates: {
27
+ schedule?: string;
28
+ prompt?: string;
29
+ }): CronJob | null | false;
30
+ private persist;
31
+ private load;
32
+ }
33
+ export {};
package/dist/cron.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Cron job manager for cc-tg.
3
+ * Persists jobs to <cwd>/.cc-tg/crons.json.
4
+ * Fires prompts into Claude sessions on schedule.
5
+ */
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
7
+ import { join } from "path";
8
+ export class CronManager {
9
+ jobs = new Map();
10
+ storePath;
11
+ fire;
12
+ constructor(cwd, fire) {
13
+ this.storePath = join(cwd, ".cc-tg", "crons.json");
14
+ this.fire = fire;
15
+ this.load();
16
+ }
17
+ /** Parse "every 30m", "every 2h", "every 1d" → ms */
18
+ static parseSchedule(schedule) {
19
+ const m = schedule.trim().match(/^every\s+(\d+)(m|h|d)$/i);
20
+ if (!m)
21
+ return null;
22
+ const n = parseInt(m[1]);
23
+ const unit = m[2].toLowerCase();
24
+ if (unit === "m")
25
+ return n * 60_000;
26
+ if (unit === "h")
27
+ return n * 3_600_000;
28
+ if (unit === "d")
29
+ return n * 86_400_000;
30
+ return null;
31
+ }
32
+ add(chatId, schedule, prompt) {
33
+ const intervalMs = CronManager.parseSchedule(schedule);
34
+ if (!intervalMs)
35
+ return null;
36
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
37
+ const job = { id, chatId, intervalMs, prompt, schedule, createdAt: new Date().toISOString() };
38
+ const timer = setInterval(() => {
39
+ console.log(`[cron:${id}] firing for chat=${chatId} prompt="${prompt}"`);
40
+ this.fire(chatId, prompt);
41
+ }, intervalMs);
42
+ this.jobs.set(id, { ...job, timer });
43
+ this.persist();
44
+ return job;
45
+ }
46
+ remove(chatId, id) {
47
+ const job = this.jobs.get(id);
48
+ if (!job || job.chatId !== chatId)
49
+ return false;
50
+ clearInterval(job.timer);
51
+ this.jobs.delete(id);
52
+ this.persist();
53
+ return true;
54
+ }
55
+ clearAll(chatId) {
56
+ let count = 0;
57
+ for (const [id, job] of this.jobs) {
58
+ if (job.chatId === chatId) {
59
+ clearInterval(job.timer);
60
+ this.jobs.delete(id);
61
+ count++;
62
+ }
63
+ }
64
+ if (count)
65
+ this.persist();
66
+ return count;
67
+ }
68
+ list(chatId) {
69
+ return [...this.jobs.values()]
70
+ .filter((j) => j.chatId === chatId)
71
+ .map(({ timer: _t, ...j }) => j);
72
+ }
73
+ update(chatId, id, updates) {
74
+ const job = this.jobs.get(id);
75
+ if (!job || job.chatId !== chatId)
76
+ return false;
77
+ if (updates.schedule !== undefined) {
78
+ const intervalMs = CronManager.parseSchedule(updates.schedule);
79
+ if (!intervalMs)
80
+ return null;
81
+ job.intervalMs = intervalMs;
82
+ job.schedule = updates.schedule;
83
+ }
84
+ if (updates.prompt !== undefined) {
85
+ job.prompt = updates.prompt;
86
+ }
87
+ // Recreate timer so it uses updated intervalMs and always reads latest job.prompt
88
+ clearInterval(job.timer);
89
+ job.timer = setInterval(() => {
90
+ console.log(`[cron:${job.id}] firing for chat=${job.chatId} prompt="${job.prompt}"`);
91
+ this.fire(job.chatId, job.prompt);
92
+ }, job.intervalMs);
93
+ this.persist();
94
+ const { timer: _t, ...cronJob } = job;
95
+ return cronJob;
96
+ }
97
+ persist() {
98
+ try {
99
+ const dir = join(this.storePath, "..");
100
+ if (!existsSync(dir))
101
+ mkdirSync(dir, { recursive: true });
102
+ const data = [...this.jobs.values()].map(({ timer: _t, ...j }) => j);
103
+ writeFileSync(this.storePath, JSON.stringify(data, null, 2));
104
+ }
105
+ catch (err) {
106
+ console.error("[cron] persist error:", err.message);
107
+ }
108
+ }
109
+ load() {
110
+ if (!existsSync(this.storePath))
111
+ return;
112
+ try {
113
+ const data = JSON.parse(readFileSync(this.storePath, "utf8"));
114
+ for (const job of data) {
115
+ const timer = setInterval(() => {
116
+ console.log(`[cron:${job.id}] firing for chat=${job.chatId} prompt="${job.prompt}"`);
117
+ this.fire(job.chatId, job.prompt);
118
+ }, job.intervalMs);
119
+ this.jobs.set(job.id, { ...job, timer });
120
+ }
121
+ console.log(`[cron] loaded ${data.length} jobs from disk`);
122
+ }
123
+ catch (err) {
124
+ console.error("[cron] load error:", err.message);
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Telegram MarkdownV2 post-processor.
3
+ * Converts standard markdown to Telegram's MarkdownV2 format.
4
+ */
5
+ /**
6
+ * Convert standard markdown text to Telegram MarkdownV2 format.
7
+ *
8
+ * Processing order:
9
+ * 1. Extract code blocks (fenced + inline) — protect from further processing
10
+ * 2. Strip raw HTML tags
11
+ * 3. Convert --- → blank line
12
+ * 4. Convert ## headings → *bold*
13
+ * 5. Convert **bold** → *bold*
14
+ * 6. Convert - list items → • item
15
+ * 7. Escape MarkdownV2 special chars (outside code blocks)
16
+ * 8. Reinsert code blocks unchanged
17
+ */
18
+ export declare function formatForTelegram(text: string): string;
19
+ /**
20
+ * Split a long message at natural boundaries (paragraph > line > word).
21
+ * Never splits mid-word. Chunks are at most maxLen characters.
22
+ */
23
+ export declare function splitLongMessage(text: string, maxLen?: number): string[];
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Telegram MarkdownV2 post-processor.
3
+ * Converts standard markdown to Telegram's MarkdownV2 format.
4
+ */
5
+ /**
6
+ * Convert standard markdown text to Telegram MarkdownV2 format.
7
+ *
8
+ * Processing order:
9
+ * 1. Extract code blocks (fenced + inline) — protect from further processing
10
+ * 2. Strip raw HTML tags
11
+ * 3. Convert --- → blank line
12
+ * 4. Convert ## headings → *bold*
13
+ * 5. Convert **bold** → *bold*
14
+ * 6. Convert - list items → • item
15
+ * 7. Escape MarkdownV2 special chars (outside code blocks)
16
+ * 8. Reinsert code blocks unchanged
17
+ */
18
+ export function formatForTelegram(text) {
19
+ // Step 1: Extract code blocks and inline code to protect them
20
+ const placeholders = [];
21
+ // Fenced code blocks first (``` ... ```)
22
+ let out = text.replace(/```[\s\S]*?```/g, (match) => {
23
+ placeholders.push(match);
24
+ return `\x00P${placeholders.length - 1}\x00`;
25
+ });
26
+ // Inline code (`...`)
27
+ out = out.replace(/`[^`\n]+`/g, (match) => {
28
+ placeholders.push(match);
29
+ return `\x00P${placeholders.length - 1}\x00`;
30
+ });
31
+ // Step 2: Strip raw HTML tags
32
+ out = out.replace(/<[^>]+>/g, "");
33
+ // Step 3: Convert --- → blank line
34
+ out = out.replace(/^-{3,}$/gm, "");
35
+ // Step 4: Convert ## headings → *bold*
36
+ out = out.replace(/^#{1,6}\s+(.+)$/gm, "*$1*");
37
+ // Step 5: Convert **bold** → *bold*
38
+ out = out.replace(/\*\*(.+?)\*\*/gs, "*$1*");
39
+ // Step 6: Convert - list items → • item (leading - or * bullet)
40
+ out = out.replace(/^[ \t]*[-*]\s+(.+)$/gm, "• $1");
41
+ // Step 7: Escape MarkdownV2 special chars outside code blocks.
42
+ // Per Telegram spec, these must be escaped: _ [ ] ( ) ~ > # + - = | { } . ! \
43
+ // * is intentionally NOT escaped — it is used for bold formatting above.
44
+ out = out.replace(/([_\[\]()~>#+\-=|{}.!\\])/g, "\\$1");
45
+ // Step 8: Reinsert code blocks unchanged (no escaping inside them)
46
+ out = out.replace(/\x00P(\d+)\x00/g, (_, i) => placeholders[parseInt(i, 10)]);
47
+ return out;
48
+ }
49
+ /**
50
+ * Split a long message at natural boundaries (paragraph > line > word).
51
+ * Never splits mid-word. Chunks are at most maxLen characters.
52
+ */
53
+ export function splitLongMessage(text, maxLen = 4096) {
54
+ if (text.length <= maxLen)
55
+ return [text];
56
+ const chunks = [];
57
+ let remaining = text;
58
+ while (remaining.length > maxLen) {
59
+ const slice = remaining.slice(0, maxLen);
60
+ // Prefer paragraph boundary (\n\n)
61
+ const lastPara = slice.lastIndexOf("\n\n");
62
+ // Then line boundary (\n)
63
+ const lastLine = slice.lastIndexOf("\n");
64
+ // Then word boundary (space)
65
+ const lastSpace = slice.lastIndexOf(" ");
66
+ let splitAt;
67
+ if (lastPara > 0) {
68
+ splitAt = lastPara + 2;
69
+ }
70
+ else if (lastLine > 0) {
71
+ splitAt = lastLine + 1;
72
+ }
73
+ else if (lastSpace > 0) {
74
+ splitAt = lastSpace + 1;
75
+ }
76
+ else {
77
+ splitAt = maxLen;
78
+ }
79
+ chunks.push(remaining.slice(0, splitAt).trimEnd());
80
+ remaining = remaining.slice(splitAt).trimStart();
81
+ }
82
+ if (remaining.length > 0) {
83
+ chunks.push(remaining);
84
+ }
85
+ return chunks;
86
+ }
@@ -0,0 +1,17 @@
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
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,123 @@
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 } from "fs";
19
+ import { tmpdir } from "os";
20
+ import { join } from "path";
21
+ import { createHash } from "crypto";
22
+ import { CcTgBot } from "./bot.js";
23
+ // Derive socket path from token hash so multiple instances (feral/law/simorgh)
24
+ // never collide, and the path is stable across restarts on the same machine.
25
+ function lockSocketPath(token) {
26
+ const hash = createHash("sha256").update(token).digest("hex").slice(0, 12);
27
+ return join(tmpdir(), `cc-tg-${hash}.sock`);
28
+ }
29
+ function acquireLock(socketPath) {
30
+ return new Promise((resolve) => {
31
+ const server = createServer();
32
+ server.listen(socketPath, () => {
33
+ // Bound successfully — we own the lock. Socket auto-released on any exit incl. SIGKILL.
34
+ resolve(true);
35
+ });
36
+ server.on("error", (err) => {
37
+ if (err.code !== "EADDRINUSE") {
38
+ resolve(true); // unrelated error, proceed
39
+ return;
40
+ }
41
+ // Socket path exists — probe if anything is actually listening
42
+ const probe = createConnection(socketPath);
43
+ probe.on("connect", () => {
44
+ probe.destroy();
45
+ console.error("[cc-tg] Another instance is already running. Exiting.");
46
+ resolve(false);
47
+ });
48
+ probe.on("error", () => {
49
+ // Nothing listening — stale socket, remove and retry
50
+ try {
51
+ unlinkSync(socketPath);
52
+ }
53
+ catch { }
54
+ const retry = createServer();
55
+ retry.listen(socketPath, () => resolve(true));
56
+ retry.on("error", () => resolve(true)); // give up on lock, just start
57
+ });
58
+ });
59
+ });
60
+ }
61
+ function required(name) {
62
+ const val = process.env[name];
63
+ if (!val) {
64
+ console.error(`
65
+ ERROR: ${name} is not set.
66
+
67
+ cc-tg requires:
68
+ TELEGRAM_BOT_TOKEN — get one from @BotFather on Telegram
69
+ CLAUDE_CODE_TOKEN — your Claude Code OAuth token
70
+
71
+ Set them and run again:
72
+ TELEGRAM_BOT_TOKEN=xxx CLAUDE_CODE_TOKEN=yyy npx @gonzih/cc-tg
73
+
74
+ Or add to your shell profile / .env file.
75
+ `);
76
+ process.exit(1);
77
+ }
78
+ return val;
79
+ }
80
+ const telegramToken = required("TELEGRAM_BOT_TOKEN");
81
+ // Acquire lock before doing anything else. Socket derived from token hash so
82
+ // multiple instances (different bots / users) never share the same socket.
83
+ const LOCK_SOCKET = lockSocketPath(telegramToken);
84
+ const lockAcquired = await acquireLock(LOCK_SOCKET);
85
+ if (!lockAcquired) {
86
+ process.exit(1);
87
+ }
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
+ process.on("SIGINT", () => {
116
+ console.log("\nShutting down...");
117
+ bot.stop();
118
+ process.exit(0);
119
+ });
120
+ process.on("SIGTERM", () => {
121
+ bot.stop();
122
+ process.exit(0);
123
+ });
@@ -0,0 +1,7 @@
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;