@gonzih/cc-tg 0.3.13 → 0.3.14
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/bot.d.ts +2 -0
- package/dist/bot.js +8 -5
- package/dist/formatter.d.ts +14 -12
- package/dist/formatter.js +72 -36
- package/dist/index.js +34 -2
- package/package.json +2 -1
package/dist/bot.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Telegram bot that routes messages to/from a Claude Code subprocess.
|
|
3
3
|
* One ClaudeProcess per chat_id — sessions are isolated per user.
|
|
4
4
|
*/
|
|
5
|
+
import TelegramBot from "node-telegram-bot-api";
|
|
5
6
|
export interface BotOptions {
|
|
6
7
|
telegramToken: string;
|
|
7
8
|
claudeToken?: string;
|
|
@@ -49,6 +50,7 @@ export declare class CcTgBot {
|
|
|
49
50
|
private handleGetFile;
|
|
50
51
|
private callCcAgentTool;
|
|
51
52
|
private killSession;
|
|
53
|
+
getMe(): Promise<TelegramBot.User>;
|
|
52
54
|
stop(): void;
|
|
53
55
|
}
|
|
54
56
|
export declare function splitMessage(text: string, maxLen?: number): string[];
|
package/dist/bot.js
CHANGED
|
@@ -524,12 +524,12 @@ export class CcTgBot {
|
|
|
524
524
|
return;
|
|
525
525
|
const text = session.isRetry ? `✅ Claude is back!\n\n${raw}` : raw;
|
|
526
526
|
session.isRetry = false;
|
|
527
|
-
// Format for Telegram
|
|
527
|
+
// Format for Telegram HTML and split if needed (max 4096 chars)
|
|
528
528
|
const formatted = formatForTelegram(text);
|
|
529
529
|
const chunks = splitLongMessage(formatted);
|
|
530
530
|
for (const chunk of chunks) {
|
|
531
|
-
this.bot.sendMessage(chatId, chunk, { parse_mode: "
|
|
532
|
-
//
|
|
531
|
+
this.bot.sendMessage(chatId, chunk, { parse_mode: "HTML" }).catch(() => {
|
|
532
|
+
// HTML parse failed — retry as plain text
|
|
533
533
|
this.bot.sendMessage(chatId, chunk).catch((err) => console.error(`[tg:${chatId}] send failed:`, err.message));
|
|
534
534
|
});
|
|
535
535
|
}
|
|
@@ -763,10 +763,10 @@ export class CcTgBot {
|
|
|
763
763
|
(async () => {
|
|
764
764
|
for (const chunk of chunks) {
|
|
765
765
|
try {
|
|
766
|
-
await this.bot.sendMessage(chatId, chunk, { parse_mode: "
|
|
766
|
+
await this.bot.sendMessage(chatId, chunk, { parse_mode: "HTML" });
|
|
767
767
|
}
|
|
768
768
|
catch {
|
|
769
|
-
//
|
|
769
|
+
// HTML parse failed — retry as plain text
|
|
770
770
|
try {
|
|
771
771
|
await this.bot.sendMessage(chatId, chunk);
|
|
772
772
|
}
|
|
@@ -1147,6 +1147,9 @@ export class CcTgBot {
|
|
|
1147
1147
|
if (!keepCrons)
|
|
1148
1148
|
this.cron.clearAll(chatId);
|
|
1149
1149
|
}
|
|
1150
|
+
getMe() {
|
|
1151
|
+
return this.bot.getMe();
|
|
1152
|
+
}
|
|
1150
1153
|
stop() {
|
|
1151
1154
|
this.bot.stopPolling();
|
|
1152
1155
|
for (const [chatId] of this.sessions) {
|
package/dist/formatter.d.ts
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram
|
|
3
|
-
* Converts standard markdown to Telegram's
|
|
2
|
+
* Telegram HTML post-processor.
|
|
3
|
+
* Converts standard markdown to Telegram's HTML parse mode format.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Convert standard markdown text to Telegram
|
|
6
|
+
* Convert standard markdown text to Telegram HTML format.
|
|
7
7
|
*
|
|
8
8
|
* Processing order:
|
|
9
|
-
* 1. Extract code blocks (
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
12
|
-
* 4. Convert
|
|
13
|
-
* 5. Convert
|
|
14
|
-
* 6. Convert
|
|
15
|
-
* 7.
|
|
16
|
-
* 8.
|
|
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
|
|
17
19
|
*/
|
|
18
20
|
export declare function formatForTelegram(text: string): string;
|
|
19
21
|
/**
|
|
20
22
|
* Split a long message at natural boundaries (paragraph > line > word).
|
|
21
|
-
* Never splits mid-word. Chunks are at most maxLen characters.
|
|
23
|
+
* Never splits mid-word or inside <pre> blocks. Chunks are at most maxLen characters.
|
|
22
24
|
*/
|
|
23
25
|
export declare function splitLongMessage(text: string, maxLen?: number): string[];
|
package/dist/formatter.js
CHANGED
|
@@ -1,54 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram
|
|
3
|
-
* Converts standard markdown to Telegram's
|
|
2
|
+
* Telegram HTML post-processor.
|
|
3
|
+
* Converts standard markdown to Telegram's HTML parse mode format.
|
|
4
4
|
*/
|
|
5
|
+
function htmlEscape(text) {
|
|
6
|
+
return text
|
|
7
|
+
.replace(/&/g, "&")
|
|
8
|
+
.replace(/</g, "<")
|
|
9
|
+
.replace(/>/g, ">");
|
|
10
|
+
}
|
|
5
11
|
/**
|
|
6
|
-
* Convert standard markdown text to Telegram
|
|
12
|
+
* Convert standard markdown text to Telegram HTML format.
|
|
7
13
|
*
|
|
8
14
|
* Processing order:
|
|
9
|
-
* 1. Extract code blocks (
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
12
|
-
* 4. Convert
|
|
13
|
-
* 5. Convert
|
|
14
|
-
* 6. Convert
|
|
15
|
-
* 7.
|
|
16
|
-
* 8.
|
|
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
|
|
17
25
|
*/
|
|
18
26
|
export function formatForTelegram(text) {
|
|
19
|
-
// Step 1: Extract code blocks and inline code to protect them
|
|
20
27
|
const placeholders = [];
|
|
21
|
-
//
|
|
22
|
-
let out = text.replace(/```[\s\S]
|
|
23
|
-
placeholders.push(
|
|
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>`);
|
|
24
31
|
return `\x00P${placeholders.length - 1}\x00`;
|
|
25
32
|
});
|
|
26
|
-
//
|
|
27
|
-
out = out.replace(/`[^`\n]
|
|
28
|
-
placeholders.push(
|
|
33
|
+
// Step 2: Extract inline code (`...`) → <code>
|
|
34
|
+
out = out.replace(/`([^`\n]+)`/g, (_, content) => {
|
|
35
|
+
placeholders.push(`<code>${htmlEscape(content)}</code>`);
|
|
29
36
|
return `\x00P${placeholders.length - 1}\x00`;
|
|
30
37
|
});
|
|
31
|
-
// Step
|
|
32
|
-
out = out
|
|
33
|
-
// Step
|
|
38
|
+
// Step 3: HTML-escape remaining text
|
|
39
|
+
out = htmlEscape(out);
|
|
40
|
+
// Step 4: Convert --- → blank line
|
|
34
41
|
out = out.replace(/^-{3,}$/gm, "");
|
|
35
|
-
// Step
|
|
36
|
-
out = out.replace(/^#{1,6}\s+(.+)$/gm, "
|
|
37
|
-
// Step
|
|
38
|
-
out = out.replace(/\*\*(.+?)\*\*/gs, "
|
|
39
|
-
// Step
|
|
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
|
|
40
47
|
out = out.replace(/^[ \t]*[-*]\s+(.+)$/gm, "• $1");
|
|
41
|
-
// Step
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
54
|
out = out.replace(/\x00P(\d+)\x00/g, (_, i) => placeholders[parseInt(i, 10)]);
|
|
47
55
|
return out;
|
|
48
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
|
+
}
|
|
49
77
|
/**
|
|
50
78
|
* Split a long message at natural boundaries (paragraph > line > word).
|
|
51
|
-
* Never splits mid-word. Chunks are at most maxLen characters.
|
|
79
|
+
* Never splits mid-word or inside <pre> blocks. Chunks are at most maxLen characters.
|
|
52
80
|
*/
|
|
53
81
|
export function splitLongMessage(text, maxLen = 4096) {
|
|
54
82
|
if (text.length <= maxLen)
|
|
@@ -57,6 +85,7 @@ export function splitLongMessage(text, maxLen = 4096) {
|
|
|
57
85
|
let remaining = text;
|
|
58
86
|
while (remaining.length > maxLen) {
|
|
59
87
|
const slice = remaining.slice(0, maxLen);
|
|
88
|
+
const preRanges = findPreRanges(remaining);
|
|
60
89
|
// Prefer paragraph boundary (\n\n)
|
|
61
90
|
const lastPara = slice.lastIndexOf("\n\n");
|
|
62
91
|
// Then line boundary (\n)
|
|
@@ -64,17 +93,24 @@ export function splitLongMessage(text, maxLen = 4096) {
|
|
|
64
93
|
// Then word boundary (space)
|
|
65
94
|
const lastSpace = slice.lastIndexOf(" ");
|
|
66
95
|
let splitAt;
|
|
67
|
-
if (lastPara > 0) {
|
|
96
|
+
if (lastPara > 0 && !isInsidePre(lastPara, preRanges)) {
|
|
68
97
|
splitAt = lastPara + 2;
|
|
69
98
|
}
|
|
70
|
-
else if (lastLine > 0) {
|
|
99
|
+
else if (lastLine > 0 && !isInsidePre(lastLine, preRanges)) {
|
|
71
100
|
splitAt = lastLine + 1;
|
|
72
101
|
}
|
|
73
|
-
else if (lastSpace > 0) {
|
|
102
|
+
else if (lastSpace > 0 && !isInsidePre(lastSpace, preRanges)) {
|
|
74
103
|
splitAt = lastSpace + 1;
|
|
75
104
|
}
|
|
76
105
|
else {
|
|
77
|
-
|
|
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
|
+
}
|
|
78
114
|
}
|
|
79
115
|
chunks.push(remaining.slice(0, splitAt).trimEnd());
|
|
80
116
|
remaining = remaining.slice(splitAt).trimStart();
|
package/dist/index.js
CHANGED
|
@@ -15,10 +15,17 @@
|
|
|
15
15
|
* CWD — working directory for Claude Code (default: process.cwd())
|
|
16
16
|
*/
|
|
17
17
|
import { createServer, createConnection } from "net";
|
|
18
|
-
import { unlinkSync } from "fs";
|
|
18
|
+
import { unlinkSync, readFileSync } from "fs";
|
|
19
19
|
import { tmpdir } from "os";
|
|
20
|
-
import
|
|
20
|
+
import os from "os";
|
|
21
|
+
import { join, dirname } from "path";
|
|
22
|
+
import { fileURLToPath } from "url";
|
|
21
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"));
|
|
22
29
|
// Make lock socket unique per bot token so multiple users on the same machine don't collide
|
|
23
30
|
const _tokenHash = Buffer.from(process.env.TELEGRAM_BOT_TOKEN ?? "default").toString("base64").replace(/[^a-z0-9]/gi, "").slice(0, 16);
|
|
24
31
|
const LOCK_SOCKET = join(tmpdir(), `cc-tg-${_tokenHash}.sock`);
|
|
@@ -105,6 +112,31 @@ const bot = new CcTgBot({
|
|
|
105
112
|
allowedUserIds,
|
|
106
113
|
groupChatIds,
|
|
107
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
|
+
}
|
|
108
140
|
process.on("SIGINT", () => {
|
|
109
141
|
console.log("\nShutting down...");
|
|
110
142
|
bot.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonzih/cc-tg",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dist/"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@gonzih/agent-ops": "^0.1.0",
|
|
21
22
|
"node-telegram-bot-api": "^0.66.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|