@gonzih/cc-tg 0.3.13 → 0.3.15
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 +16 -9
- package/dist/cron.d.ts +7 -1
- package/dist/cron.js +24 -3
- 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
|
@@ -168,9 +168,11 @@ export class CcTgBot {
|
|
|
168
168
|
this.botId = me.id;
|
|
169
169
|
console.log(`[tg] bot identity: @${this.botUsername} (id=${this.botId})`);
|
|
170
170
|
}).catch((err) => console.error("[tg] getMe failed:", err.message));
|
|
171
|
-
// Cron manager — fires each task into an isolated ClaudeProcess
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
// Cron manager — fires each task into an isolated ClaudeProcess.
|
|
172
|
+
// The `done` callback is passed through to runCronTask so the cron manager
|
|
173
|
+
// knows when a task finishes and can allow the next tick to run.
|
|
174
|
+
this.cron = new CronManager(opts.cwd ?? process.cwd(), (chatId, prompt, jobId, done) => {
|
|
175
|
+
this.runCronTask(chatId, prompt, done);
|
|
174
176
|
});
|
|
175
177
|
this.costStore = new CostStore(opts.cwd ?? process.cwd());
|
|
176
178
|
this.registerBotCommands();
|
|
@@ -524,12 +526,12 @@ export class CcTgBot {
|
|
|
524
526
|
return;
|
|
525
527
|
const text = session.isRetry ? `✅ Claude is back!\n\n${raw}` : raw;
|
|
526
528
|
session.isRetry = false;
|
|
527
|
-
// Format for Telegram
|
|
529
|
+
// Format for Telegram HTML and split if needed (max 4096 chars)
|
|
528
530
|
const formatted = formatForTelegram(text);
|
|
529
531
|
const chunks = splitLongMessage(formatted);
|
|
530
532
|
for (const chunk of chunks) {
|
|
531
|
-
this.bot.sendMessage(chatId, chunk, { parse_mode: "
|
|
532
|
-
//
|
|
533
|
+
this.bot.sendMessage(chatId, chunk, { parse_mode: "HTML" }).catch(() => {
|
|
534
|
+
// HTML parse failed — retry as plain text
|
|
533
535
|
this.bot.sendMessage(chatId, chunk).catch((err) => console.error(`[tg:${chatId}] send failed:`, err.message));
|
|
534
536
|
});
|
|
535
537
|
}
|
|
@@ -722,7 +724,7 @@ export class CcTgBot {
|
|
|
722
724
|
const toolUse = content.find((b) => b.type === "tool_use");
|
|
723
725
|
return toolUse?.name ?? "";
|
|
724
726
|
}
|
|
725
|
-
runCronTask(chatId, prompt) {
|
|
727
|
+
runCronTask(chatId, prompt, done = () => { }) {
|
|
726
728
|
// Fresh isolated Claude session — never touches main conversation
|
|
727
729
|
const cronProcess = new ClaudeProcess({
|
|
728
730
|
cwd: this.opts.cwd,
|
|
@@ -763,10 +765,10 @@ export class CcTgBot {
|
|
|
763
765
|
(async () => {
|
|
764
766
|
for (const chunk of chunks) {
|
|
765
767
|
try {
|
|
766
|
-
await this.bot.sendMessage(chatId, chunk, { parse_mode: "
|
|
768
|
+
await this.bot.sendMessage(chatId, chunk, { parse_mode: "HTML" });
|
|
767
769
|
}
|
|
768
770
|
catch {
|
|
769
|
-
//
|
|
771
|
+
// HTML parse failed — retry as plain text
|
|
770
772
|
try {
|
|
771
773
|
await this.bot.sendMessage(chatId, chunk);
|
|
772
774
|
}
|
|
@@ -783,9 +785,11 @@ export class CcTgBot {
|
|
|
783
785
|
cronProcess.on("error", (err) => {
|
|
784
786
|
console.error(`[cron] task error for chat=${chatId}:`, err.message);
|
|
785
787
|
cronProcess.kill();
|
|
788
|
+
done();
|
|
786
789
|
});
|
|
787
790
|
cronProcess.on("exit", () => {
|
|
788
791
|
console.log(`[cron] task complete for chat=${chatId}`);
|
|
792
|
+
done();
|
|
789
793
|
});
|
|
790
794
|
cronProcess.sendPrompt(taskPrompt);
|
|
791
795
|
}
|
|
@@ -1147,6 +1151,9 @@ export class CcTgBot {
|
|
|
1147
1151
|
if (!keepCrons)
|
|
1148
1152
|
this.cron.clearAll(chatId);
|
|
1149
1153
|
}
|
|
1154
|
+
getMe() {
|
|
1155
|
+
return this.bot.getMe();
|
|
1156
|
+
}
|
|
1150
1157
|
stop() {
|
|
1151
1158
|
this.bot.stopPolling();
|
|
1152
1159
|
for (const [chatId] of this.sessions) {
|
package/dist/cron.d.ts
CHANGED
|
@@ -11,9 +11,15 @@ export interface CronJob {
|
|
|
11
11
|
createdAt: string;
|
|
12
12
|
schedule: string;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/** Called when a job fires. `done` must be called when the task completes so
|
|
15
|
+
* the next scheduled tick is allowed to run. Until `done` is called, concurrent
|
|
16
|
+
* ticks for the same job are silently skipped (prevents the resume-loop explosion
|
|
17
|
+
* where each tick spawns more agents than the last). */
|
|
18
|
+
type FireCallback = (chatId: number, prompt: string, jobId: string, done: () => void) => void;
|
|
15
19
|
export declare class CronManager {
|
|
16
20
|
private jobs;
|
|
21
|
+
/** Job IDs whose fire callback has been invoked but whose `done` hasn't fired yet. */
|
|
22
|
+
private activeJobs;
|
|
17
23
|
private storePath;
|
|
18
24
|
private fire;
|
|
19
25
|
constructor(cwd: string, fire: FireCallback);
|
package/dist/cron.js
CHANGED
|
@@ -7,6 +7,8 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
export class CronManager {
|
|
9
9
|
jobs = new Map();
|
|
10
|
+
/** Job IDs whose fire callback has been invoked but whose `done` hasn't fired yet. */
|
|
11
|
+
activeJobs = new Set();
|
|
10
12
|
storePath;
|
|
11
13
|
fire;
|
|
12
14
|
constructor(cwd, fire) {
|
|
@@ -36,8 +38,13 @@ export class CronManager {
|
|
|
36
38
|
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
37
39
|
const job = { id, chatId, intervalMs, prompt, schedule, createdAt: new Date().toISOString() };
|
|
38
40
|
const timer = setInterval(() => {
|
|
41
|
+
if (this.activeJobs.has(id)) {
|
|
42
|
+
console.log(`[cron:${id}] skipping tick — previous task still running`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.activeJobs.add(id);
|
|
39
46
|
console.log(`[cron:${id}] firing for chat=${chatId} prompt="${prompt}"`);
|
|
40
|
-
this.fire(chatId, prompt);
|
|
47
|
+
this.fire(chatId, prompt, id, () => { this.activeJobs.delete(id); });
|
|
41
48
|
}, intervalMs);
|
|
42
49
|
this.jobs.set(id, { ...job, timer });
|
|
43
50
|
this.persist();
|
|
@@ -48,6 +55,7 @@ export class CronManager {
|
|
|
48
55
|
if (!job || job.chatId !== chatId)
|
|
49
56
|
return false;
|
|
50
57
|
clearInterval(job.timer);
|
|
58
|
+
this.activeJobs.delete(id);
|
|
51
59
|
this.jobs.delete(id);
|
|
52
60
|
this.persist();
|
|
53
61
|
return true;
|
|
@@ -57,6 +65,7 @@ export class CronManager {
|
|
|
57
65
|
for (const [id, job] of this.jobs) {
|
|
58
66
|
if (job.chatId === chatId) {
|
|
59
67
|
clearInterval(job.timer);
|
|
68
|
+
this.activeJobs.delete(id);
|
|
60
69
|
this.jobs.delete(id);
|
|
61
70
|
count++;
|
|
62
71
|
}
|
|
@@ -86,9 +95,16 @@ export class CronManager {
|
|
|
86
95
|
}
|
|
87
96
|
// Recreate timer so it uses updated intervalMs and always reads latest job.prompt
|
|
88
97
|
clearInterval(job.timer);
|
|
98
|
+
// Also clear any active-job lock so the updated timer can fire immediately next tick
|
|
99
|
+
this.activeJobs.delete(job.id);
|
|
89
100
|
job.timer = setInterval(() => {
|
|
101
|
+
if (this.activeJobs.has(job.id)) {
|
|
102
|
+
console.log(`[cron:${job.id}] skipping tick — previous task still running`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.activeJobs.add(job.id);
|
|
90
106
|
console.log(`[cron:${job.id}] firing for chat=${job.chatId} prompt="${job.prompt}"`);
|
|
91
|
-
this.fire(job.chatId, job.prompt);
|
|
107
|
+
this.fire(job.chatId, job.prompt, job.id, () => { this.activeJobs.delete(job.id); });
|
|
92
108
|
}, job.intervalMs);
|
|
93
109
|
this.persist();
|
|
94
110
|
const { timer: _t, ...cronJob } = job;
|
|
@@ -113,8 +129,13 @@ export class CronManager {
|
|
|
113
129
|
const data = JSON.parse(readFileSync(this.storePath, "utf8"));
|
|
114
130
|
for (const job of data) {
|
|
115
131
|
const timer = setInterval(() => {
|
|
132
|
+
if (this.activeJobs.has(job.id)) {
|
|
133
|
+
console.log(`[cron:${job.id}] skipping tick — previous task still running`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.activeJobs.add(job.id);
|
|
116
137
|
console.log(`[cron:${job.id}] firing for chat=${job.chatId} prompt="${job.prompt}"`);
|
|
117
|
-
this.fire(job.chatId, job.prompt);
|
|
138
|
+
this.fire(job.chatId, job.prompt, job.id, () => { this.activeJobs.delete(job.id); });
|
|
118
139
|
}, job.intervalMs);
|
|
119
140
|
this.jobs.set(job.id, { ...job, timer });
|
|
120
141
|
}
|
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.15",
|
|
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": {
|