@gonzih/cc-tg 0.6.5 → 0.6.6

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.
@@ -1,617 +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 { readFileSync } from "fs";
14
- import { join } from "path";
15
- import { Redis } from "ioredis";
16
- import TelegramBot from "node-telegram-bot-api";
17
- const STREAM_KEY = "cca:event-stream";
18
- import { ClaudeProcess, extractText } from "./claude.js";
19
- function log(level, ...args) {
20
- const fn = level === "error"
21
- ? console.error
22
- : level === "warn"
23
- ? console.warn
24
- : console.log;
25
- fn("[cc-agent-events]", ...args);
26
- }
27
- export function buildDecisionPrompt(event, last40lines, coordinatorPlan) {
28
- const scoreStr = event.score !== undefined ? String(event.score) : "n/a";
29
- const planStr = coordinatorPlan ? JSON.stringify(coordinatorPlan, null, 2) : "none";
30
- return `A cc-agent job just completed.
31
-
32
- Job: ${event.title}
33
- Repo: ${event.repoUrl}
34
- Status: ${event.status}
35
- Score: ${scoreStr}
36
-
37
- Last output + LEARNINGS:
38
- ${last40lines.join("\n")}
39
-
40
- Coordinator plan for this job (if any):
41
- ${planStr}
42
-
43
- Decide what to do next:
44
- 1. SPAWN_FOLLOWUP — spawn a follow-up job (provide repo_url and task)
45
- 2. NOTIFY_ONLY — send Telegram message, no spawn needed
46
- 3. SILENT — routine completion, no action
47
-
48
- Rules:
49
- - If LEARNINGS has "Recommendations for next agent" with a clear actionable next step → consider SPAWN_FOLLOWUP
50
- - If coordinator plan has nextStep → SPAWN_FOLLOWUP with that task (prefer coordinator plan over LEARNINGS)
51
- - Failed jobs → NOTIFY_ONLY always
52
- - Score < 0.5 → NOTIFY_ONLY
53
- - Routine/expected completions → SILENT
54
-
55
- Reply in JSON:
56
- {
57
- "action": "SPAWN_FOLLOWUP" | "NOTIFY_ONLY" | "SILENT",
58
- "message": "brief telegram message (1-2 lines)",
59
- "followup": { "repo_url": "...", "task": "..." } | null
60
- }`;
61
- }
62
- function extractJson(text) {
63
- // Strip ```json ... ``` fences
64
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
65
- if (fenced)
66
- return fenced[1].trim();
67
- // Find first { ... } block
68
- const start = text.indexOf("{");
69
- const end = text.lastIndexOf("}");
70
- if (start !== -1 && end !== -1)
71
- return text.slice(start, end + 1);
72
- return "";
73
- }
74
- export function parseDecision(raw) {
75
- const extracted = extractJson(raw);
76
- if (!extracted)
77
- throw new Error(`No JSON found in Claude response: ${raw.slice(0, 200)}`);
78
- const parsed = JSON.parse(extracted);
79
- if (!["NOTIFY_ONLY", "SPAWN_FOLLOWUP", "SILENT"].includes(parsed.action)) {
80
- throw new Error(`Unknown action: ${parsed.action}`);
81
- }
82
- return parsed;
83
- }
84
- function formatSpawnMessage(event, followup, runningCount) {
85
- const scoreStr = event.score !== undefined ? ` (score: ${event.score})` : "";
86
- const repoShort = followup.repo_url.replace(/^https?:\/\/github\.com\//, "");
87
- const lines = [
88
- `✓ ${event.title} done${scoreStr}`,
89
- `→ spawned: ${followup.task} (${repoShort})`,
90
- ];
91
- if (runningCount > 0) {
92
- lines.push(`${runningCount} jobs running`);
93
- }
94
- return lines.join("\n");
95
- }
96
- function formatFailureMessage(event) {
97
- const lastLine = event.lastLines[event.lastLines.length - 1] ?? "";
98
- const repoShort = event.repoUrl.replace(/^https?:\/\/github\.com\//, "");
99
- return `✗ ${event.title} failed\n${repoShort} — exit 1\nLast line: ${lastLine}`;
100
- }
101
- /**
102
- * Ask Claude to make a decision about a completed job.
103
- * Returns the raw text response from Claude.
104
- */
105
- export function defaultAskClaude(prompt) {
106
- return new Promise((resolve, reject) => {
107
- const token = process.env.CLAUDE_CODE_TOKEN ??
108
- process.env.CLAUDE_CODE_OAUTH_TOKEN ??
109
- process.env.ANTHROPIC_API_KEY;
110
- if (!token) {
111
- reject(new Error("No Claude token configured"));
112
- return;
113
- }
114
- const claude = new ClaudeProcess({ token });
115
- let output = "";
116
- const timeout = setTimeout(() => {
117
- claude.kill();
118
- reject(new Error("Claude decision timed out after 60s"));
119
- }, 60_000);
120
- claude.on("message", (msg) => {
121
- if (msg.type === "result") {
122
- const text = extractText(msg);
123
- if (text)
124
- output += text;
125
- clearTimeout(timeout);
126
- claude.kill();
127
- resolve(output.trim());
128
- }
129
- else if (msg.type === "assistant") {
130
- const text = extractText(msg);
131
- if (text)
132
- output += text;
133
- }
134
- });
135
- claude.on("error", (err) => {
136
- clearTimeout(timeout);
137
- reject(err);
138
- });
139
- claude.on("exit", (code) => {
140
- clearTimeout(timeout);
141
- if (!output) {
142
- reject(new Error(`Claude exited with code ${code} and no output`));
143
- }
144
- else {
145
- resolve(output.trim());
146
- }
147
- });
148
- claude.sendPrompt(prompt);
149
- });
150
- }
151
- export async function defaultSendTelegramMessage(chatId, text) {
152
- const token = process.env.TELEGRAM_BOT_TOKEN;
153
- if (!token)
154
- throw new Error("TELEGRAM_BOT_TOKEN not set");
155
- const tg = new TelegramBot(token, { polling: false });
156
- await tg.sendMessage(chatId, text);
157
- }
158
- export async function defaultSpawnFollowupAgent(repoUrl, task) {
159
- const token = process.env.CLAUDE_CODE_TOKEN ??
160
- process.env.CLAUDE_CODE_OAUTH_TOKEN ??
161
- process.env.ANTHROPIC_API_KEY;
162
- const prompt = `Use the spawn_agent MCP tool to start a new cc-agent job with these parameters:
163
- repo_url: ${repoUrl}
164
- task: ${task}
165
-
166
- Call the spawn_agent tool now with these exact parameters. Report the job ID when done.`;
167
- return new Promise((resolve) => {
168
- const claude = new ClaudeProcess({ token: token ?? undefined });
169
- const timeout = setTimeout(() => {
170
- log("warn", "spawnFollowupAgent: timed out");
171
- claude.kill();
172
- resolve();
173
- }, 120_000);
174
- claude.on("message", (msg) => {
175
- if (msg.type === "result") {
176
- clearTimeout(timeout);
177
- claude.kill();
178
- resolve();
179
- }
180
- });
181
- claude.on("error", (err) => {
182
- log("error", "spawnFollowupAgent error:", err.message);
183
- clearTimeout(timeout);
184
- resolve();
185
- });
186
- claude.on("exit", () => {
187
- clearTimeout(timeout);
188
- resolve();
189
- });
190
- claude.sendPrompt(prompt);
191
- });
192
- }
193
- function makeRedisClient() {
194
- return new Redis(process.env.REDIS_URL || "redis://localhost:6379", {
195
- lazyConnect: true,
196
- enableOfflineQueue: false,
197
- });
198
- }
199
- export async function defaultReadJobOutput(jobId) {
200
- const redis = makeRedisClient();
201
- try {
202
- await redis.connect();
203
- const lines = await redis.lrange(`cca:job:${jobId}:output`, -40, -1);
204
- return lines;
205
- }
206
- finally {
207
- try {
208
- redis.disconnect();
209
- }
210
- catch { }
211
- }
212
- }
213
- export async function defaultReadCoordinatorPlan(jobId) {
214
- const redis = makeRedisClient();
215
- try {
216
- await redis.connect();
217
- const raw = await redis.get(`cca:coordinator:plan:${jobId}`);
218
- if (!raw)
219
- return null;
220
- return JSON.parse(raw);
221
- }
222
- finally {
223
- try {
224
- redis.disconnect();
225
- }
226
- catch { }
227
- }
228
- }
229
- export async function defaultGetRunningJobCount() {
230
- return 0;
231
- }
232
- /**
233
- * Returns chat IDs to notify about job events.
234
- * Reads unique chatIds from the cron jobs file (same users who set up cron jobs).
235
- * Falls back to CC_AGENT_NOTIFY_CHAT_ID env var for backward compatibility.
236
- */
237
- export async function defaultGetActiveChatIds() {
238
- const ids = new Set();
239
- // Backward compat: explicit env var
240
- const chatIdStr = process.env.CC_AGENT_NOTIFY_CHAT_ID;
241
- if (chatIdStr) {
242
- const chatId = Number(chatIdStr);
243
- if (!isNaN(chatId))
244
- ids.add(chatId);
245
- }
246
- // Read chatIds from cron jobs persistence file
247
- try {
248
- const cwd = process.env.CWD ?? process.cwd();
249
- const cronFile = join(cwd, ".cc-tg", "crons.json");
250
- const raw = readFileSync(cronFile, "utf-8");
251
- const jobs = JSON.parse(raw);
252
- for (const job of jobs) {
253
- if (typeof job.chatId === "number")
254
- ids.add(job.chatId);
255
- }
256
- }
257
- catch {
258
- // file doesn't exist or parse error — ignore
259
- }
260
- return Array.from(ids);
261
- }
262
- /**
263
- * Write a coordinator plan for a job, so cc-tg knows what follow-up to spawn.
264
- * Call this when spawning a job that has a planned follow-up.
265
- * TTL: 7 days.
266
- */
267
- export async function writeCoordinatorPlan(jobId, plan) {
268
- const redis = makeRedisClient();
269
- try {
270
- await redis.connect();
271
- const key = `cca:coordinator:plan:${jobId}`;
272
- const ttlSeconds = 7 * 24 * 60 * 60; // 7 days
273
- await redis.set(key, JSON.stringify(plan), "EX", ttlSeconds);
274
- }
275
- finally {
276
- try {
277
- redis.disconnect();
278
- }
279
- catch { }
280
- }
281
- }
282
- /**
283
- * Handle a single job event message from Redis pub/sub.
284
- * Exported for testability — production code passes defaultDeps.
285
- */
286
- export async function handleJobEvent(message, deps) {
287
- let event;
288
- try {
289
- event = JSON.parse(message);
290
- }
291
- catch (err) {
292
- log("error", "Failed to parse job event:", err.message);
293
- return;
294
- }
295
- // Only act on terminal states
296
- if (event.status !== "done" && event.status !== "failed") {
297
- log("info", `Ignoring ${event.status} event for job ${event.jobId}`);
298
- return;
299
- }
300
- log("info", `Processing ${event.status} event for job: ${event.title} (${event.jobId})`);
301
- // Read job output from Redis (fall back to event.lastLines on error)
302
- let last40lines = event.lastLines;
303
- try {
304
- const lines = await deps.readJobOutput(event.jobId);
305
- if (lines.length > 0)
306
- last40lines = lines;
307
- }
308
- catch (err) {
309
- log("warn", "Failed to read job output, using event.lastLines:", err.message);
310
- }
311
- // Read coordinator plan from Redis (fall back to null on error)
312
- let coordinatorPlan = null;
313
- try {
314
- coordinatorPlan = await deps.readCoordinatorPlan(event.jobId);
315
- }
316
- catch (err) {
317
- log("warn", "Failed to read coordinator plan:", err.message);
318
- }
319
- // Fast path: coordinator plan has explicit next step — spawn directly, no Claude needed
320
- // This eliminates JSON truncation issues when Claude regenerates long task strings.
321
- if (coordinatorPlan?.nextStep) {
322
- log("info", `Fast path: coordinator plan nextStep found for job ${event.jobId}`);
323
- const { repo_url, task } = coordinatorPlan.nextStep;
324
- let fpChatIds = [];
325
- try {
326
- fpChatIds = await deps.getActiveChatIds();
327
- }
328
- catch (err) {
329
- log("warn", "Fast path: failed to get active chat IDs:", err.message);
330
- }
331
- try {
332
- await deps.spawnFollowupAgent(repo_url, task);
333
- }
334
- catch (err) {
335
- log("error", "Fast path: spawnFollowupAgent failed:", err.message);
336
- }
337
- if (fpChatIds.length > 0) {
338
- const scoreStr = event.score !== undefined ? ` (score: ${event.score})` : "";
339
- const repoShort = repo_url.split("/").pop() ?? repo_url;
340
- const msg = `✓ ${event.title} done${scoreStr}\n→ spawned: ${repoShort}`;
341
- for (const chatId of fpChatIds) {
342
- try {
343
- await deps.sendTelegramMessage(chatId, msg);
344
- }
345
- catch (err) {
346
- log("error", "Fast path: sendTelegramMessage failed:", err.message);
347
- }
348
- }
349
- }
350
- return;
351
- }
352
- let decision;
353
- let rawResponse = "";
354
- try {
355
- rawResponse = await deps.askClaude(buildDecisionPrompt(event, last40lines, coordinatorPlan));
356
- decision = parseDecision(rawResponse);
357
- }
358
- catch (err) {
359
- if (rawResponse) {
360
- log("error", "[cc-agent-events] Claude raw response:", rawResponse.slice(0, 200));
361
- }
362
- log("error", "Claude decision failed, falling back to NOTIFY_ONLY:", err.message);
363
- const fallbackMsg = event.status === "failed"
364
- ? formatFailureMessage(event)
365
- : `Job completed: ${event.title}`;
366
- decision = { action: "NOTIFY_ONLY", message: fallbackMsg };
367
- }
368
- log("info", `Decision: ${decision.action} for job ${event.jobId}`);
369
- let chatIds = [];
370
- try {
371
- chatIds = await deps.getActiveChatIds();
372
- }
373
- catch (err) {
374
- log("warn", "Failed to get active chat IDs:", err.message);
375
- }
376
- try {
377
- if (decision.action === "NOTIFY_ONLY") {
378
- if (chatIds.length === 0) {
379
- log("warn", "NOTIFY_ONLY: no active chat IDs, skipping notification");
380
- return;
381
- }
382
- const msg = decision.message
383
- ?? (event.status === "failed" ? formatFailureMessage(event) : `Job completed: ${event.title}`);
384
- for (const chatId of chatIds) {
385
- await deps.sendTelegramMessage(chatId, msg);
386
- }
387
- }
388
- else if (decision.action === "SPAWN_FOLLOWUP") {
389
- if (!decision.followup) {
390
- log("warn", "SPAWN_FOLLOWUP: no followup details in response");
391
- return;
392
- }
393
- await deps.spawnFollowupAgent(decision.followup.repo_url, decision.followup.task);
394
- // Send Telegram notification about the spawn
395
- if (chatIds.length > 0) {
396
- let runningCount = 0;
397
- try {
398
- runningCount = await deps.getRunningJobCount();
399
- }
400
- catch { }
401
- const spawnMsg = formatSpawnMessage(event, decision.followup, runningCount);
402
- for (const chatId of chatIds) {
403
- await deps.sendTelegramMessage(chatId, spawnMsg);
404
- }
405
- }
406
- }
407
- else {
408
- // SILENT — log only
409
- log("info", `SILENT: no action taken for job ${event.jobId}`);
410
- }
411
- }
412
- catch (err) {
413
- log("error", `Action ${decision.action} failed:`, err.message);
414
- }
415
- }
416
- /** Parse flat key-value field array from a Redis Stream entry into a record. */
417
- export function parseStreamFields(fields) {
418
- const obj = {};
419
- for (let i = 0; i + 1 < fields.length; i += 2) {
420
- obj[fields[i]] = fields[i + 1];
421
- }
422
- return obj;
423
- }
424
- /** Convert stream entry fields to a JobEvent JSON string for handleJobEvent. */
425
- export function streamEntryToMessage(fields) {
426
- try {
427
- const score = fields["score"] !== undefined && fields["score"] !== ""
428
- ? Number(fields["score"])
429
- : undefined;
430
- const event = {
431
- jobId: fields["jobId"] ?? "",
432
- status: (fields["status"] ?? "done"),
433
- title: fields["title"] ?? "",
434
- repoUrl: fields["repoUrl"] ?? "",
435
- lastLines: JSON.parse(fields["lastLines"] ?? "[]"),
436
- score,
437
- timestamp: Number(fields["timestamp"] ?? Date.now()),
438
- };
439
- return JSON.stringify(event);
440
- }
441
- catch {
442
- return null;
443
- }
444
- }
445
- /**
446
- * Replay events from the Redis Stream that were missed since last-seen ID.
447
- * Uses `cca:event-stream:last-id:{botName}` in Redis to track position.
448
- * Exported for testability — pass a real or mock Redis instance.
449
- */
450
- export async function replayStreamEvents(redis, deps, botName) {
451
- const name = botName ?? (process.env.CC_TG_BOT_NAME ?? "cc-tg");
452
- const lastIdKey = `cca:event-stream:last-id:${name}`;
453
- let lastId = "0";
454
- try {
455
- lastId = (await redis.get(lastIdKey)) ?? "0";
456
- }
457
- catch (err) {
458
- log("warn", "replayStreamEvents: failed to read last-id:", err.message);
459
- }
460
- let results = null;
461
- try {
462
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
463
- results = (await redis.xread("COUNT", 20, "STREAMS", STREAM_KEY, lastId));
464
- }
465
- catch (err) {
466
- log("warn", "replayStreamEvents: xread failed:", err.message);
467
- return;
468
- }
469
- if (!results || results.length === 0)
470
- return;
471
- log("info", `Replaying missed stream events from last-id=${lastId}`);
472
- for (const [, entries] of results) {
473
- for (const [id, fields] of entries) {
474
- const message = streamEntryToMessage(parseStreamFields(fields));
475
- if (message) {
476
- await handleJobEvent(message, deps).catch((err) => {
477
- log("error", `replayStreamEvents: handleJobEvent error for entry ${id}:`, err.message);
478
- });
479
- }
480
- try {
481
- await redis.set(lastIdKey, id);
482
- }
483
- catch (err) {
484
- log("warn", "replayStreamEvents: failed to update last-id:", err.message);
485
- }
486
- }
487
- }
488
- log("info", "Stream replay complete.");
489
- }
490
- function makeDefaultDeps() {
491
- return {
492
- askClaude: defaultAskClaude,
493
- sendTelegramMessage: defaultSendTelegramMessage,
494
- spawnFollowupAgent: defaultSpawnFollowupAgent,
495
- readJobOutput: defaultReadJobOutput,
496
- readCoordinatorPlan: defaultReadCoordinatorPlan,
497
- getRunningJobCount: defaultGetRunningJobCount,
498
- getActiveChatIds: defaultGetActiveChatIds,
499
- };
500
- }
501
- let subscriberClient = null;
502
- /**
503
- * Connect to Redis and subscribe to cca:events.
504
- * Reconnects automatically on disconnect.
505
- * Call once at startup.
506
- */
507
- export async function connectEventSubscriber() {
508
- if (process.env.CC_AGENT_EVENTS_ENABLED === "false") {
509
- log("info", "CC_AGENT_EVENTS_ENABLED=false, skipping subscriber");
510
- return;
511
- }
512
- await connectWithBackoff(0);
513
- }
514
- async function connectWithBackoff(attempt) {
515
- const delay = Math.min(5_000 * Math.pow(2, attempt), 60_000);
516
- const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
517
- const botName = process.env.CC_TG_BOT_NAME ?? "cc-tg";
518
- const lastIdKey = `cca:event-stream:last-id:${botName}`;
519
- // Pub/sub subscriber client — enters subscriber mode after subscribe()
520
- const sub = new Redis(redisUrl, {
521
- lazyConnect: true,
522
- enableOfflineQueue: false,
523
- });
524
- // Regular command client — stays in normal mode for xread/get/set
525
- const reg = new Redis(redisUrl, {
526
- lazyConnect: true,
527
- enableOfflineQueue: true,
528
- });
529
- subscriberClient = sub;
530
- sub.on("error", (err) => {
531
- log("warn", "subscriber error, reconnecting...", err.message);
532
- try {
533
- sub.disconnect();
534
- }
535
- catch { }
536
- try {
537
- reg.disconnect();
538
- }
539
- catch { }
540
- setTimeout(() => connectWithBackoff(0), 5_000);
541
- });
542
- reg.on("error", (err) => {
543
- log("warn", "regular client error (non-fatal):", err.message);
544
- });
545
- try {
546
- await sub.connect();
547
- }
548
- catch (err) {
549
- log("warn", `Redis connect failed (attempt ${attempt}), retrying in ${delay}ms:`, err.message);
550
- try {
551
- sub.disconnect();
552
- }
553
- catch { }
554
- setTimeout(() => connectWithBackoff(attempt + 1), delay);
555
- return;
556
- }
557
- // Connect regular client (best-effort — stream replay is non-critical)
558
- try {
559
- await reg.connect();
560
- }
561
- catch (err) {
562
- log("warn", "Regular Redis client connect failed (stream replay skipped):", err.message);
563
- }
564
- const deps = makeDefaultDeps();
565
- // Replay events missed during downtime, then mark current time as last-id
566
- // Must happen BEFORE sub.subscribe() because subscribe() puts sub in subscriber mode
567
- try {
568
- await replayStreamEvents(reg, deps, botName);
569
- }
570
- catch (err) {
571
- log("warn", "Stream replay failed, continuing:", err.message);
572
- }
573
- // Mark current timestamp so next restart only replays events after now
574
- try {
575
- await reg.set(lastIdKey, `${Date.now()}-0`);
576
- }
577
- catch {
578
- // Non-fatal
579
- }
580
- sub.on("message", (channel, message) => {
581
- if (channel !== "cca:events")
582
- return;
583
- handleJobEvent(message, deps).then(() => {
584
- // Advance stream last-id so next restart doesn't re-replay this event
585
- reg.set(lastIdKey, `${Date.now()}-0`).catch(() => { });
586
- }).catch((err) => {
587
- log("error", "handleJobEvent uncaught:", err.message);
588
- });
589
- });
590
- try {
591
- await sub.subscribe("cca:events");
592
- log("info", "Subscribed to cca:events");
593
- }
594
- catch (err) {
595
- log("warn", "subscribe failed, retrying...", err.message);
596
- try {
597
- sub.disconnect();
598
- }
599
- catch { }
600
- try {
601
- reg.disconnect();
602
- }
603
- catch { }
604
- setTimeout(() => connectWithBackoff(attempt + 1), delay);
605
- return;
606
- }
607
- const cleanup = async () => {
608
- log("info", "SIGTERM received, shutting down event subscriber...");
609
- try {
610
- await sub.unsubscribe("cca:events");
611
- sub.disconnect();
612
- reg.disconnect();
613
- }
614
- catch { }
615
- };
616
- process.once("SIGTERM", () => { cleanup().catch(() => { }); });
617
- }
package/dist/claude.d.ts DELETED
@@ -1,54 +0,0 @@
1
- /**
2
- * Claude Code subprocess wrapper.
3
- * Mirrors ce_ce's mechanism: spawn `claude` CLI with stream-json I/O,
4
- * pipe prompts in, parse streaming JSON messages out.
5
- */
6
- import { EventEmitter } from "events";
7
- export type MessageType = "system" | "assistant" | "user" | "result";
8
- export interface ClaudeMessage {
9
- type: MessageType;
10
- session_id?: string;
11
- uuid?: string;
12
- payload: Record<string, unknown>;
13
- raw: Record<string, unknown>;
14
- }
15
- export interface ClaudeOptions {
16
- cwd?: string;
17
- systemPrompt?: string;
18
- /** OAuth token (sk-ant-oat01-...) or API key (sk-ant-api03-...) */
19
- token?: string;
20
- }
21
- export interface UsageEvent {
22
- inputTokens: number;
23
- outputTokens: number;
24
- cacheReadTokens: number;
25
- cacheWriteTokens: number;
26
- }
27
- export declare interface ClaudeProcess {
28
- on(event: "message", listener: (msg: ClaudeMessage) => void): this;
29
- on(event: "usage", listener: (usage: UsageEvent) => void): this;
30
- on(event: "error", listener: (err: Error) => void): this;
31
- on(event: "exit", listener: (code: number | null) => void): this;
32
- on(event: "stderr", listener: (data: string) => void): this;
33
- }
34
- export declare class ClaudeProcess extends EventEmitter {
35
- private proc;
36
- private buffer;
37
- private _exited;
38
- constructor(opts?: ClaudeOptions);
39
- sendPrompt(text: string): void;
40
- /**
41
- * Send an image (with optional text caption) to Claude via stream-json content blocks.
42
- * mediaType: image/jpeg | image/png | image/gif | image/webp
43
- */
44
- sendImage(base64Data: string, mediaType: string, caption?: string): void;
45
- kill(): void;
46
- get exited(): boolean;
47
- private drainBuffer;
48
- private parseMessage;
49
- }
50
- /**
51
- * Extract the text content from an assistant message payload.
52
- * Handles both simple string content and content-block arrays.
53
- */
54
- export declare function extractText(msg: ClaudeMessage): string;