@gonzih/cc-tg 0.7.0 → 0.7.1
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/package.json +1 -1
- package/dist/bot.d.ts +0 -66
- package/dist/bot.js +0 -1166
- package/dist/claude.d.ts +0 -54
- package/dist/claude.js +0 -208
- package/dist/formatter.d.ts +0 -25
- package/dist/formatter.js +0 -122
- package/dist/index.d.ts +0 -17
- package/dist/index.js +0 -169
- package/dist/notifier.d.ts +0 -36
- package/dist/notifier.js +0 -105
- package/dist/tokens.d.ts +0 -22
- package/dist/tokens.js +0 -56
- package/dist/usage-limit.d.ts +0 -7
- package/dist/usage-limit.js +0 -29
- package/dist/voice.d.ts +0 -13
- package/dist/voice.js +0 -124
package/dist/claude.d.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
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/formatter.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telegram HTML post-processor.
|
|
3
|
-
* Converts standard markdown to Telegram's HTML parse mode format.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Convert standard markdown text to Telegram HTML format.
|
|
7
|
-
*
|
|
8
|
-
* Processing order:
|
|
9
|
-
* 1. Extract fenced code blocks (``` ... ```) → <pre>, protect from further processing
|
|
10
|
-
* 2. Extract inline code (`...`) → <code>, protect from further processing
|
|
11
|
-
* 3. HTML-escape remaining text: & → & < → < > → >
|
|
12
|
-
* 4. Convert --- → blank line
|
|
13
|
-
* 5. Convert ## headings → <b>Heading</b>
|
|
14
|
-
* 6. Convert **bold** → <b>bold</b>
|
|
15
|
-
* 7. Convert - item / * item → • item
|
|
16
|
-
* 8. Convert *bold* → <b>bold</b>
|
|
17
|
-
* 9. Convert _italic_ → <i>italic</i>
|
|
18
|
-
* 10. Reinsert code blocks
|
|
19
|
-
*/
|
|
20
|
-
export declare function formatForTelegram(text: string): string;
|
|
21
|
-
/**
|
|
22
|
-
* Split a long message at natural boundaries (paragraph > line > word).
|
|
23
|
-
* Never splits mid-word or inside <pre> blocks. Chunks are at most maxLen characters.
|
|
24
|
-
*/
|
|
25
|
-
export declare function splitLongMessage(text: string, maxLen?: number): string[];
|
package/dist/formatter.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telegram HTML post-processor.
|
|
3
|
-
* Converts standard markdown to Telegram's HTML parse mode format.
|
|
4
|
-
*/
|
|
5
|
-
function htmlEscape(text) {
|
|
6
|
-
return text
|
|
7
|
-
.replace(/&/g, "&")
|
|
8
|
-
.replace(/</g, "<")
|
|
9
|
-
.replace(/>/g, ">");
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Convert standard markdown text to Telegram HTML format.
|
|
13
|
-
*
|
|
14
|
-
* Processing order:
|
|
15
|
-
* 1. Extract fenced code blocks (``` ... ```) → <pre>, protect from further processing
|
|
16
|
-
* 2. Extract inline code (`...`) → <code>, protect from further processing
|
|
17
|
-
* 3. HTML-escape remaining text: & → & < → < > → >
|
|
18
|
-
* 4. Convert --- → blank line
|
|
19
|
-
* 5. Convert ## headings → <b>Heading</b>
|
|
20
|
-
* 6. Convert **bold** → <b>bold</b>
|
|
21
|
-
* 7. Convert - item / * item → • item
|
|
22
|
-
* 8. Convert *bold* → <b>bold</b>
|
|
23
|
-
* 9. Convert _italic_ → <i>italic</i>
|
|
24
|
-
* 10. Reinsert code blocks
|
|
25
|
-
*/
|
|
26
|
-
export function formatForTelegram(text) {
|
|
27
|
-
const placeholders = [];
|
|
28
|
-
// Step 1: Extract fenced code blocks (``` ... ```) → <pre>
|
|
29
|
-
let out = text.replace(/```(?:\w*)\n?([\s\S]*?)```/g, (_, content) => {
|
|
30
|
-
placeholders.push(`<pre>${htmlEscape(content)}</pre>`);
|
|
31
|
-
return `\x00P${placeholders.length - 1}\x00`;
|
|
32
|
-
});
|
|
33
|
-
// Step 2: Extract inline code (`...`) → <code>
|
|
34
|
-
out = out.replace(/`([^`\n]+)`/g, (_, content) => {
|
|
35
|
-
placeholders.push(`<code>${htmlEscape(content)}</code>`);
|
|
36
|
-
return `\x00P${placeholders.length - 1}\x00`;
|
|
37
|
-
});
|
|
38
|
-
// Step 3: HTML-escape remaining text
|
|
39
|
-
out = htmlEscape(out);
|
|
40
|
-
// Step 4: Convert --- → blank line
|
|
41
|
-
out = out.replace(/^-{3,}$/gm, "");
|
|
42
|
-
// Step 5: Convert ## headings → <b>Heading</b>
|
|
43
|
-
out = out.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
|
|
44
|
-
// Step 6: Convert **bold** → <b>bold</b>
|
|
45
|
-
out = out.replace(/\*\*(.+?)\*\*/gs, "<b>$1</b>");
|
|
46
|
-
// Step 7: Convert - item / * item → • item
|
|
47
|
-
out = out.replace(/^[ \t]*[-*]\s+(.+)$/gm, "• $1");
|
|
48
|
-
// Step 8: Convert *bold* → <b>bold</b> (single asterisk, after bullets handled)
|
|
49
|
-
out = out.replace(/\*([^*\n]+)\*/g, "<b>$1</b>");
|
|
50
|
-
// Step 9: Convert _italic_ → <i>italic</i>
|
|
51
|
-
// Use word-boundary guards to avoid mangling snake_case identifiers
|
|
52
|
-
out = out.replace(/(?<![a-zA-Z0-9])_([^_\n]+?)_(?![a-zA-Z0-9])/g, "<i>$1</i>");
|
|
53
|
-
// Step 10: Reinsert code blocks
|
|
54
|
-
out = out.replace(/\x00P(\d+)\x00/g, (_, i) => placeholders[parseInt(i, 10)]);
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
function findPreRanges(text) {
|
|
58
|
-
const ranges = [];
|
|
59
|
-
const open = "<pre>";
|
|
60
|
-
const close = "</pre>";
|
|
61
|
-
let i = 0;
|
|
62
|
-
while (i < text.length) {
|
|
63
|
-
const start = text.indexOf(open, i);
|
|
64
|
-
if (start === -1)
|
|
65
|
-
break;
|
|
66
|
-
const end = text.indexOf(close, start);
|
|
67
|
-
if (end === -1)
|
|
68
|
-
break;
|
|
69
|
-
ranges.push([start, end + close.length]);
|
|
70
|
-
i = end + close.length;
|
|
71
|
-
}
|
|
72
|
-
return ranges;
|
|
73
|
-
}
|
|
74
|
-
function isInsidePre(pos, ranges) {
|
|
75
|
-
return ranges.some(([start, end]) => pos > start && pos < end);
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Split a long message at natural boundaries (paragraph > line > word).
|
|
79
|
-
* Never splits mid-word or inside <pre> blocks. Chunks are at most maxLen characters.
|
|
80
|
-
*/
|
|
81
|
-
export function splitLongMessage(text, maxLen = 4096) {
|
|
82
|
-
if (text.length <= maxLen)
|
|
83
|
-
return [text];
|
|
84
|
-
const chunks = [];
|
|
85
|
-
let remaining = text;
|
|
86
|
-
while (remaining.length > maxLen) {
|
|
87
|
-
const slice = remaining.slice(0, maxLen);
|
|
88
|
-
const preRanges = findPreRanges(remaining);
|
|
89
|
-
// Prefer paragraph boundary (\n\n)
|
|
90
|
-
const lastPara = slice.lastIndexOf("\n\n");
|
|
91
|
-
// Then line boundary (\n)
|
|
92
|
-
const lastLine = slice.lastIndexOf("\n");
|
|
93
|
-
// Then word boundary (space)
|
|
94
|
-
const lastSpace = slice.lastIndexOf(" ");
|
|
95
|
-
let splitAt;
|
|
96
|
-
if (lastPara > 0 && !isInsidePre(lastPara, preRanges)) {
|
|
97
|
-
splitAt = lastPara + 2;
|
|
98
|
-
}
|
|
99
|
-
else if (lastLine > 0 && !isInsidePre(lastLine, preRanges)) {
|
|
100
|
-
splitAt = lastLine + 1;
|
|
101
|
-
}
|
|
102
|
-
else if (lastSpace > 0 && !isInsidePre(lastSpace, preRanges)) {
|
|
103
|
-
splitAt = lastSpace + 1;
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
// If all candidate split points are inside a <pre> block, split after it
|
|
107
|
-
const coveringPre = preRanges.find(([start, end]) => start < maxLen && end > maxLen);
|
|
108
|
-
if (coveringPre) {
|
|
109
|
-
splitAt = coveringPre[1];
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
splitAt = maxLen;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
chunks.push(remaining.slice(0, splitAt).trimEnd());
|
|
116
|
-
remaining = remaining.slice(splitAt).trimStart();
|
|
117
|
-
}
|
|
118
|
-
if (remaining.length > 0) {
|
|
119
|
-
chunks.push(remaining);
|
|
120
|
-
}
|
|
121
|
-
return chunks;
|
|
122
|
-
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,17 +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
|
-
export {};
|
package/dist/index.js
DELETED
|
@@ -1,169 +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 TelegramBot from "node-telegram-bot-api";
|
|
24
|
-
import { CcTgBot } from "./bot.js";
|
|
25
|
-
import { loadTokens } from "./tokens.js";
|
|
26
|
-
import { Registry, startControlServer } from "@gonzih/agent-ops";
|
|
27
|
-
import { Redis } from "ioredis";
|
|
28
|
-
import { startNotifier } from "./notifier.js";
|
|
29
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
-
const __dirname = dirname(__filename);
|
|
31
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
32
|
-
// Make lock socket unique per bot token so multiple users on the same machine don't collide
|
|
33
|
-
const _tokenHash = Buffer.from(process.env.TELEGRAM_BOT_TOKEN ?? "default").toString("base64").replace(/[^a-z0-9]/gi, "").slice(0, 16);
|
|
34
|
-
const LOCK_SOCKET = join(tmpdir(), `cc-tg-${_tokenHash}.sock`);
|
|
35
|
-
function acquireLock() {
|
|
36
|
-
return new Promise((resolve) => {
|
|
37
|
-
const server = createServer();
|
|
38
|
-
server.listen(LOCK_SOCKET, () => {
|
|
39
|
-
// Bound successfully — we own the lock. Socket auto-released on any exit incl. SIGKILL.
|
|
40
|
-
resolve(true);
|
|
41
|
-
});
|
|
42
|
-
server.on("error", (err) => {
|
|
43
|
-
if (err.code !== "EADDRINUSE") {
|
|
44
|
-
resolve(true); // unrelated error, proceed
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
// Socket path exists — probe if anything is actually listening
|
|
48
|
-
const probe = createConnection(LOCK_SOCKET);
|
|
49
|
-
probe.on("connect", () => {
|
|
50
|
-
probe.destroy();
|
|
51
|
-
console.error("[cc-tg] Another instance is already running. Exiting.");
|
|
52
|
-
resolve(false);
|
|
53
|
-
});
|
|
54
|
-
probe.on("error", () => {
|
|
55
|
-
// Nothing listening — stale socket, remove and retry
|
|
56
|
-
try {
|
|
57
|
-
unlinkSync(LOCK_SOCKET);
|
|
58
|
-
}
|
|
59
|
-
catch { }
|
|
60
|
-
const retry = createServer();
|
|
61
|
-
retry.listen(LOCK_SOCKET, () => resolve(true));
|
|
62
|
-
retry.on("error", () => resolve(true)); // give up on lock, just start
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
const lockAcquired = await acquireLock();
|
|
68
|
-
if (!lockAcquired) {
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
function required(name) {
|
|
72
|
-
const val = process.env[name];
|
|
73
|
-
if (!val) {
|
|
74
|
-
console.error(`
|
|
75
|
-
ERROR: ${name} is not set.
|
|
76
|
-
|
|
77
|
-
cc-tg requires:
|
|
78
|
-
TELEGRAM_BOT_TOKEN — get one from @BotFather on Telegram
|
|
79
|
-
CLAUDE_CODE_TOKEN — your Claude Code OAuth token
|
|
80
|
-
|
|
81
|
-
Set them and run again:
|
|
82
|
-
TELEGRAM_BOT_TOKEN=xxx CLAUDE_CODE_TOKEN=yyy npx @gonzih/cc-tg
|
|
83
|
-
|
|
84
|
-
Or add to your shell profile / .env file.
|
|
85
|
-
`);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
return val;
|
|
89
|
-
}
|
|
90
|
-
const telegramToken = required("TELEGRAM_BOT_TOKEN");
|
|
91
|
-
// Accept CLAUDE_CODE_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or ANTHROPIC_API_KEY
|
|
92
|
-
const claudeToken = process.env.CLAUDE_CODE_TOKEN ??
|
|
93
|
-
process.env.CLAUDE_CODE_OAUTH_TOKEN ??
|
|
94
|
-
process.env.ANTHROPIC_API_KEY;
|
|
95
|
-
if (!claudeToken) {
|
|
96
|
-
console.error(`
|
|
97
|
-
ERROR: No Claude token set. Set one of: CLAUDE_CODE_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or ANTHROPIC_API_KEY.
|
|
98
|
-
|
|
99
|
-
Set one and run again:
|
|
100
|
-
TELEGRAM_BOT_TOKEN=xxx CLAUDE_CODE_TOKEN=yyy npx @gonzih/cc-tg
|
|
101
|
-
`);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
// Load OAuth token pool (supports CLAUDE_CODE_OAUTH_TOKENS for multi-account rotation)
|
|
105
|
-
const tokenPool = loadTokens();
|
|
106
|
-
if (tokenPool.length > 1) {
|
|
107
|
-
console.log(`[cc-tg] Token pool loaded: ${tokenPool.length} tokens — will rotate on usage limit`);
|
|
108
|
-
}
|
|
109
|
-
const allowedUserIds = process.env.ALLOWED_USER_IDS
|
|
110
|
-
? process.env.ALLOWED_USER_IDS.split(",").map((s) => parseInt(s.trim(), 10)).filter(Boolean)
|
|
111
|
-
: [];
|
|
112
|
-
const groupChatIds = process.env.GROUP_CHAT_IDS
|
|
113
|
-
? process.env.GROUP_CHAT_IDS.split(",").map((s) => parseInt(s.trim(), 10)).filter(Boolean)
|
|
114
|
-
: [];
|
|
115
|
-
const cwd = process.env.CWD ?? process.cwd();
|
|
116
|
-
const bot = new CcTgBot({
|
|
117
|
-
telegramToken,
|
|
118
|
-
claudeToken,
|
|
119
|
-
cwd,
|
|
120
|
-
allowedUserIds,
|
|
121
|
-
groupChatIds,
|
|
122
|
-
});
|
|
123
|
-
// agent-ops: optional self-registration + HTTP control endpoint
|
|
124
|
-
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
125
|
-
const namespace = process.env.CC_AGENT_NAMESPACE || "default";
|
|
126
|
-
let sharedRedis = null;
|
|
127
|
-
if (process.env.CC_AGENT_OPS_PORT) {
|
|
128
|
-
const botInfo = await bot.getMe();
|
|
129
|
-
sharedRedis = new Redis(redisUrl);
|
|
130
|
-
const registry = new Registry(sharedRedis);
|
|
131
|
-
await registry.register({
|
|
132
|
-
namespace,
|
|
133
|
-
hostname: os.hostname(),
|
|
134
|
-
user: os.userInfo().username,
|
|
135
|
-
pid: String(process.pid),
|
|
136
|
-
version: pkg.version,
|
|
137
|
-
cwd: process.env.CWD || process.cwd(),
|
|
138
|
-
control_port: process.env.CC_AGENT_OPS_PORT,
|
|
139
|
-
bot_username: botInfo.username ?? "",
|
|
140
|
-
started_at: new Date().toISOString(),
|
|
141
|
-
});
|
|
142
|
-
setInterval(() => registry.heartbeat(namespace), 60_000);
|
|
143
|
-
startControlServer(Number(process.env.CC_AGENT_OPS_PORT), {
|
|
144
|
-
namespace,
|
|
145
|
-
version: pkg.version,
|
|
146
|
-
logFile: process.env.CC_AGENT_LOG_FILE || process.env.LOG_FILE,
|
|
147
|
-
});
|
|
148
|
-
console.log(`[ops] control server on port ${process.env.CC_AGENT_OPS_PORT}`);
|
|
149
|
-
}
|
|
150
|
-
// Notifier — subscribe to cca:notify:{namespace} and cca:chat:incoming:{namespace}
|
|
151
|
-
const notifyChatId = process.env.CC_AGENT_NOTIFY_CHAT_ID
|
|
152
|
-
? Number(process.env.CC_AGENT_NOTIFY_CHAT_ID)
|
|
153
|
-
: null;
|
|
154
|
-
if (notifyChatId) {
|
|
155
|
-
if (!sharedRedis)
|
|
156
|
-
sharedRedis = new Redis(redisUrl);
|
|
157
|
-
const notifierBot = new TelegramBot(telegramToken, { polling: false });
|
|
158
|
-
startNotifier(notifierBot, notifyChatId, namespace, sharedRedis);
|
|
159
|
-
console.log(`[notifier] started for namespace=${namespace} chatId=${notifyChatId}`);
|
|
160
|
-
}
|
|
161
|
-
process.on("SIGINT", () => {
|
|
162
|
-
console.log("\nShutting down...");
|
|
163
|
-
bot.stop();
|
|
164
|
-
process.exit(0);
|
|
165
|
-
});
|
|
166
|
-
process.on("SIGTERM", () => {
|
|
167
|
-
bot.stop();
|
|
168
|
-
process.exit(0);
|
|
169
|
-
});
|
package/dist/notifier.d.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Notifier — subscribes to Redis pub/sub channels and bridges messages to Telegram.
|
|
3
|
-
*
|
|
4
|
-
* Channels:
|
|
5
|
-
* cca:notify:{namespace} — job completion notifications from cc-agent → forward to Telegram
|
|
6
|
-
* cca:chat:incoming:{namespace} — messages from the web UI → echo to Telegram + feed into Claude session
|
|
7
|
-
*
|
|
8
|
-
* All messages (Telegram incoming, Claude responses) are also written to:
|
|
9
|
-
* cca:chat:log:{namespace} — LPUSH + LTRIM 0 499 (last 500 messages)
|
|
10
|
-
* cca:chat:outgoing:{namespace} — PUBLISH for web UI to consume
|
|
11
|
-
*/
|
|
12
|
-
import { Redis } from "ioredis";
|
|
13
|
-
import TelegramBot from "node-telegram-bot-api";
|
|
14
|
-
export interface ChatMessage {
|
|
15
|
-
id: string;
|
|
16
|
-
source: "telegram" | "ui" | "claude";
|
|
17
|
-
role: "user" | "assistant";
|
|
18
|
-
content: string;
|
|
19
|
-
timestamp: string;
|
|
20
|
-
chatId: number;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Write a message to the chat log in Redis.
|
|
24
|
-
* Fire-and-forget — errors are logged but not thrown.
|
|
25
|
-
*/
|
|
26
|
-
export declare function writeChatLog(redis: Redis, namespace: string, msg: ChatMessage): void;
|
|
27
|
-
/**
|
|
28
|
-
* Start the notifier.
|
|
29
|
-
*
|
|
30
|
-
* @param bot - Telegram bot instance (for sending messages)
|
|
31
|
-
* @param chatId - Telegram chat ID to forward notifications to
|
|
32
|
-
* @param namespace - cc-agent namespace (used to build Redis channel names)
|
|
33
|
-
* @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
|
|
34
|
-
* @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
|
|
35
|
-
*/
|
|
36
|
-
export declare function startNotifier(bot: TelegramBot, chatId: number, namespace: string, redis: Redis, handleUserMessage?: (chatId: number, text: string) => void): void;
|