@gonzih/cc-tg 0.2.1 → 0.2.2
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/README.md +98 -15
- package/dist/bot.d.ts +2 -0
- package/dist/bot.js +70 -3
- package/dist/cron.d.ts +29 -0
- package/dist/cron.js +103 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cc-tg
|
|
2
2
|
|
|
3
|
-
Claude Code Telegram bot. Chat with Claude Code from Telegram.
|
|
3
|
+
Claude Code Telegram bot. Chat with Claude Code from Telegram — voice messages, scheduled prompts, and automatic file delivery.
|
|
4
4
|
|
|
5
5
|
## Quickstart
|
|
6
6
|
|
|
@@ -19,12 +19,12 @@ That's it. Open your bot in Telegram and start chatting.
|
|
|
19
19
|
| Variable | Required | Description |
|
|
20
20
|
|---|---|---|
|
|
21
21
|
| `TELEGRAM_BOT_TOKEN` | yes | From @BotFather |
|
|
22
|
-
| `CLAUDE_CODE_TOKEN` | yes* | Claude Code OAuth token |
|
|
23
|
-
| `ANTHROPIC_API_KEY` | yes* | Alternative
|
|
22
|
+
| `CLAUDE_CODE_TOKEN` | yes* | Claude Code OAuth token (starts with `sk-ant-oat`) |
|
|
23
|
+
| `ANTHROPIC_API_KEY` | yes* | Alternative — API key from console.anthropic.com |
|
|
24
24
|
| `ALLOWED_USER_IDS` | no | Comma-separated Telegram user IDs. Leave empty to allow anyone |
|
|
25
25
|
| `CWD` | no | Working directory for Claude Code. Defaults to current directory |
|
|
26
26
|
|
|
27
|
-
*One of CLAUDE_CODE_TOKEN or ANTHROPIC_API_KEY is required.
|
|
27
|
+
*One of `CLAUDE_CODE_TOKEN` or `ANTHROPIC_API_KEY` is required.
|
|
28
28
|
|
|
29
29
|
## How to get your Claude Code token
|
|
30
30
|
|
|
@@ -36,27 +36,107 @@ npx @anthropic-ai/claude-code setup-token
|
|
|
36
36
|
|
|
37
37
|
It opens a browser, logs you in with your Anthropic account, and prints a token starting with `sk-ant-oat`. Paste that as `CLAUDE_CODE_TOKEN`.
|
|
38
38
|
|
|
39
|
-
Alternatively, use an `ANTHROPIC_API_KEY` from [console.anthropic.com](https://console.anthropic.com) (API key starts with `sk-ant-api`).
|
|
40
|
-
|
|
41
39
|
## How to get your Telegram user ID
|
|
42
40
|
|
|
43
|
-
Message [@userinfobot](https://t.me/userinfobot) on Telegram — it replies with your ID.
|
|
41
|
+
Message [@userinfobot](https://t.me/userinfobot) on Telegram — it replies with your numeric ID.
|
|
44
42
|
|
|
45
43
|
## Bot commands
|
|
46
44
|
|
|
47
45
|
| Command | Action |
|
|
48
46
|
|---|---|
|
|
49
|
-
| `/start` |
|
|
50
|
-
| `/
|
|
51
|
-
| `/
|
|
52
|
-
| `/
|
|
47
|
+
| `/start` or `/reset` | Kill current Claude session and start fresh |
|
|
48
|
+
| `/stop` | Interrupt the running Claude task |
|
|
49
|
+
| `/status` | Check if a session is active |
|
|
50
|
+
| `/cron every 1h <prompt>` | Schedule a recurring prompt |
|
|
51
|
+
| `/cron list` | Show active cron jobs |
|
|
52
|
+
| `/cron remove <id>` | Remove a specific cron job |
|
|
53
|
+
| `/cron clear` | Remove all cron jobs |
|
|
53
54
|
| Any text | Sent directly to Claude Code |
|
|
55
|
+
| Voice message | Transcribed via whisper.cpp and sent to Claude |
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
### Persistent sessions
|
|
60
|
+
Each Telegram chat ID gets its own isolated Claude Code subprocess. Sessions survive between messages — Claude remembers context within a conversation. `/reset` starts a fresh session.
|
|
61
|
+
|
|
62
|
+
### Voice messages
|
|
63
|
+
Send a voice message → automatically transcribed via whisper.cpp → fed into Claude as text. Requires `whisper-cpp` and `ffmpeg` installed on the host.
|
|
64
|
+
|
|
65
|
+
### File delivery
|
|
66
|
+
When Claude writes a file and mentions it in the response, the bot automatically uploads it to Telegram. Hybrid detection: tracks `Write`/`Edit` tool calls during the session, cross-references with filenames mentioned in the final response.
|
|
67
|
+
|
|
68
|
+
### Cron jobs
|
|
69
|
+
Schedule recurring prompts that fire into your Claude session on a timer:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
/cron every 1h check whale-watcher logs and summarize any new large trades
|
|
73
|
+
/cron every 6h run the market scan and save results to daily-report.md
|
|
74
|
+
/cron every 30m ping the API and alert me if anything looks off
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Cron jobs persist to `<CWD>/.cc-tg/crons.json` and are restored on bot restart. Output is prefixed with `CRON: <prompt>` so you know what triggered it. Files written by cron jobs are uploaded the same way as regular responses.
|
|
78
|
+
|
|
79
|
+
### Typing indicator
|
|
80
|
+
While Claude is working, the bot sends a continuous typing indicator so you know it's active.
|
|
81
|
+
|
|
82
|
+
### Permissions
|
|
83
|
+
Runs Claude Code with `--dangerously-skip-permissions` — no confirmation prompts blocking headless execution.
|
|
54
84
|
|
|
55
85
|
## How it works
|
|
56
86
|
|
|
57
|
-
Spawns a `claude` CLI subprocess per chat session using the
|
|
87
|
+
Spawns a `claude` CLI subprocess per chat session using the stream-JSON protocol (same mechanism as the [ce_ce](https://github.com/ityonemo/ce_ce) Elixir library). Prompts pipe in via stdin, streaming JSON responses parse out. Only the final `result` message is forwarded to Telegram — no duplicate streaming chunks.
|
|
88
|
+
|
|
89
|
+
## Run persistently
|
|
90
|
+
|
|
91
|
+
### macOS launchd
|
|
92
|
+
|
|
93
|
+
```xml
|
|
94
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
95
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
96
|
+
<plist version="1.0">
|
|
97
|
+
<dict>
|
|
98
|
+
<key>Label</key>
|
|
99
|
+
<string>com.yourname.cc-tg</string>
|
|
100
|
+
<key>ProgramArguments</key>
|
|
101
|
+
<array>
|
|
102
|
+
<string>/opt/homebrew/bin/npx</string>
|
|
103
|
+
<string>-y</string>
|
|
104
|
+
<string>@gonzih/cc-tg</string>
|
|
105
|
+
</array>
|
|
106
|
+
<key>EnvironmentVariables</key>
|
|
107
|
+
<dict>
|
|
108
|
+
<key>TELEGRAM_BOT_TOKEN</key>
|
|
109
|
+
<string>your_token</string>
|
|
110
|
+
<key>CLAUDE_CODE_TOKEN</key>
|
|
111
|
+
<string>your_claude_token</string>
|
|
112
|
+
<key>ALLOWED_USER_IDS</key>
|
|
113
|
+
<string>your_telegram_id</string>
|
|
114
|
+
<key>CWD</key>
|
|
115
|
+
<string>/Users/you/your-project</string>
|
|
116
|
+
<key>PATH</key>
|
|
117
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
118
|
+
</dict>
|
|
119
|
+
<key>WorkingDirectory</key>
|
|
120
|
+
<string>/Users/you/your-project</string>
|
|
121
|
+
<key>RunAtLoad</key>
|
|
122
|
+
<true/>
|
|
123
|
+
<key>KeepAlive</key>
|
|
124
|
+
<true/>
|
|
125
|
+
<key>StandardOutPath</key>
|
|
126
|
+
<string>/tmp/cc-tg.log</string>
|
|
127
|
+
<key>StandardErrorPath</key>
|
|
128
|
+
<string>/tmp/cc-tg.log</string>
|
|
129
|
+
</dict>
|
|
130
|
+
</plist>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Save to `~/Library/LaunchAgents/com.yourname.cc-tg.plist`, then:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
launchctl load ~/Library/LaunchAgents/com.yourname.cc-tg.plist
|
|
137
|
+
```
|
|
58
138
|
|
|
59
|
-
|
|
139
|
+
### Linux systemd
|
|
60
140
|
|
|
61
141
|
```ini
|
|
62
142
|
[Unit]
|
|
@@ -66,7 +146,9 @@ Description=cc-tg Claude Code Telegram bot
|
|
|
66
146
|
Environment=TELEGRAM_BOT_TOKEN=xxx
|
|
67
147
|
Environment=CLAUDE_CODE_TOKEN=yyy
|
|
68
148
|
Environment=ALLOWED_USER_IDS=123456789
|
|
69
|
-
|
|
149
|
+
Environment=CWD=/home/you/your-project
|
|
150
|
+
WorkingDirectory=/home/you/your-project
|
|
151
|
+
ExecStart=npx -y @gonzih/cc-tg
|
|
70
152
|
Restart=always
|
|
71
153
|
|
|
72
154
|
[Install]
|
|
@@ -76,4 +158,5 @@ WantedBy=multi-user.target
|
|
|
76
158
|
## Requirements
|
|
77
159
|
|
|
78
160
|
- Node.js 18+
|
|
79
|
-
- `claude` CLI
|
|
161
|
+
- `claude` CLI: `npm install -g @anthropic-ai/claude-code`
|
|
162
|
+
- Voice transcription (optional): `whisper-cpp` + `ffmpeg`
|
package/dist/bot.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare class CcTgBot {
|
|
|
12
12
|
private bot;
|
|
13
13
|
private sessions;
|
|
14
14
|
private opts;
|
|
15
|
+
private cron;
|
|
15
16
|
constructor(opts: BotOptions);
|
|
16
17
|
private isAllowed;
|
|
17
18
|
private handleTelegram;
|
|
@@ -24,6 +25,7 @@ export declare class CcTgBot {
|
|
|
24
25
|
private trackWrittenFiles;
|
|
25
26
|
private uploadMentionedFiles;
|
|
26
27
|
private extractToolName;
|
|
28
|
+
private handleCron;
|
|
27
29
|
private killSession;
|
|
28
30
|
stop(): void;
|
|
29
31
|
}
|
package/dist/bot.js
CHANGED
|
@@ -7,17 +7,32 @@ import { existsSync } from "fs";
|
|
|
7
7
|
import { resolve, basename } from "path";
|
|
8
8
|
import { ClaudeProcess, extractText } from "./claude.js";
|
|
9
9
|
import { transcribeVoice, isVoiceAvailable } from "./voice.js";
|
|
10
|
+
import { CronManager } from "./cron.js";
|
|
10
11
|
const FLUSH_DELAY_MS = 800; // debounce streaming chunks into one Telegram message
|
|
11
12
|
const TYPING_INTERVAL_MS = 4000; // re-send typing action before Telegram's 5s expiry
|
|
12
13
|
export class CcTgBot {
|
|
13
14
|
bot;
|
|
14
15
|
sessions = new Map();
|
|
15
16
|
opts;
|
|
17
|
+
cron;
|
|
16
18
|
constructor(opts) {
|
|
17
19
|
this.opts = opts;
|
|
18
20
|
this.bot = new TelegramBot(opts.telegramToken, { polling: true });
|
|
19
21
|
this.bot.on("message", (msg) => this.handleTelegram(msg));
|
|
20
22
|
this.bot.on("polling_error", (err) => console.error("[tg]", err.message));
|
|
23
|
+
// Cron manager — fires prompts into user sessions on schedule
|
|
24
|
+
this.cron = new CronManager(opts.cwd ?? process.cwd(), (chatId, prompt) => {
|
|
25
|
+
const session = this.getOrCreateSession(chatId);
|
|
26
|
+
try {
|
|
27
|
+
session.claude.sendPrompt(`[CRON: ${prompt}]\n\n${prompt}`);
|
|
28
|
+
this.startTyping(chatId, session);
|
|
29
|
+
// Tag result with cron prefix
|
|
30
|
+
session.pendingPrefix = `CRON: ${prompt}\n\n`;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error(`[cron] failed to fire for chat=${chatId}:`, err.message);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
21
36
|
console.log("cc-tg bot started");
|
|
22
37
|
console.log(`[voice] whisper available: ${isVoiceAvailable()}`);
|
|
23
38
|
}
|
|
@@ -60,6 +75,11 @@ export class CcTgBot {
|
|
|
60
75
|
await this.bot.sendMessage(chatId, has ? "Session active." : "No active session.");
|
|
61
76
|
return;
|
|
62
77
|
}
|
|
78
|
+
// /cron <schedule> <prompt> | /cron list | /cron clear | /cron remove <id>
|
|
79
|
+
if (text.startsWith("/cron")) {
|
|
80
|
+
await this.handleCron(chatId, text);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
63
83
|
const session = this.getOrCreateSession(chatId);
|
|
64
84
|
try {
|
|
65
85
|
session.claude.sendPrompt(text);
|
|
@@ -111,6 +131,7 @@ export class CcTgBot {
|
|
|
111
131
|
const session = {
|
|
112
132
|
claude,
|
|
113
133
|
pendingText: "",
|
|
134
|
+
pendingPrefix: "",
|
|
114
135
|
flushTimer: null,
|
|
115
136
|
typingTimer: null,
|
|
116
137
|
writtenFiles: new Set(),
|
|
@@ -178,11 +199,14 @@ export class CcTgBot {
|
|
|
178
199
|
}
|
|
179
200
|
}
|
|
180
201
|
flushPending(chatId, session) {
|
|
181
|
-
const
|
|
202
|
+
const raw = session.pendingText.trim();
|
|
203
|
+
const prefix = session.pendingPrefix;
|
|
182
204
|
session.pendingText = "";
|
|
205
|
+
session.pendingPrefix = "";
|
|
183
206
|
session.flushTimer = null;
|
|
184
|
-
if (!
|
|
207
|
+
if (!raw)
|
|
185
208
|
return;
|
|
209
|
+
const text = prefix ? `${prefix}${raw}` : raw;
|
|
186
210
|
// Telegram max message length is 4096 chars — split if needed
|
|
187
211
|
const chunks = splitMessage(text);
|
|
188
212
|
for (const chunk of chunks) {
|
|
@@ -274,13 +298,56 @@ export class CcTgBot {
|
|
|
274
298
|
const toolUse = content.find((b) => b.type === "tool_use");
|
|
275
299
|
return toolUse?.name ?? "";
|
|
276
300
|
}
|
|
277
|
-
|
|
301
|
+
async handleCron(chatId, text) {
|
|
302
|
+
const args = text.slice("/cron".length).trim();
|
|
303
|
+
// /cron list
|
|
304
|
+
if (args === "list" || args === "") {
|
|
305
|
+
const jobs = this.cron.list(chatId);
|
|
306
|
+
if (!jobs.length) {
|
|
307
|
+
await this.bot.sendMessage(chatId, "No cron jobs.");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const lines = jobs.map((j) => `[${j.id}] ${j.schedule}: ${j.prompt}`);
|
|
311
|
+
await this.bot.sendMessage(chatId, lines.join("\n"));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// /cron clear
|
|
315
|
+
if (args === "clear") {
|
|
316
|
+
const n = this.cron.clearAll(chatId);
|
|
317
|
+
await this.bot.sendMessage(chatId, `Cleared ${n} cron job(s).`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// /cron remove <id>
|
|
321
|
+
if (args.startsWith("remove ")) {
|
|
322
|
+
const id = args.slice("remove ".length).trim();
|
|
323
|
+
const ok = this.cron.remove(chatId, id);
|
|
324
|
+
await this.bot.sendMessage(chatId, ok ? `Removed ${id}.` : `Not found: ${id}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// /cron every 1h <prompt>
|
|
328
|
+
const scheduleMatch = args.match(/^(every\s+\d+[mhd])\s+(.+)$/i);
|
|
329
|
+
if (!scheduleMatch) {
|
|
330
|
+
await this.bot.sendMessage(chatId, "Usage:\n/cron every 1h <prompt>\n/cron list\n/cron remove <id>\n/cron clear");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const schedule = scheduleMatch[1];
|
|
334
|
+
const prompt = scheduleMatch[2];
|
|
335
|
+
const job = this.cron.add(chatId, schedule, prompt);
|
|
336
|
+
if (!job) {
|
|
337
|
+
await this.bot.sendMessage(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
await this.bot.sendMessage(chatId, `Cron set [${job.id}]: ${schedule} — "${prompt}"`);
|
|
341
|
+
}
|
|
342
|
+
killSession(chatId, keepCrons = true) {
|
|
278
343
|
const session = this.sessions.get(chatId);
|
|
279
344
|
if (session) {
|
|
280
345
|
this.stopTyping(session);
|
|
281
346
|
session.claude.kill();
|
|
282
347
|
this.sessions.delete(chatId);
|
|
283
348
|
}
|
|
349
|
+
if (!keepCrons)
|
|
350
|
+
this.cron.clearAll(chatId);
|
|
284
351
|
}
|
|
285
352
|
stop() {
|
|
286
353
|
this.bot.stopPolling();
|
package/dist/cron.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
private persist;
|
|
27
|
+
private load;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
package/dist/cron.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
persist() {
|
|
74
|
+
try {
|
|
75
|
+
const dir = join(this.storePath, "..");
|
|
76
|
+
if (!existsSync(dir))
|
|
77
|
+
mkdirSync(dir, { recursive: true });
|
|
78
|
+
const data = [...this.jobs.values()].map(({ timer: _t, ...j }) => j);
|
|
79
|
+
writeFileSync(this.storePath, JSON.stringify(data, null, 2));
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error("[cron] persist error:", err.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
load() {
|
|
86
|
+
if (!existsSync(this.storePath))
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
const data = JSON.parse(readFileSync(this.storePath, "utf8"));
|
|
90
|
+
for (const job of data) {
|
|
91
|
+
const timer = setInterval(() => {
|
|
92
|
+
console.log(`[cron:${job.id}] firing for chat=${job.chatId} prompt="${job.prompt}"`);
|
|
93
|
+
this.fire(job.chatId, job.prompt);
|
|
94
|
+
}, job.intervalMs);
|
|
95
|
+
this.jobs.set(job.id, { ...job, timer });
|
|
96
|
+
}
|
|
97
|
+
console.log(`[cron] loaded ${data.length} jobs from disk`);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.error("[cron] load error:", err.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|