@gonzih/cc-tg 0.6.3 → 0.6.4

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.
@@ -42,6 +42,7 @@ export interface HandlerDeps {
42
42
  readJobOutput: (jobId: string) => Promise<string[]>;
43
43
  readCoordinatorPlan: (jobId: string) => Promise<CoordinatorPlan | null>;
44
44
  getRunningJobCount: () => Promise<number>;
45
+ getActiveChatIds: () => Promise<number[]>;
45
46
  }
46
47
  export declare function buildDecisionPrompt(event: JobEvent, last40lines: string[], coordinatorPlan: CoordinatorPlan | null): string;
47
48
  export declare function parseDecision(raw: string): DecisionResult;
@@ -55,6 +56,12 @@ export declare function defaultSpawnFollowupAgent(repoUrl: string, task: string)
55
56
  export declare function defaultReadJobOutput(jobId: string): Promise<string[]>;
56
57
  export declare function defaultReadCoordinatorPlan(jobId: string): Promise<CoordinatorPlan | null>;
57
58
  export declare function defaultGetRunningJobCount(): Promise<number>;
59
+ /**
60
+ * Returns chat IDs to notify about job events.
61
+ * Reads unique chatIds from the cron jobs file (same users who set up cron jobs).
62
+ * Falls back to CC_AGENT_NOTIFY_CHAT_ID env var for backward compatibility.
63
+ */
64
+ export declare function defaultGetActiveChatIds(): Promise<number[]>;
58
65
  /**
59
66
  * Write a coordinator plan for a job, so cc-tg knows what follow-up to spawn.
60
67
  * Call this when spawning a job that has a planned follow-up.
@@ -10,6 +10,8 @@
10
10
  * Controlled via CC_AGENT_EVENTS_ENABLED env var (default: true).
11
11
  * Requires CC_AGENT_NOTIFY_CHAT_ID to send Telegram notifications.
12
12
  */
13
+ import { readFileSync } from "fs";
14
+ import { join } from "path";
13
15
  import { Redis } from "ioredis";
14
16
  import TelegramBot from "node-telegram-bot-api";
15
17
  import { ClaudeProcess, extractText } from "./claude.js";
@@ -56,11 +58,23 @@ Reply in JSON:
56
58
  "followup": { "repo_url": "...", "task": "..." } | null
57
59
  }`;
58
60
  }
61
+ function extractJson(text) {
62
+ // Strip ```json ... ``` fences
63
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
64
+ if (fenced)
65
+ return fenced[1].trim();
66
+ // Find first { ... } block
67
+ const start = text.indexOf("{");
68
+ const end = text.lastIndexOf("}");
69
+ if (start !== -1 && end !== -1)
70
+ return text.slice(start, end + 1);
71
+ return "";
72
+ }
59
73
  export function parseDecision(raw) {
60
- const match = raw.match(/\{[\s\S]*\}/);
61
- if (!match)
74
+ const extracted = extractJson(raw);
75
+ if (!extracted)
62
76
  throw new Error(`No JSON found in Claude response: ${raw.slice(0, 200)}`);
63
- const parsed = JSON.parse(match[0]);
77
+ const parsed = JSON.parse(extracted);
64
78
  if (!["NOTIFY_ONLY", "SPAWN_FOLLOWUP", "SILENT"].includes(parsed.action)) {
65
79
  throw new Error(`Unknown action: ${parsed.action}`);
66
80
  }
@@ -83,15 +97,6 @@ function formatFailureMessage(event) {
83
97
  const repoShort = event.repoUrl.replace(/^https?:\/\/github\.com\//, "");
84
98
  return `✗ ${event.title} failed\n${repoShort} — exit 1\nLast line: ${lastLine}`;
85
99
  }
86
- function getChatId() {
87
- const chatIdStr = process.env.CC_AGENT_NOTIFY_CHAT_ID;
88
- if (!chatIdStr)
89
- return null;
90
- const chatId = Number(chatIdStr);
91
- if (isNaN(chatId))
92
- return null;
93
- return chatId;
94
- }
95
100
  /**
96
101
  * Ask Claude to make a decision about a completed job.
97
102
  * Returns the raw text response from Claude.
@@ -223,6 +228,36 @@ export async function defaultReadCoordinatorPlan(jobId) {
223
228
  export async function defaultGetRunningJobCount() {
224
229
  return 0;
225
230
  }
231
+ /**
232
+ * Returns chat IDs to notify about job events.
233
+ * Reads unique chatIds from the cron jobs file (same users who set up cron jobs).
234
+ * Falls back to CC_AGENT_NOTIFY_CHAT_ID env var for backward compatibility.
235
+ */
236
+ export async function defaultGetActiveChatIds() {
237
+ const ids = new Set();
238
+ // Backward compat: explicit env var
239
+ const chatIdStr = process.env.CC_AGENT_NOTIFY_CHAT_ID;
240
+ if (chatIdStr) {
241
+ const chatId = Number(chatIdStr);
242
+ if (!isNaN(chatId))
243
+ ids.add(chatId);
244
+ }
245
+ // Read chatIds from cron jobs persistence file
246
+ try {
247
+ const cwd = process.env.CWD ?? process.cwd();
248
+ const cronFile = join(cwd, ".cc-tg", "crons.json");
249
+ const raw = readFileSync(cronFile, "utf-8");
250
+ const jobs = JSON.parse(raw);
251
+ for (const job of jobs) {
252
+ if (typeof job.chatId === "number")
253
+ ids.add(job.chatId);
254
+ }
255
+ }
256
+ catch {
257
+ // file doesn't exist or parse error — ignore
258
+ }
259
+ return Array.from(ids);
260
+ }
226
261
  /**
227
262
  * Write a coordinator plan for a job, so cc-tg knows what follow-up to spawn.
228
263
  * Call this when spawning a job that has a planned follow-up.
@@ -281,11 +316,15 @@ export async function handleJobEvent(message, deps) {
281
316
  log("warn", "Failed to read coordinator plan:", err.message);
282
317
  }
283
318
  let decision;
319
+ let rawResponse = "";
284
320
  try {
285
- const rawResponse = await deps.askClaude(buildDecisionPrompt(event, last40lines, coordinatorPlan));
321
+ rawResponse = await deps.askClaude(buildDecisionPrompt(event, last40lines, coordinatorPlan));
286
322
  decision = parseDecision(rawResponse);
287
323
  }
288
324
  catch (err) {
325
+ if (rawResponse) {
326
+ log("error", "[cc-agent-events] Claude raw response:", rawResponse.slice(0, 200));
327
+ }
289
328
  log("error", "Claude decision failed, falling back to NOTIFY_ONLY:", err.message);
290
329
  const fallbackMsg = event.status === "failed"
291
330
  ? formatFailureMessage(event)
@@ -293,16 +332,24 @@ export async function handleJobEvent(message, deps) {
293
332
  decision = { action: "NOTIFY_ONLY", message: fallbackMsg };
294
333
  }
295
334
  log("info", `Decision: ${decision.action} for job ${event.jobId}`);
296
- const chatId = getChatId();
335
+ let chatIds = [];
336
+ try {
337
+ chatIds = await deps.getActiveChatIds();
338
+ }
339
+ catch (err) {
340
+ log("warn", "Failed to get active chat IDs:", err.message);
341
+ }
297
342
  try {
298
343
  if (decision.action === "NOTIFY_ONLY") {
299
- if (!chatId) {
300
- log("warn", "NOTIFY_ONLY: CC_AGENT_NOTIFY_CHAT_ID not set, skipping notification");
344
+ if (chatIds.length === 0) {
345
+ log("warn", "NOTIFY_ONLY: no active chat IDs, skipping notification");
301
346
  return;
302
347
  }
303
348
  const msg = decision.message
304
349
  ?? (event.status === "failed" ? formatFailureMessage(event) : `Job completed: ${event.title}`);
305
- await deps.sendTelegramMessage(chatId, msg);
350
+ for (const chatId of chatIds) {
351
+ await deps.sendTelegramMessage(chatId, msg);
352
+ }
306
353
  }
307
354
  else if (decision.action === "SPAWN_FOLLOWUP") {
308
355
  if (!decision.followup) {
@@ -311,14 +358,16 @@ export async function handleJobEvent(message, deps) {
311
358
  }
312
359
  await deps.spawnFollowupAgent(decision.followup.repo_url, decision.followup.task);
313
360
  // Send Telegram notification about the spawn
314
- if (chatId) {
361
+ if (chatIds.length > 0) {
315
362
  let runningCount = 0;
316
363
  try {
317
364
  runningCount = await deps.getRunningJobCount();
318
365
  }
319
366
  catch { }
320
367
  const spawnMsg = formatSpawnMessage(event, decision.followup, runningCount);
321
- await deps.sendTelegramMessage(chatId, spawnMsg);
368
+ for (const chatId of chatIds) {
369
+ await deps.sendTelegramMessage(chatId, spawnMsg);
370
+ }
322
371
  }
323
372
  }
324
373
  else {
@@ -338,6 +387,7 @@ function makeDefaultDeps() {
338
387
  readJobOutput: defaultReadJobOutput,
339
388
  readCoordinatorPlan: defaultReadCoordinatorPlan,
340
389
  getRunningJobCount: defaultGetRunningJobCount,
390
+ getActiveChatIds: defaultGetActiveChatIds,
341
391
  };
342
392
  }
343
393
  let subscriberClient = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {