@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.
- package/dist/cc-agent-events.d.ts +7 -0
- package/dist/cc-agent-events.js +69 -19
- package/package.json +1 -1
|
@@ -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.
|
package/dist/cc-agent-events.js
CHANGED
|
@@ -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
|
|
61
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
300
|
-
log("warn", "NOTIFY_ONLY:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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;
|