@gonzih/cc-tg 0.6.5 → 0.7.0

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 CHANGED
@@ -15,7 +15,6 @@ export declare class CcTgBot {
15
15
  private sessions;
16
16
  private pendingRetries;
17
17
  private opts;
18
- private cron;
19
18
  private costStore;
20
19
  private botUsername;
21
20
  private botId;
@@ -45,9 +44,6 @@ export declare class CcTgBot {
45
44
  private isSensitiveFile;
46
45
  private uploadMentionedFiles;
47
46
  private extractToolName;
48
- private runCronTask;
49
- private handleCron;
50
- private handleCronEdit;
51
47
  /** Find cc-agent PIDs via pgrep. Returns array of numeric PIDs. */
52
48
  private findCcAgentPids;
53
49
  /** Kill cc-agent PIDs with SIGTERM. Returns the list of killed PIDs. */
package/dist/bot.js CHANGED
@@ -11,7 +11,6 @@ import https from "https";
11
11
  import http from "http";
12
12
  import { ClaudeProcess, extractText } from "./claude.js";
13
13
  import { transcribeVoice, isVoiceAvailable } from "./voice.js";
14
- import { CronManager } from "./cron.js";
15
14
  import { formatForTelegram, splitLongMessage } from "./formatter.js";
16
15
  import { detectUsageLimit } from "./usage-limit.js";
17
16
  import { getCurrentToken, rotateToken, getTokenIndex, getTokenCount } from "./tokens.js";
@@ -21,7 +20,6 @@ const BOT_COMMANDS = [
21
20
  { command: "stop", description: "Stop the current Claude task" },
22
21
  { command: "status", description: "Check if a session is active" },
23
22
  { command: "help", description: "Show all available commands" },
24
- { command: "cron", description: "Manage cron jobs — add/list/edit/remove/clear" },
25
23
  { command: "reload_mcp", description: "Restart the cc-agent MCP server process" },
26
24
  { command: "mcp_status", description: "Check MCP server connection status" },
27
25
  { command: "mcp_version", description: "Show cc-agent npm version and npx cache info" },
@@ -66,10 +64,6 @@ function formatCostReport(cost) {
66
64
  ` Cache write: ${formatTokens(cost.totalCacheWriteTokens)} tokens ($${cacheWriteCost.toFixed(3)})`,
67
65
  ].join("\n");
68
66
  }
69
- function formatCronCostFooter(usage) {
70
- const cost = computeCostUsd(usage);
71
- return `\nšŸ’° Cron cost: $${cost.toFixed(4)} (${formatTokens(usage.inputTokens)} in / ${formatTokens(usage.outputTokens)} out tokens)`;
72
- }
73
67
  function formatAgentCostSummary(text) {
74
68
  try {
75
69
  const data = JSON.parse(text);
@@ -156,7 +150,6 @@ export class CcTgBot {
156
150
  sessions = new Map();
157
151
  pendingRetries = new Map();
158
152
  opts;
159
- cron;
160
153
  costStore;
161
154
  botUsername = "";
162
155
  botId = 0;
@@ -170,12 +163,6 @@ export class CcTgBot {
170
163
  this.botId = me.id;
171
164
  console.log(`[tg] bot identity: @${this.botUsername} (id=${this.botId})`);
172
165
  }).catch((err) => console.error("[tg] getMe failed:", err.message));
173
- // Cron manager — fires each task into an isolated ClaudeProcess.
174
- // The `done` callback is passed through to runCronTask so the cron manager
175
- // knows when a task finishes and can allow the next tick to run.
176
- this.cron = new CronManager(opts.cwd ?? process.cwd(), (chatId, prompt, jobId, done) => {
177
- this.runCronTask(chatId, prompt, done);
178
- });
179
166
  this.costStore = new CostStore(opts.cwd ?? process.cwd());
180
167
  this.registerBotCommands();
181
168
  console.log("cc-tg bot started");
@@ -305,11 +292,6 @@ export class CcTgBot {
305
292
  await this.replyToChat(chatId, status, threadId);
306
293
  return;
307
294
  }
308
- // /cron <schedule> <prompt> | /cron list | /cron clear | /cron remove <id>
309
- if (text.startsWith("/cron")) {
310
- await this.handleCron(chatId, text, threadId);
311
- return;
312
- }
313
295
  // /reload_mcp — kill cc-agent process so Claude Code auto-restarts it
314
296
  if (text === "/reload_mcp") {
315
297
  await this.handleReloadMcp(chatId, threadId);
@@ -814,210 +796,6 @@ export class CcTgBot {
814
796
  const toolUse = content.find((b) => b.type === "tool_use");
815
797
  return toolUse?.name ?? "";
816
798
  }
817
- runCronTask(chatId, prompt, done = () => { }) {
818
- // Fresh isolated Claude session — never touches main conversation
819
- const cronProcess = new ClaudeProcess({
820
- cwd: this.opts.cwd,
821
- token: this.opts.claudeToken,
822
- });
823
- const taskPrompt = [
824
- "You are handling a scheduled background task.",
825
- "This is NOT part of the user's ongoing conversation.",
826
- "Be concise. Report results only. No greetings or pleasantries.",
827
- "If there is nothing to report, say so in one sentence.",
828
- "DEDUP RULE: If this task involves resuming or restarting interrupted agents/jobs,",
829
- " skip any job whose task description already starts with 'RESUMING' (it is already",
830
- " a resume attempt). Also skip any job that has a non-empty 'resumed_by' field.",
831
- " Only spawn a resume agent for a job if resume_count < 2 (when that field exists).",
832
- " This prevents exponential job growth when a cron re-discovers its own spawned agents.",
833
- "",
834
- `SCHEDULED TASK: ${prompt}`,
835
- ].join("\n");
836
- let output = "";
837
- const cronUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
838
- cronProcess.on("usage", (usage) => {
839
- cronUsage.inputTokens += usage.inputTokens;
840
- cronUsage.outputTokens += usage.outputTokens;
841
- cronUsage.cacheReadTokens += usage.cacheReadTokens;
842
- cronUsage.cacheWriteTokens += usage.cacheWriteTokens;
843
- });
844
- cronProcess.on("message", (msg) => {
845
- if (msg.type === "result") {
846
- const text = extractText(msg);
847
- if (text)
848
- output += text;
849
- const result = output.trim();
850
- if (result) {
851
- let footer = "";
852
- try {
853
- footer = formatCronCostFooter(cronUsage);
854
- }
855
- catch (err) {
856
- console.error(`[cron] cost footer error:`, err.message);
857
- }
858
- const cronFormatted = formatForTelegram(`šŸ• ${result}${footer}`);
859
- const chunks = splitLongMessage(cronFormatted);
860
- (async () => {
861
- for (const chunk of chunks) {
862
- try {
863
- await this.bot.sendMessage(chatId, chunk, { parse_mode: "HTML" });
864
- }
865
- catch {
866
- // HTML parse failed — retry as plain text
867
- try {
868
- await this.bot.sendMessage(chatId, chunk);
869
- }
870
- catch (err) {
871
- console.error(`[cron] failed to send result to chat=${chatId}:`, err.message);
872
- }
873
- }
874
- }
875
- })();
876
- }
877
- cronProcess.kill();
878
- }
879
- });
880
- cronProcess.on("error", (err) => {
881
- console.error(`[cron] task error for chat=${chatId}:`, err.message);
882
- cronProcess.kill();
883
- done();
884
- });
885
- cronProcess.on("exit", () => {
886
- console.log(`[cron] task complete for chat=${chatId}`);
887
- done();
888
- });
889
- cronProcess.sendPrompt(taskPrompt);
890
- }
891
- async handleCron(chatId, text, threadId) {
892
- const args = text.slice("/cron".length).trim();
893
- // /cron list
894
- if (args === "list" || args === "") {
895
- const jobs = this.cron.list(chatId);
896
- if (!jobs.length) {
897
- await this.replyToChat(chatId, "No cron jobs.", threadId);
898
- return;
899
- }
900
- const lines = jobs.map((j, i) => {
901
- const short = j.prompt.length > 50 ? j.prompt.slice(0, 50) + "…" : j.prompt;
902
- return `#${i + 1} ${j.schedule} — "${short}"`;
903
- });
904
- await this.replyToChat(chatId, `Cron jobs (${jobs.length}):\n${lines.join("\n")}`, threadId);
905
- return;
906
- }
907
- // /cron clear
908
- if (args === "clear") {
909
- const n = this.cron.clearAll(chatId);
910
- await this.replyToChat(chatId, `Cleared ${n} cron job(s).`, threadId);
911
- return;
912
- }
913
- // /cron remove <id>
914
- if (args.startsWith("remove ")) {
915
- const id = args.slice("remove ".length).trim();
916
- const ok = this.cron.remove(chatId, id);
917
- await this.replyToChat(chatId, ok ? `Removed ${id}.` : `Not found: ${id}`, threadId);
918
- return;
919
- }
920
- // /cron edit [<#> ...]
921
- if (args === "edit" || args.startsWith("edit ")) {
922
- await this.handleCronEdit(chatId, args.slice("edit".length).trim(), threadId);
923
- return;
924
- }
925
- // /cron every 1h <prompt>
926
- const scheduleMatch = args.match(/^(every\s+\d+[mhd])\s+(.+)$/i);
927
- if (!scheduleMatch) {
928
- await this.replyToChat(chatId, "Usage:\n/cron every 1h <prompt>\n/cron list\n/cron edit\n/cron remove <id>\n/cron clear", threadId);
929
- return;
930
- }
931
- const schedule = scheduleMatch[1];
932
- const prompt = scheduleMatch[2];
933
- const job = this.cron.add(chatId, schedule, prompt);
934
- if (!job) {
935
- await this.replyToChat(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d", threadId);
936
- return;
937
- }
938
- await this.replyToChat(chatId, `Cron set [${job.id}]: ${schedule} — "${prompt}"`, threadId);
939
- }
940
- async handleCronEdit(chatId, editArgs, threadId) {
941
- const jobs = this.cron.list(chatId);
942
- // No args — show numbered list with edit instructions
943
- if (!editArgs) {
944
- if (!jobs.length) {
945
- await this.replyToChat(chatId, "No cron jobs to edit.", threadId);
946
- return;
947
- }
948
- const lines = jobs.map((j, i) => {
949
- const short = j.prompt.length > 50 ? j.prompt.slice(0, 50) + "…" : j.prompt;
950
- return `#${i + 1} ${j.schedule} — "${short}"`;
951
- });
952
- await this.replyToChat(chatId, `Cron jobs:\n${lines.join("\n")}\n\n` +
953
- "Edit options:\n" +
954
- "/cron edit <#> every <N><unit> <new prompt>\n" +
955
- "/cron edit <#> schedule every <N><unit>\n" +
956
- "/cron edit <#> prompt <new prompt>", threadId);
957
- return;
958
- }
959
- // Expect: <index> <rest>
960
- const indexMatch = editArgs.match(/^(\d+)\s+(.+)$/);
961
- if (!indexMatch) {
962
- await this.replyToChat(chatId, "Usage: /cron edit <#> every <N><unit> <new prompt>", threadId);
963
- return;
964
- }
965
- const index = parseInt(indexMatch[1], 10) - 1;
966
- if (index < 0 || index >= jobs.length) {
967
- await this.replyToChat(chatId, `Invalid job number. Use /cron edit to see the list.`, threadId);
968
- return;
969
- }
970
- const job = jobs[index];
971
- const editCmd = indexMatch[2];
972
- // /cron edit <#> schedule every <N><unit>
973
- if (editCmd.startsWith("schedule ")) {
974
- const newSchedule = editCmd.slice("schedule ".length).trim();
975
- const result = this.cron.update(chatId, job.id, { schedule: newSchedule });
976
- if (result === null) {
977
- await this.replyToChat(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d", threadId);
978
- }
979
- else if (result === false) {
980
- await this.replyToChat(chatId, "Job not found.", threadId);
981
- }
982
- else {
983
- await this.replyToChat(chatId, `#${index + 1} schedule updated to ${newSchedule}.`, threadId);
984
- }
985
- return;
986
- }
987
- // /cron edit <#> prompt <new-prompt>
988
- if (editCmd.startsWith("prompt ")) {
989
- const newPrompt = editCmd.slice("prompt ".length).trim();
990
- const result = this.cron.update(chatId, job.id, { prompt: newPrompt });
991
- if (result === false) {
992
- await this.replyToChat(chatId, "Job not found.", threadId);
993
- }
994
- else {
995
- await this.replyToChat(chatId, `#${index + 1} prompt updated to "${newPrompt}".`, threadId);
996
- }
997
- return;
998
- }
999
- // /cron edit <#> every <N><unit> <new-prompt>
1000
- const fullMatch = editCmd.match(/^(every\s+\d+[mhd])\s+(.+)$/i);
1001
- if (fullMatch) {
1002
- const newSchedule = fullMatch[1];
1003
- const newPrompt = fullMatch[2];
1004
- const result = this.cron.update(chatId, job.id, { schedule: newSchedule, prompt: newPrompt });
1005
- if (result === null) {
1006
- await this.replyToChat(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d", threadId);
1007
- }
1008
- else if (result === false) {
1009
- await this.replyToChat(chatId, "Job not found.", threadId);
1010
- }
1011
- else {
1012
- await this.replyToChat(chatId, `#${index + 1} updated: ${newSchedule} — "${newPrompt}"`, threadId);
1013
- }
1014
- return;
1015
- }
1016
- await this.replyToChat(chatId, "Edit options:\n" +
1017
- "/cron edit <#> every <N><unit> <new prompt>\n" +
1018
- "/cron edit <#> schedule every <N><unit>\n" +
1019
- "/cron edit <#> prompt <new prompt>", threadId);
1020
- }
1021
799
  /** Find cc-agent PIDs via pgrep. Returns array of numeric PIDs. */
1022
800
  findCcAgentPids() {
1023
801
  try {
@@ -1239,7 +1017,7 @@ export class CcTgBot {
1239
1017
  proc.on("exit", () => { clearTimeout(timeout); done(null); });
1240
1018
  });
1241
1019
  }
1242
- killSession(chatId, keepCrons = true, threadId) {
1020
+ killSession(chatId, _keepCrons = true, threadId) {
1243
1021
  const key = this.sessionKey(chatId, threadId);
1244
1022
  const session = this.sessions.get(key);
1245
1023
  if (session) {
@@ -1247,8 +1025,6 @@ export class CcTgBot {
1247
1025
  session.claude.kill();
1248
1026
  this.sessions.delete(key);
1249
1027
  }
1250
- if (!keepCrons)
1251
- this.cron.clearAll(chatId);
1252
1028
  }
1253
1029
  getMe() {
1254
1030
  return this.bot.getMe();
package/dist/index.js CHANGED
@@ -20,11 +20,12 @@ import { tmpdir } from "os";
20
20
  import os from "os";
21
21
  import { join, dirname } from "path";
22
22
  import { fileURLToPath } from "url";
23
+ import TelegramBot from "node-telegram-bot-api";
23
24
  import { CcTgBot } from "./bot.js";
24
25
  import { loadTokens } from "./tokens.js";
25
26
  import { Registry, startControlServer } from "@gonzih/agent-ops";
26
27
  import { Redis } from "ioredis";
27
- import { connectEventSubscriber } from "./cc-agent-events.js";
28
+ import { startNotifier } from "./notifier.js";
28
29
  const __filename = fileURLToPath(import.meta.url);
29
30
  const __dirname = dirname(__filename);
30
31
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
@@ -120,11 +121,13 @@ const bot = new CcTgBot({
120
121
  groupChatIds,
121
122
  });
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;
123
127
  if (process.env.CC_AGENT_OPS_PORT) {
124
128
  const botInfo = await bot.getMe();
125
- const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
126
- const registry = new Registry(redis);
127
- const namespace = process.env.CC_AGENT_NAMESPACE || "default";
129
+ sharedRedis = new Redis(redisUrl);
130
+ const registry = new Registry(sharedRedis);
128
131
  await registry.register({
129
132
  namespace,
130
133
  hostname: os.hostname(),
@@ -144,10 +147,17 @@ if (process.env.CC_AGENT_OPS_PORT) {
144
147
  });
145
148
  console.log(`[ops] control server on port ${process.env.CC_AGENT_OPS_PORT}`);
146
149
  }
147
- // cc-agent event subscriber — watches Redis cca:events for job completions
148
- connectEventSubscriber().catch((err) => {
149
- console.error("[cc-agent-events] startup error:", err.message);
150
- });
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
+ }
151
161
  process.on("SIGINT", () => {
152
162
  console.log("\nShutting down...");
153
163
  bot.stop();
@@ -0,0 +1,36 @@
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;
@@ -0,0 +1,105 @@
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
+ function log(level, ...args) {
13
+ const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
14
+ fn("[notifier]", ...args);
15
+ }
16
+ /**
17
+ * Write a message to the chat log in Redis.
18
+ * Fire-and-forget — errors are logged but not thrown.
19
+ */
20
+ export function writeChatLog(redis, namespace, msg) {
21
+ const logKey = `cca:chat:log:${namespace}`;
22
+ const outKey = `cca:chat:outgoing:${namespace}`;
23
+ const payload = JSON.stringify(msg);
24
+ redis.lpush(logKey, payload).catch((err) => {
25
+ log("warn", "writeChatLog lpush failed:", err.message);
26
+ });
27
+ redis.ltrim(logKey, 0, 499).catch((err) => {
28
+ log("warn", "writeChatLog ltrim failed:", err.message);
29
+ });
30
+ redis.publish(outKey, payload).catch((err) => {
31
+ log("warn", "writeChatLog publish failed:", err.message);
32
+ });
33
+ }
34
+ /**
35
+ * Start the notifier.
36
+ *
37
+ * @param bot - Telegram bot instance (for sending messages)
38
+ * @param chatId - Telegram chat ID to forward notifications to
39
+ * @param namespace - cc-agent namespace (used to build Redis channel names)
40
+ * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
41
+ * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
42
+ */
43
+ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage) {
44
+ const sub = redis.duplicate();
45
+ sub.on("error", (err) => {
46
+ log("warn", "subscriber error:", err.message);
47
+ });
48
+ // cca:notify:{namespace} — forward job completion notifications to Telegram
49
+ sub.subscribe(`cca:notify:${namespace}`, (err) => {
50
+ if (err) {
51
+ log("error", `subscribe cca:notify:${namespace} failed:`, err.message);
52
+ }
53
+ else {
54
+ log("info", `subscribed to cca:notify:${namespace}`);
55
+ }
56
+ });
57
+ // cca:chat:incoming:{namespace} — messages from UI
58
+ sub.subscribe(`cca:chat:incoming:${namespace}`, (err) => {
59
+ if (err) {
60
+ log("error", `subscribe cca:chat:incoming:${namespace} failed:`, err.message);
61
+ }
62
+ else {
63
+ log("info", `subscribed to cca:chat:incoming:${namespace}`);
64
+ }
65
+ });
66
+ sub.on("message", (channel, message) => {
67
+ const notifyChannel = `cca:notify:${namespace}`;
68
+ const incomingChannel = `cca:chat:incoming:${namespace}`;
69
+ if (channel === notifyChannel) {
70
+ bot.sendMessage(chatId, message).catch((err) => {
71
+ log("warn", "sendMessage failed:", err.message);
72
+ });
73
+ return;
74
+ }
75
+ if (channel === incomingChannel) {
76
+ let content = message;
77
+ try {
78
+ const parsed = JSON.parse(message);
79
+ if (parsed.content)
80
+ content = parsed.content;
81
+ }
82
+ catch {
83
+ // raw string message — use as-is
84
+ }
85
+ // Echo to Telegram so the user sees UI messages in the chat
86
+ bot.sendMessage(chatId, `šŸ“± [from UI]: ${content}`).catch((err) => {
87
+ log("warn", "sendMessage (UI echo) failed:", err.message);
88
+ });
89
+ // Log the incoming message
90
+ const inMsg = {
91
+ id: `ui-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
92
+ source: "ui",
93
+ role: "user",
94
+ content,
95
+ timestamp: new Date().toISOString(),
96
+ chatId,
97
+ };
98
+ writeChatLog(redis, namespace, inMsg);
99
+ // Feed into active Claude session as if user typed it
100
+ if (handleUserMessage) {
101
+ handleUserMessage(chatId, content);
102
+ }
103
+ }
104
+ });
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,98 +0,0 @@
1
- /**
2
- * cc-agent Redis event subscriber.
3
- *
4
- * Listens to the `cca:events` pub/sub channel for job completion events,
5
- * asks Claude to decide what to do, and acts accordingly:
6
- * NOTIFY_ONLY — send a Telegram message to the configured chat
7
- * SPAWN_FOLLOWUP — spawn a follow-up cc-agent job via MCP + notify Telegram
8
- * SILENT — log and do nothing
9
- *
10
- * Controlled via CC_AGENT_EVENTS_ENABLED env var (default: true).
11
- * Requires CC_AGENT_NOTIFY_CHAT_ID to send Telegram notifications.
12
- */
13
- import { Redis } from "ioredis";
14
- export interface JobEvent {
15
- jobId: string;
16
- status: "done" | "failed" | "interrupted" | "running" | "cancelled";
17
- title: string;
18
- repoUrl: string;
19
- lastLines: string[];
20
- score?: number;
21
- timestamp: number;
22
- }
23
- export interface CoordinatorPlan {
24
- nextStep?: {
25
- repo_url: string;
26
- task: string;
27
- };
28
- summary: string;
29
- }
30
- export interface DecisionResult {
31
- action: "NOTIFY_ONLY" | "SPAWN_FOLLOWUP" | "SILENT";
32
- message?: string;
33
- followup?: {
34
- repo_url: string;
35
- task: string;
36
- };
37
- }
38
- /** Injectable dependencies for testability */
39
- export interface HandlerDeps {
40
- askClaude: (prompt: string) => Promise<string>;
41
- sendTelegramMessage: (chatId: number, text: string) => Promise<void>;
42
- spawnFollowupAgent: (repoUrl: string, task: string) => Promise<void>;
43
- readJobOutput: (jobId: string) => Promise<string[]>;
44
- readCoordinatorPlan: (jobId: string) => Promise<CoordinatorPlan | null>;
45
- getRunningJobCount: () => Promise<number>;
46
- getActiveChatIds: () => Promise<number[]>;
47
- }
48
- export declare function buildDecisionPrompt(event: JobEvent, last40lines: string[], coordinatorPlan: CoordinatorPlan | null): string;
49
- export declare function parseDecision(raw: string): DecisionResult;
50
- /**
51
- * Ask Claude to make a decision about a completed job.
52
- * Returns the raw text response from Claude.
53
- */
54
- export declare function defaultAskClaude(prompt: string): Promise<string>;
55
- export declare function defaultSendTelegramMessage(chatId: number, text: string): Promise<void>;
56
- export declare function defaultSpawnFollowupAgent(repoUrl: string, task: string): Promise<void>;
57
- export declare function defaultReadJobOutput(jobId: string): Promise<string[]>;
58
- export declare function defaultReadCoordinatorPlan(jobId: string): Promise<CoordinatorPlan | null>;
59
- export declare function defaultGetRunningJobCount(): Promise<number>;
60
- /**
61
- * Returns chat IDs to notify about job events.
62
- * Reads unique chatIds from the cron jobs file (same users who set up cron jobs).
63
- * Falls back to CC_AGENT_NOTIFY_CHAT_ID env var for backward compatibility.
64
- */
65
- export declare function defaultGetActiveChatIds(): Promise<number[]>;
66
- /**
67
- * Write a coordinator plan for a job, so cc-tg knows what follow-up to spawn.
68
- * Call this when spawning a job that has a planned follow-up.
69
- * TTL: 7 days.
70
- */
71
- export declare function writeCoordinatorPlan(jobId: string, plan: {
72
- nextStep?: {
73
- repo_url: string;
74
- task: string;
75
- };
76
- summary: string;
77
- }): Promise<void>;
78
- /**
79
- * Handle a single job event message from Redis pub/sub.
80
- * Exported for testability — production code passes defaultDeps.
81
- */
82
- export declare function handleJobEvent(message: string, deps: HandlerDeps): Promise<void>;
83
- /** Parse flat key-value field array from a Redis Stream entry into a record. */
84
- export declare function parseStreamFields(fields: string[]): Record<string, string>;
85
- /** Convert stream entry fields to a JobEvent JSON string for handleJobEvent. */
86
- export declare function streamEntryToMessage(fields: Record<string, string>): string | null;
87
- /**
88
- * Replay events from the Redis Stream that were missed since last-seen ID.
89
- * Uses `cca:event-stream:last-id:{botName}` in Redis to track position.
90
- * Exported for testability — pass a real or mock Redis instance.
91
- */
92
- export declare function replayStreamEvents(redis: Redis, deps: HandlerDeps, botName?: string): Promise<void>;
93
- /**
94
- * Connect to Redis and subscribe to cca:events.
95
- * Reconnects automatically on disconnect.
96
- * Call once at startup.
97
- */
98
- export declare function connectEventSubscriber(): Promise<void>;