clawless 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1115 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import http from 'node:http';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { Readable, Writable } from 'node:stream';
7
+ import * as acp from '@agentclientprotocol/sdk';
8
+ import { TelegramMessagingClient } from './messaging/telegramClient.js';
9
+ import { CronScheduler } from './scheduler/cronScheduler.js';
10
+ import { createScheduledJobHandler } from './scheduler/scheduledJobHandler.js';
11
+ import { runPromptWithTempAcp } from './acp/tempAcpRunner.js';
12
+ import { buildPermissionResponse, noOpAcpFileOperation } from './acp/clientHelpers.js';
13
+ import { getErrorMessage } from './utils/error.js';
14
+ import dotenv from 'dotenv';
15
+ // Load environment variables
16
+ dotenv.config();
17
+ // Validate required environment variables
18
+ if (!process.env.TELEGRAM_TOKEN) {
19
+ console.error('Error: TELEGRAM_TOKEN environment variable is required');
20
+ process.exit(1);
21
+ }
22
+ if (process.env.TELEGRAM_TOKEN.includes('your_telegram_bot_token_here') || !process.env.TELEGRAM_TOKEN.includes(':')) {
23
+ console.error('Error: TELEGRAM_TOKEN looks invalid. Set a real token from @BotFather in your config/env.');
24
+ process.exit(1);
25
+ }
26
+ const GEMINI_COMMAND = process.env.GEMINI_COMMAND || 'gemini';
27
+ const GEMINI_TIMEOUT_MS = parseInt(process.env.GEMINI_TIMEOUT_MS || '900000', 10);
28
+ const GEMINI_NO_OUTPUT_TIMEOUT_MS = parseInt(process.env.GEMINI_NO_OUTPUT_TIMEOUT_MS || '60000', 10);
29
+ const GEMINI_APPROVAL_MODE = process.env.GEMINI_APPROVAL_MODE || 'yolo';
30
+ const GEMINI_MODEL = process.env.GEMINI_MODEL || '';
31
+ const ACP_PERMISSION_STRATEGY = process.env.ACP_PERMISSION_STRATEGY || 'allow_once';
32
+ const ACP_STREAM_STDOUT = String(process.env.ACP_STREAM_STDOUT || '').toLowerCase() === 'true';
33
+ const ACP_DEBUG_STREAM = String(process.env.ACP_DEBUG_STREAM || '').toLowerCase() === 'true';
34
+ const HEARTBEAT_INTERVAL_MS = parseInt(process.env.HEARTBEAT_INTERVAL_MS || '60000', 10);
35
+ const ACP_PREWARM_RETRY_MS = parseInt(process.env.ACP_PREWARM_RETRY_MS || '30000', 10);
36
+ const GEMINI_KILL_GRACE_MS = parseInt(process.env.GEMINI_KILL_GRACE_MS || '5000', 10);
37
+ const AGENT_BRIDGE_HOME = process.env.AGENT_BRIDGE_HOME || path.join(os.homedir(), '.gemini-bridge');
38
+ const MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH || path.join(AGENT_BRIDGE_HOME, 'MEMORY.md');
39
+ const SCHEDULES_FILE_PATH = process.env.SCHEDULES_FILE_PATH || path.join(AGENT_BRIDGE_HOME, 'schedules.json');
40
+ const CALLBACK_CHAT_STATE_FILE_PATH = path.join(AGENT_BRIDGE_HOME, 'callback-chat-state.json');
41
+ const MEMORY_MAX_CHARS = parseInt(process.env.MEMORY_MAX_CHARS || '12000', 10);
42
+ const CALLBACK_HOST = process.env.CALLBACK_HOST || 'localhost';
43
+ const CALLBACK_PORT = parseInt(process.env.CALLBACK_PORT || '8788', 10);
44
+ const CALLBACK_AUTH_TOKEN = process.env.CALLBACK_AUTH_TOKEN || '';
45
+ const CALLBACK_MAX_BODY_BYTES = parseInt(process.env.CALLBACK_MAX_BODY_BYTES || '65536', 10);
46
+ // Typing indicator refresh interval (Telegram typing state expires quickly)
47
+ const TYPING_INTERVAL_MS = parseInt(process.env.TYPING_INTERVAL_MS || '4000', 10);
48
+ const TELEGRAM_STREAM_UPDATE_INTERVAL_MS = 1000;
49
+ // Maximum response length to prevent memory issues (Telegram has 4096 char limit anyway)
50
+ const MAX_RESPONSE_LENGTH = parseInt(process.env.MAX_RESPONSE_LENGTH || '4000', 10);
51
+ const messagingClient = new TelegramMessagingClient({
52
+ token: process.env.TELEGRAM_TOKEN,
53
+ typingIntervalMs: TYPING_INTERVAL_MS,
54
+ maxMessageLength: MAX_RESPONSE_LENGTH,
55
+ });
56
+ let geminiProcess = null;
57
+ let acpConnection = null;
58
+ let acpSessionId = null;
59
+ let acpInitPromise = null;
60
+ let activePromptCollector = null;
61
+ let manualAbortRequested = false;
62
+ let messageSequence = 0;
63
+ let acpPrewarmRetryTimer = null;
64
+ let geminiStderrTail = '';
65
+ let callbackServer = null;
66
+ let lastIncomingChatId = null;
67
+ const GEMINI_STDERR_TAIL_MAX = 4000;
68
+ function validateGeminiCommandOrExit() {
69
+ const result = spawnSync(GEMINI_COMMAND, ['--version'], {
70
+ stdio: 'ignore',
71
+ timeout: 5000,
72
+ killSignal: 'SIGKILL',
73
+ });
74
+ if (result.error?.code === 'ENOENT') {
75
+ console.error(`Error: GEMINI_COMMAND executable not found: ${GEMINI_COMMAND}`);
76
+ console.error('Install Gemini CLI or set GEMINI_COMMAND to a valid executable path.');
77
+ process.exit(1);
78
+ }
79
+ if (result.error) {
80
+ console.error(`Error: failed to execute GEMINI_COMMAND (${GEMINI_COMMAND}):`, result.error.message);
81
+ process.exit(1);
82
+ }
83
+ }
84
+ function terminateProcessGracefully(childProcess, processLabel, details) {
85
+ return new Promise((resolve) => {
86
+ if (!childProcess || childProcess.killed || childProcess.exitCode !== null) {
87
+ resolve();
88
+ return;
89
+ }
90
+ let settled = false;
91
+ const finalize = (reason) => {
92
+ if (settled) {
93
+ return;
94
+ }
95
+ settled = true;
96
+ logInfo('Gemini process termination finalized', {
97
+ processLabel,
98
+ reason,
99
+ pid: childProcess.pid,
100
+ ...details,
101
+ });
102
+ resolve();
103
+ };
104
+ childProcess.once('exit', () => finalize('exit'));
105
+ logInfo('Sending SIGTERM to Gemini process', {
106
+ processLabel,
107
+ pid: childProcess.pid,
108
+ graceMs: GEMINI_KILL_GRACE_MS,
109
+ ...details,
110
+ });
111
+ childProcess.kill('SIGTERM');
112
+ setTimeout(() => {
113
+ if (settled || childProcess.killed || childProcess.exitCode !== null) {
114
+ finalize('already-exited');
115
+ return;
116
+ }
117
+ logInfo('Escalating Gemini process termination to SIGKILL', {
118
+ processLabel,
119
+ pid: childProcess.pid,
120
+ ...details,
121
+ });
122
+ childProcess.kill('SIGKILL');
123
+ finalize('sigkill');
124
+ }, Math.max(0, GEMINI_KILL_GRACE_MS));
125
+ });
126
+ }
127
+ const handleScheduledJob = createScheduledJobHandler({
128
+ logInfo,
129
+ buildPromptWithMemory,
130
+ runScheduledPromptWithTempAcp,
131
+ resolveTargetChatId: () => resolveChatId(lastIncomingChatId),
132
+ sendTextToChat: (chatId, text) => messagingClient.sendTextToChat(chatId, text),
133
+ normalizeOutgoingText,
134
+ });
135
+ const cronScheduler = new CronScheduler(handleScheduledJob, {
136
+ persistenceFilePath: SCHEDULES_FILE_PATH,
137
+ timezone: process.env.TZ || 'UTC',
138
+ logInfo,
139
+ });
140
+ function ensureBridgeHomeDirectory() {
141
+ fs.mkdirSync(AGENT_BRIDGE_HOME, { recursive: true });
142
+ }
143
+ function loadPersistedCallbackChatId() {
144
+ try {
145
+ if (!fs.existsSync(CALLBACK_CHAT_STATE_FILE_PATH)) {
146
+ return null;
147
+ }
148
+ const parsed = JSON.parse(fs.readFileSync(CALLBACK_CHAT_STATE_FILE_PATH, 'utf8'));
149
+ return resolveChatId(parsed?.chatId);
150
+ }
151
+ catch (error) {
152
+ logInfo('Failed to load callback chat state', {
153
+ callbackChatStateFilePath: CALLBACK_CHAT_STATE_FILE_PATH,
154
+ error: getErrorMessage(error),
155
+ });
156
+ return null;
157
+ }
158
+ }
159
+ function persistCallbackChatId(chatId) {
160
+ try {
161
+ ensureBridgeHomeDirectory();
162
+ fs.writeFileSync(CALLBACK_CHAT_STATE_FILE_PATH, `${JSON.stringify({ chatId: String(chatId), updatedAt: new Date().toISOString() }, null, 2)}\n`, 'utf8');
163
+ }
164
+ catch (error) {
165
+ logInfo('Failed to persist callback chat state', {
166
+ callbackChatStateFilePath: CALLBACK_CHAT_STATE_FILE_PATH,
167
+ error: getErrorMessage(error),
168
+ });
169
+ }
170
+ }
171
+ function logInfo(message, details) {
172
+ const timestamp = new Date().toISOString();
173
+ if (details !== undefined) {
174
+ console.log(`[${timestamp}] ${message}`, details);
175
+ return;
176
+ }
177
+ console.log(`[${timestamp}] ${message}`);
178
+ }
179
+ function appendGeminiStderrTail(text) {
180
+ geminiStderrTail = `${geminiStderrTail}${text}`;
181
+ if (geminiStderrTail.length > GEMINI_STDERR_TAIL_MAX) {
182
+ geminiStderrTail = geminiStderrTail.slice(-GEMINI_STDERR_TAIL_MAX);
183
+ }
184
+ }
185
+ function sendJson(res, statusCode, payload) {
186
+ res.statusCode = statusCode;
187
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
188
+ res.end(JSON.stringify(payload));
189
+ }
190
+ function isCallbackAuthorized(req) {
191
+ if (!CALLBACK_AUTH_TOKEN) {
192
+ return true;
193
+ }
194
+ const headerToken = req.headers['x-callback-token'];
195
+ const authHeader = req.headers.authorization;
196
+ const bearerToken = typeof authHeader === 'string' && authHeader.startsWith('Bearer ')
197
+ ? authHeader.slice('Bearer '.length)
198
+ : null;
199
+ return headerToken === CALLBACK_AUTH_TOKEN || bearerToken === CALLBACK_AUTH_TOKEN;
200
+ }
201
+ function readRequestBody(req, maxBytes) {
202
+ return new Promise((resolve, reject) => {
203
+ const chunks = [];
204
+ let total = 0;
205
+ req.on('data', (chunk) => {
206
+ total += chunk.length;
207
+ if (total > maxBytes) {
208
+ reject(new Error(`Payload too large (>${maxBytes} bytes)`));
209
+ req.destroy();
210
+ return;
211
+ }
212
+ chunks.push(chunk);
213
+ });
214
+ req.on('end', () => {
215
+ resolve(Buffer.concat(chunks).toString('utf8'));
216
+ });
217
+ req.on('error', (error) => {
218
+ reject(error);
219
+ });
220
+ });
221
+ }
222
+ function resolveChatId(value) {
223
+ if (value === undefined || value === null) {
224
+ return null;
225
+ }
226
+ const normalized = String(value).trim();
227
+ if (!normalized) {
228
+ return null;
229
+ }
230
+ if (/^-?\d+$/.test(normalized)) {
231
+ return normalized;
232
+ }
233
+ return normalized;
234
+ }
235
+ function hasActiveAcpPrompt() {
236
+ return Boolean(activePromptCollector && acpConnection && acpSessionId);
237
+ }
238
+ function normalizeCommandText(text) {
239
+ return String(text || '')
240
+ .trim()
241
+ .toLowerCase()
242
+ .replace(/[!?.]+$/g, '');
243
+ }
244
+ function isAbortCommand(text) {
245
+ const normalized = normalizeCommandText(text);
246
+ if (!normalized) {
247
+ return false;
248
+ }
249
+ const commands = new Set(['abort', 'cancel', 'stop', '/abort', '/cancel', '/stop']);
250
+ if (commands.has(normalized)) {
251
+ return true;
252
+ }
253
+ const compact = normalized.replace(/\s+/g, ' ');
254
+ return compact === 'please abort' || compact === 'please cancel' || compact === 'please stop';
255
+ }
256
+ function normalizeOutgoingText(text) {
257
+ const normalized = String(text || '').trim();
258
+ return normalized;
259
+ }
260
+ async function handleSchedulerRequest(req, res, requestUrl) {
261
+ if (!isCallbackAuthorized(req)) {
262
+ sendJson(res, 401, { ok: false, error: 'Unauthorized' });
263
+ return;
264
+ }
265
+ // POST /api/schedule - Create a new schedule
266
+ if (requestUrl.pathname === '/api/schedule' && req.method === 'POST') {
267
+ try {
268
+ const bodyText = await readRequestBody(req, CALLBACK_MAX_BODY_BYTES);
269
+ const body = bodyText ? JSON.parse(bodyText) : {};
270
+ // Validate required fields
271
+ if (!body.message || typeof body.message !== 'string') {
272
+ sendJson(res, 400, { ok: false, error: 'Field `message` is required and must be a string' });
273
+ return;
274
+ }
275
+ // Validate optional string fields
276
+ if (body.description !== undefined && typeof body.description !== 'string') {
277
+ sendJson(res, 400, { ok: false, error: 'Field `description` must be a string' });
278
+ return;
279
+ }
280
+ if (body.cronExpression !== undefined && typeof body.cronExpression !== 'string') {
281
+ sendJson(res, 400, { ok: false, error: 'Field `cronExpression` must be a string' });
282
+ return;
283
+ }
284
+ // Parse runAt date if provided
285
+ let runAt;
286
+ if (body.runAt) {
287
+ runAt = new Date(body.runAt);
288
+ if (isNaN(runAt.getTime())) {
289
+ sendJson(res, 400, { ok: false, error: 'Invalid date format for `runAt`' });
290
+ return;
291
+ }
292
+ }
293
+ const schedule = cronScheduler.createSchedule({
294
+ message: body.message,
295
+ description: body.description,
296
+ cronExpression: body.cronExpression,
297
+ oneTime: body.oneTime === true,
298
+ runAt,
299
+ });
300
+ logInfo('Schedule created', { scheduleId: schedule.id });
301
+ sendJson(res, 201, { ok: true, schedule });
302
+ }
303
+ catch (error) {
304
+ logInfo('Failed to create schedule', { error: getErrorMessage(error) });
305
+ sendJson(res, 400, { ok: false, error: getErrorMessage(error, 'Failed to create schedule') });
306
+ }
307
+ return;
308
+ }
309
+ // GET /api/schedule - List all schedules
310
+ if (requestUrl.pathname === '/api/schedule' && req.method === 'GET') {
311
+ try {
312
+ const schedules = cronScheduler.listSchedules();
313
+ sendJson(res, 200, { ok: true, schedules });
314
+ }
315
+ catch (error) {
316
+ sendJson(res, 500, { ok: false, error: getErrorMessage(error, 'Failed to list schedules') });
317
+ }
318
+ return;
319
+ }
320
+ // GET /api/schedule/:id - Get a specific schedule
321
+ const getScheduleMatch = requestUrl.pathname.match(/^\/api\/schedule\/([^/]+)$/);
322
+ if (getScheduleMatch && req.method === 'GET') {
323
+ try {
324
+ const scheduleId = getScheduleMatch[1];
325
+ const schedule = cronScheduler.getSchedule(scheduleId);
326
+ if (!schedule) {
327
+ sendJson(res, 404, { ok: false, error: 'Schedule not found' });
328
+ return;
329
+ }
330
+ sendJson(res, 200, { ok: true, schedule });
331
+ }
332
+ catch (error) {
333
+ sendJson(res, 500, { ok: false, error: getErrorMessage(error, 'Failed to get schedule') });
334
+ }
335
+ return;
336
+ }
337
+ // DELETE /api/schedule/:id - Delete a schedule
338
+ const deleteScheduleMatch = requestUrl.pathname.match(/^\/api\/schedule\/([^/]+)$/);
339
+ if (deleteScheduleMatch && req.method === 'DELETE') {
340
+ try {
341
+ const scheduleId = deleteScheduleMatch[1];
342
+ const removed = cronScheduler.removeSchedule(scheduleId);
343
+ if (!removed) {
344
+ sendJson(res, 404, { ok: false, error: 'Schedule not found' });
345
+ return;
346
+ }
347
+ logInfo('Schedule removed', { scheduleId });
348
+ sendJson(res, 200, { ok: true, message: 'Schedule removed' });
349
+ }
350
+ catch (error) {
351
+ sendJson(res, 500, { ok: false, error: getErrorMessage(error, 'Failed to remove schedule') });
352
+ }
353
+ return;
354
+ }
355
+ sendJson(res, 404, { ok: false, error: 'Not found' });
356
+ }
357
+ async function handleCallbackRequest(req, res) {
358
+ const hostHeader = req.headers.host || `${CALLBACK_HOST}:${CALLBACK_PORT}`;
359
+ const requestUrl = new URL(req.url || '/', `http://${hostHeader}`);
360
+ // Health check endpoint
361
+ if (requestUrl.pathname === '/healthz') {
362
+ sendJson(res, 200, { ok: true });
363
+ return;
364
+ }
365
+ // Scheduler endpoints
366
+ if (requestUrl.pathname.startsWith('/api/schedule')) {
367
+ await handleSchedulerRequest(req, res, requestUrl);
368
+ return;
369
+ }
370
+ // Original callback/telegram endpoint
371
+ if (requestUrl.pathname !== '/callback/telegram') {
372
+ sendJson(res, 404, { ok: false, error: 'Not found' });
373
+ return;
374
+ }
375
+ if (req.method !== 'POST') {
376
+ sendJson(res, 405, { ok: false, error: 'Method not allowed' });
377
+ return;
378
+ }
379
+ if (!isCallbackAuthorized(req)) {
380
+ sendJson(res, 401, { ok: false, error: 'Unauthorized' });
381
+ return;
382
+ }
383
+ let body = null;
384
+ try {
385
+ const bodyText = await readRequestBody(req, CALLBACK_MAX_BODY_BYTES);
386
+ body = bodyText ? JSON.parse(bodyText) : {};
387
+ }
388
+ catch (error) {
389
+ sendJson(res, 400, { ok: false, error: getErrorMessage(error, 'Invalid JSON body') });
390
+ return;
391
+ }
392
+ const callbackText = normalizeOutgoingText(body?.text);
393
+ if (!callbackText) {
394
+ sendJson(res, 400, { ok: false, error: 'Field `text` is required' });
395
+ return;
396
+ }
397
+ const targetChatId = resolveChatId(body?.chatId
398
+ ?? requestUrl.searchParams.get('chatId')
399
+ ?? lastIncomingChatId);
400
+ if (!targetChatId) {
401
+ sendJson(res, 400, {
402
+ ok: false,
403
+ error: 'No chat id available. Send one Telegram message to the bot once to bind a target chat, or provide `chatId` in this callback request.',
404
+ });
405
+ return;
406
+ }
407
+ try {
408
+ await messagingClient.sendTextToChat(targetChatId, callbackText);
409
+ logInfo('Callback message sent', { targetChatId });
410
+ sendJson(res, 200, { ok: true, chatId: targetChatId });
411
+ }
412
+ catch (error) {
413
+ sendJson(res, 500, { ok: false, error: getErrorMessage(error, 'Failed to send Telegram message') });
414
+ }
415
+ }
416
+ function startCallbackServer() {
417
+ if (callbackServer) {
418
+ return;
419
+ }
420
+ callbackServer = http.createServer((req, res) => {
421
+ handleCallbackRequest(req, res).catch((error) => {
422
+ sendJson(res, 500, { ok: false, error: getErrorMessage(error, 'Internal callback server error') });
423
+ });
424
+ });
425
+ callbackServer.on('error', (error) => {
426
+ if (error.code === 'EADDRINUSE') {
427
+ logInfo('Callback server port already in use; skipping local callback listener for this process', {
428
+ host: CALLBACK_HOST,
429
+ port: CALLBACK_PORT,
430
+ });
431
+ callbackServer?.close();
432
+ callbackServer = null;
433
+ return;
434
+ }
435
+ console.error('Callback server error:', error);
436
+ });
437
+ callbackServer.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
438
+ logInfo('Callback server listening', {
439
+ host: CALLBACK_HOST,
440
+ port: CALLBACK_PORT,
441
+ authEnabled: Boolean(CALLBACK_AUTH_TOKEN),
442
+ endpoint: '/callback/telegram',
443
+ });
444
+ });
445
+ }
446
+ function stopCallbackServer() {
447
+ if (!callbackServer) {
448
+ return;
449
+ }
450
+ callbackServer.close();
451
+ callbackServer = null;
452
+ }
453
+ async function cancelActiveAcpPrompt() {
454
+ try {
455
+ if (acpConnection && acpSessionId) {
456
+ await acpConnection.cancel({ sessionId: acpSessionId });
457
+ }
458
+ }
459
+ catch (_) {
460
+ }
461
+ }
462
+ async function shutdownAcpRuntime(reason) {
463
+ const processToStop = geminiProcess;
464
+ const runtimeSessionId = acpSessionId;
465
+ activePromptCollector = null;
466
+ acpConnection = null;
467
+ acpSessionId = null;
468
+ acpInitPromise = null;
469
+ geminiProcess = null;
470
+ geminiStderrTail = '';
471
+ if (processToStop && !processToStop.killed && processToStop.exitCode === null) {
472
+ await terminateProcessGracefully(processToStop, 'main-acp-runtime', {
473
+ reason,
474
+ sessionId: runtimeSessionId,
475
+ });
476
+ }
477
+ }
478
+ function setupGracefulShutdown() {
479
+ const shutdownSignals = ['SIGINT', 'SIGTERM'];
480
+ for (const signal of shutdownSignals) {
481
+ process.once(signal, () => {
482
+ console.log(`Received ${signal}, stopping bot...`);
483
+ cronScheduler.shutdown();
484
+ stopCallbackServer();
485
+ messagingClient.stop(signal);
486
+ void shutdownAcpRuntime(`signal:${signal}`);
487
+ });
488
+ }
489
+ }
490
+ function ensureMemoryFile() {
491
+ ensureBridgeHomeDirectory();
492
+ fs.mkdirSync(path.dirname(MEMORY_FILE_PATH), { recursive: true });
493
+ if (!fs.existsSync(MEMORY_FILE_PATH)) {
494
+ const template = [
495
+ '# Gemini Bridge Memory',
496
+ '',
497
+ 'This file stores durable memory notes for Gemini Bridge.',
498
+ '',
499
+ '## Notes',
500
+ '',
501
+ ].join('\n');
502
+ fs.writeFileSync(MEMORY_FILE_PATH, `${template}\n`, 'utf8');
503
+ logInfo('Created memory file', { memoryFilePath: MEMORY_FILE_PATH });
504
+ }
505
+ }
506
+ function readMemoryContext() {
507
+ try {
508
+ const content = fs.readFileSync(MEMORY_FILE_PATH, 'utf8');
509
+ if (content.length <= MEMORY_MAX_CHARS) {
510
+ return content;
511
+ }
512
+ return content.slice(-MEMORY_MAX_CHARS);
513
+ }
514
+ catch (error) {
515
+ logInfo('Unable to read memory file; continuing without memory context', {
516
+ memoryFilePath: MEMORY_FILE_PATH,
517
+ error: getErrorMessage(error),
518
+ });
519
+ return '';
520
+ }
521
+ }
522
+ function buildPromptWithMemory(userPrompt) {
523
+ const memoryContext = readMemoryContext() || '(No saved memory yet)';
524
+ const callbackEndpoint = `http://${CALLBACK_HOST}:${CALLBACK_PORT}/callback/telegram`;
525
+ const scheduleEndpoint = `http://${CALLBACK_HOST}:${CALLBACK_PORT}/api/schedule`;
526
+ return [
527
+ 'System instruction:',
528
+ `- Persistent memory file path: ${MEMORY_FILE_PATH}`,
529
+ '- If user asks to remember/memorize/save for later, append a concise bullet under "## Notes" in that file.',
530
+ '- Do not overwrite existing memory entries; append only.',
531
+ `- Callback endpoint for proactive notifications (cron/jobs): POST ${callbackEndpoint}`,
532
+ '- Callback payload should include a JSON `text` field; `chatId` is optional.',
533
+ `- Persisted callback chat binding file: ${CALLBACK_CHAT_STATE_FILE_PATH}`,
534
+ '- If no `chatId` is provided, the bridge sends to the persisted bound chat.',
535
+ '- For scheduled jobs, include callback delivery steps so results are pushed to Telegram when jobs complete.',
536
+ '',
537
+ '**Scheduler API:**',
538
+ `- Create schedule: POST ${scheduleEndpoint}`,
539
+ ' Body format for recurring: {"message": "prompt text", "description": "optional", "cronExpression": "* * * * *"}',
540
+ ' Body format for one-time: {"message": "prompt text", "description": "optional", "oneTime": true, "runAt": "2026-12-31T23:59:59Z"}',
541
+ ' Cron format: "minute hour day month weekday" (e.g., "0 9 * * *" = daily at 9am, "*/5 * * * *" = every 5 minutes)',
542
+ `- List schedules: GET ${scheduleEndpoint}`,
543
+ `- Get schedule: GET ${scheduleEndpoint}/:id`,
544
+ `- Delete schedule: DELETE ${scheduleEndpoint}/:id`,
545
+ '- When schedule runs, it executes the message through Gemini CLI and sends results to Telegram.',
546
+ '- Use this API when user asks to schedule tasks, set reminders, or create recurring jobs.',
547
+ CALLBACK_AUTH_TOKEN
548
+ ? '- Scheduler auth is enabled: include `x-callback-token` (or bearer token) header when creating requests.'
549
+ : '- Scheduler auth is disabled unless CALLBACK_AUTH_TOKEN is configured.',
550
+ '',
551
+ 'Current memory context:',
552
+ memoryContext,
553
+ '',
554
+ 'User message:',
555
+ userPrompt,
556
+ ].join('\n');
557
+ }
558
+ class TelegramAcpClient {
559
+ async requestPermission(params) {
560
+ return buildPermissionResponse(params?.options, ACP_PERMISSION_STRATEGY);
561
+ }
562
+ async sessionUpdate(params) {
563
+ if (!activePromptCollector || params.sessionId !== acpSessionId) {
564
+ return;
565
+ }
566
+ activePromptCollector.onActivity();
567
+ if (params.update?.sessionUpdate === 'agent_message_chunk' && params.update?.content?.type === 'text') {
568
+ const chunkText = params.update.content.text;
569
+ activePromptCollector.append(chunkText);
570
+ if (ACP_STREAM_STDOUT && chunkText) {
571
+ process.stdout.write(chunkText);
572
+ }
573
+ }
574
+ }
575
+ async readTextFile(_params) {
576
+ return noOpAcpFileOperation(_params);
577
+ }
578
+ async writeTextFile(_params) {
579
+ return noOpAcpFileOperation(_params);
580
+ }
581
+ }
582
+ const acpClient = new TelegramAcpClient();
583
+ function resetAcpRuntime() {
584
+ logInfo('Resetting ACP runtime state');
585
+ void shutdownAcpRuntime('runtime-reset');
586
+ scheduleAcpPrewarm('runtime reset');
587
+ }
588
+ function scheduleAcpPrewarm(reason) {
589
+ if (hasHealthyAcpRuntime() || acpInitPromise) {
590
+ return;
591
+ }
592
+ if (acpPrewarmRetryTimer) {
593
+ return;
594
+ }
595
+ logInfo('Triggering ACP prewarm', { reason });
596
+ ensureAcpSession()
597
+ .then(() => {
598
+ logInfo('Gemini ACP prewarm complete');
599
+ })
600
+ .catch((error) => {
601
+ logInfo('Gemini ACP prewarm failed', { error: getErrorMessage(error) });
602
+ if (ACP_PREWARM_RETRY_MS > 0) {
603
+ acpPrewarmRetryTimer = setTimeout(() => {
604
+ acpPrewarmRetryTimer = null;
605
+ scheduleAcpPrewarm('retry');
606
+ }, ACP_PREWARM_RETRY_MS);
607
+ }
608
+ });
609
+ }
610
+ function buildGeminiAcpArgs() {
611
+ const args = ['--experimental-acp'];
612
+ const includeDirectories = new Set([AGENT_BRIDGE_HOME, os.homedir()]);
613
+ for (const includeDirectory of includeDirectories) {
614
+ args.push('--include-directories', includeDirectory);
615
+ }
616
+ if (GEMINI_APPROVAL_MODE) {
617
+ args.push('--approval-mode', GEMINI_APPROVAL_MODE);
618
+ }
619
+ if (GEMINI_MODEL) {
620
+ args.push('--model', GEMINI_MODEL);
621
+ }
622
+ return args;
623
+ }
624
+ async function ensureAcpSession() {
625
+ ensureMemoryFile();
626
+ if (acpConnection && acpSessionId && geminiProcess && !geminiProcess.killed) {
627
+ return;
628
+ }
629
+ if (acpInitPromise) {
630
+ await acpInitPromise;
631
+ return;
632
+ }
633
+ acpInitPromise = (async () => {
634
+ const args = buildGeminiAcpArgs();
635
+ logInfo('Starting Gemini ACP process', { command: GEMINI_COMMAND, args });
636
+ geminiStderrTail = '';
637
+ geminiProcess = spawn(GEMINI_COMMAND, args, {
638
+ stdio: ['pipe', 'pipe', 'pipe'],
639
+ cwd: process.cwd(),
640
+ });
641
+ geminiProcess.stderr.on('data', (chunk) => {
642
+ const rawText = chunk.toString();
643
+ appendGeminiStderrTail(rawText);
644
+ const text = rawText.trim();
645
+ if (text) {
646
+ console.error(`[gemini] ${text}`);
647
+ }
648
+ if (activePromptCollector) {
649
+ activePromptCollector.onActivity();
650
+ }
651
+ });
652
+ geminiProcess.on('error', (error) => {
653
+ console.error('Gemini ACP process error:', error.message);
654
+ resetAcpRuntime();
655
+ });
656
+ geminiProcess.on('close', (code, signal) => {
657
+ console.error(`Gemini ACP process closed (code=${code}, signal=${signal})`);
658
+ resetAcpRuntime();
659
+ });
660
+ // ACP uses JSON-RPC over streams; Gemini stdio is the ACP transport here.
661
+ const input = Writable.toWeb(geminiProcess.stdin);
662
+ const output = Readable.toWeb(geminiProcess.stdout);
663
+ const stream = acp.ndJsonStream(input, output);
664
+ acpConnection = new acp.ClientSideConnection(() => acpClient, stream);
665
+ try {
666
+ await acpConnection.initialize({
667
+ protocolVersion: acp.PROTOCOL_VERSION,
668
+ clientCapabilities: {},
669
+ });
670
+ logInfo('ACP connection initialized');
671
+ const session = await acpConnection.newSession({
672
+ cwd: process.cwd(),
673
+ mcpServers: [],
674
+ });
675
+ acpSessionId = session.sessionId;
676
+ logInfo('ACP session ready', { sessionId: acpSessionId });
677
+ }
678
+ catch (error) {
679
+ const baseMessage = getErrorMessage(error);
680
+ const isInternalError = baseMessage.includes('Internal error');
681
+ const hint = isInternalError
682
+ ? 'Gemini ACP newSession returned Internal error. This is often caused by a local MCP server or skill initialization issue. Try launching `gemini` directly and checking MCP/skills diagnostics.'
683
+ : '';
684
+ logInfo('ACP initialization failed', {
685
+ error: baseMessage,
686
+ stderrTail: geminiStderrTail || '(empty)',
687
+ });
688
+ resetAcpRuntime();
689
+ throw new Error(hint ? `${baseMessage}. ${hint}` : baseMessage);
690
+ }
691
+ })();
692
+ try {
693
+ await acpInitPromise;
694
+ }
695
+ finally {
696
+ acpInitPromise = null;
697
+ }
698
+ }
699
+ function hasHealthyAcpRuntime() {
700
+ return Boolean(acpConnection && acpSessionId && geminiProcess && !geminiProcess.killed);
701
+ }
702
+ const messageQueue = [];
703
+ let isQueueProcessing = false;
704
+ function enqueueMessage(messageContext) {
705
+ return new Promise((resolve, reject) => {
706
+ const requestId = ++messageSequence;
707
+ messageQueue.push({ requestId, messageContext, resolve, reject });
708
+ logInfo('Message enqueued', { requestId, queueLength: messageQueue.length });
709
+ processQueue().catch((error) => {
710
+ console.error('Queue processor failed:', error);
711
+ });
712
+ });
713
+ }
714
+ async function processQueue() {
715
+ if (isQueueProcessing) {
716
+ return;
717
+ }
718
+ isQueueProcessing = true;
719
+ while (messageQueue.length > 0) {
720
+ const item = messageQueue.shift();
721
+ if (!item) {
722
+ continue;
723
+ }
724
+ try {
725
+ logInfo('Processing queued message', { requestId: item.requestId, queueLength: messageQueue.length });
726
+ await processSingleMessage(item.messageContext, item.requestId);
727
+ logInfo('Message processed', { requestId: item.requestId });
728
+ item.resolve();
729
+ }
730
+ catch (error) {
731
+ logInfo('Message processing failed', { requestId: item.requestId, error: getErrorMessage(error) });
732
+ item.reject(error);
733
+ }
734
+ }
735
+ isQueueProcessing = false;
736
+ }
737
+ async function runScheduledPromptWithTempAcp(promptForGemini, scheduleId) {
738
+ return runPromptWithTempAcp({
739
+ scheduleId,
740
+ promptForGemini,
741
+ command: GEMINI_COMMAND,
742
+ args: buildGeminiAcpArgs(),
743
+ cwd: process.cwd(),
744
+ timeoutMs: GEMINI_TIMEOUT_MS,
745
+ noOutputTimeoutMs: GEMINI_NO_OUTPUT_TIMEOUT_MS,
746
+ permissionStrategy: ACP_PERMISSION_STRATEGY,
747
+ stderrTailMaxChars: GEMINI_STDERR_TAIL_MAX,
748
+ logInfo,
749
+ });
750
+ }
751
+ /**
752
+ * Streams text output from Gemini CLI for a single prompt.
753
+ */
754
+ async function runAcpPrompt(promptText, onChunk) {
755
+ await ensureAcpSession();
756
+ const promptInvocationId = `telegram-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
757
+ logInfo('Starting ACP prompt', {
758
+ invocationId: promptInvocationId,
759
+ sessionId: acpSessionId,
760
+ promptLength: promptText.length,
761
+ });
762
+ const promptForGemini = buildPromptWithMemory(promptText);
763
+ return new Promise((resolve, reject) => {
764
+ let fullResponse = '';
765
+ let isSettled = false;
766
+ let noOutputTimeout = null;
767
+ const startedAt = Date.now();
768
+ let chunkCount = 0;
769
+ let firstChunkAt = null;
770
+ const clearTimers = () => {
771
+ clearTimeout(overallTimeout);
772
+ if (noOutputTimeout) {
773
+ clearTimeout(noOutputTimeout);
774
+ }
775
+ };
776
+ const failOnce = (error) => {
777
+ if (isSettled) {
778
+ return;
779
+ }
780
+ isSettled = true;
781
+ manualAbortRequested = false;
782
+ clearTimers();
783
+ activePromptCollector = null;
784
+ logInfo('ACP prompt failed', {
785
+ invocationId: promptInvocationId,
786
+ sessionId: acpSessionId,
787
+ chunkCount,
788
+ firstChunkDelayMs: firstChunkAt ? firstChunkAt - startedAt : null,
789
+ elapsedMs: Date.now() - startedAt,
790
+ error: getErrorMessage(error),
791
+ });
792
+ reject(error);
793
+ };
794
+ const resolveOnce = (value) => {
795
+ if (isSettled) {
796
+ return;
797
+ }
798
+ isSettled = true;
799
+ manualAbortRequested = false;
800
+ clearTimers();
801
+ activePromptCollector = null;
802
+ logInfo('ACP prompt completed', {
803
+ invocationId: promptInvocationId,
804
+ sessionId: acpSessionId,
805
+ chunkCount,
806
+ firstChunkDelayMs: firstChunkAt ? firstChunkAt - startedAt : null,
807
+ elapsedMs: Date.now() - startedAt,
808
+ responseLength: value.length,
809
+ });
810
+ resolve(value);
811
+ };
812
+ const refreshNoOutputTimer = () => {
813
+ if (!GEMINI_NO_OUTPUT_TIMEOUT_MS || GEMINI_NO_OUTPUT_TIMEOUT_MS <= 0) {
814
+ return;
815
+ }
816
+ if (noOutputTimeout) {
817
+ clearTimeout(noOutputTimeout);
818
+ }
819
+ noOutputTimeout = setTimeout(async () => {
820
+ await cancelActiveAcpPrompt();
821
+ failOnce(new Error(`Gemini ACP produced no output for ${GEMINI_NO_OUTPUT_TIMEOUT_MS}ms`));
822
+ }, GEMINI_NO_OUTPUT_TIMEOUT_MS);
823
+ };
824
+ const overallTimeout = setTimeout(async () => {
825
+ await cancelActiveAcpPrompt();
826
+ failOnce(new Error(`Gemini ACP timed out after ${GEMINI_TIMEOUT_MS}ms`));
827
+ }, GEMINI_TIMEOUT_MS);
828
+ activePromptCollector = {
829
+ onActivity: refreshNoOutputTimer,
830
+ append: (textChunk) => {
831
+ refreshNoOutputTimer();
832
+ chunkCount += 1;
833
+ if (!firstChunkAt) {
834
+ firstChunkAt = Date.now();
835
+ }
836
+ if (ACP_DEBUG_STREAM) {
837
+ logInfo('ACP chunk received', {
838
+ invocationId: promptInvocationId,
839
+ chunkIndex: chunkCount,
840
+ chunkLength: textChunk.length,
841
+ elapsedMs: Date.now() - startedAt,
842
+ bufferLengthBeforeAppend: fullResponse.length,
843
+ });
844
+ }
845
+ fullResponse += textChunk;
846
+ if (onChunk) {
847
+ try {
848
+ onChunk(textChunk);
849
+ }
850
+ catch (_) {
851
+ }
852
+ }
853
+ },
854
+ };
855
+ refreshNoOutputTimer();
856
+ acpConnection.prompt({
857
+ sessionId: acpSessionId,
858
+ prompt: [
859
+ {
860
+ type: 'text',
861
+ text: promptForGemini,
862
+ },
863
+ ],
864
+ })
865
+ .then((result) => {
866
+ if (ACP_DEBUG_STREAM) {
867
+ logInfo('ACP prompt stop reason', {
868
+ invocationId: promptInvocationId,
869
+ stopReason: result?.stopReason || '(none)',
870
+ chunkCount,
871
+ bufferedLength: fullResponse.length,
872
+ deliveryMode: 'telegram-live-preview-then-final',
873
+ });
874
+ }
875
+ if (result?.stopReason === 'cancelled' && !fullResponse) {
876
+ failOnce(new Error(manualAbortRequested ? 'Gemini ACP prompt was aborted by user' : 'Gemini ACP prompt was cancelled'));
877
+ return;
878
+ }
879
+ resolveOnce(fullResponse || 'No response received.');
880
+ })
881
+ .catch((error) => {
882
+ failOnce(new Error(error?.message || 'Gemini ACP prompt failed'));
883
+ });
884
+ });
885
+ }
886
+ async function processSingleMessage(messageContext, messageRequestId) {
887
+ logInfo('Starting Telegram message processing', {
888
+ requestId: messageRequestId,
889
+ chatId: messageContext.chatId,
890
+ });
891
+ const stopTypingIndicator = messageContext.startTyping();
892
+ let liveMessageId;
893
+ let previewBuffer = '';
894
+ let flushTimer = null;
895
+ let lastFlushAt = 0;
896
+ let finalizedViaLiveMessage = false;
897
+ let startingLiveMessage = null;
898
+ let promptCompleted = false;
899
+ const clearFlushTimer = () => {
900
+ if (flushTimer) {
901
+ clearTimeout(flushTimer);
902
+ flushTimer = null;
903
+ }
904
+ };
905
+ const previewText = () => {
906
+ if (previewBuffer.length <= MAX_RESPONSE_LENGTH) {
907
+ return previewBuffer;
908
+ }
909
+ return `${previewBuffer.slice(0, MAX_RESPONSE_LENGTH - 1)}…`;
910
+ };
911
+ const flushPreview = async (force = false) => {
912
+ if (!liveMessageId || finalizedViaLiveMessage) {
913
+ return;
914
+ }
915
+ const now = Date.now();
916
+ if (!force && now - lastFlushAt < TELEGRAM_STREAM_UPDATE_INTERVAL_MS) {
917
+ return;
918
+ }
919
+ lastFlushAt = now;
920
+ const text = previewText();
921
+ if (!text) {
922
+ return;
923
+ }
924
+ try {
925
+ await messageContext.updateLiveMessage(liveMessageId, text);
926
+ if (ACP_DEBUG_STREAM) {
927
+ logInfo('Telegram live preview updated', {
928
+ requestId: messageRequestId,
929
+ previewLength: text.length,
930
+ });
931
+ }
932
+ }
933
+ catch (error) {
934
+ const errorMessage = getErrorMessage(error).toLowerCase();
935
+ if (!errorMessage.includes('message is not modified')) {
936
+ logInfo('Telegram live preview update skipped', {
937
+ requestId: messageRequestId,
938
+ error: getErrorMessage(error),
939
+ });
940
+ }
941
+ }
942
+ };
943
+ const scheduleFlush = () => {
944
+ if (flushTimer) {
945
+ return;
946
+ }
947
+ const dueIn = Math.max(0, TELEGRAM_STREAM_UPDATE_INTERVAL_MS - (Date.now() - lastFlushAt));
948
+ flushTimer = setTimeout(async () => {
949
+ flushTimer = null;
950
+ await flushPreview(true);
951
+ }, dueIn);
952
+ };
953
+ const ensureLiveMessageStarted = async () => {
954
+ if (liveMessageId || finalizedViaLiveMessage) {
955
+ return;
956
+ }
957
+ if (startingLiveMessage) {
958
+ await startingLiveMessage;
959
+ return;
960
+ }
961
+ startingLiveMessage = (async () => {
962
+ try {
963
+ const initialPreview = previewText() || '…';
964
+ liveMessageId = await messageContext.startLiveMessage(initialPreview);
965
+ lastFlushAt = Date.now();
966
+ }
967
+ catch (_) {
968
+ liveMessageId = undefined;
969
+ }
970
+ })();
971
+ try {
972
+ await startingLiveMessage;
973
+ }
974
+ finally {
975
+ startingLiveMessage = null;
976
+ }
977
+ };
978
+ try {
979
+ const fullResponse = await runAcpPrompt(messageContext.text, (chunk) => {
980
+ previewBuffer += chunk;
981
+ void ensureLiveMessageStarted();
982
+ void scheduleFlush();
983
+ });
984
+ promptCompleted = true;
985
+ clearFlushTimer();
986
+ await flushPreview(true);
987
+ if (startingLiveMessage) {
988
+ try {
989
+ await startingLiveMessage;
990
+ }
991
+ catch (_) {
992
+ }
993
+ }
994
+ if (liveMessageId) {
995
+ try {
996
+ await messageContext.finalizeLiveMessage(liveMessageId, fullResponse || 'No response received.');
997
+ finalizedViaLiveMessage = true;
998
+ }
999
+ catch (error) {
1000
+ finalizedViaLiveMessage = true;
1001
+ logInfo('Live message finalize failed; keeping streamed message as final output', {
1002
+ requestId: messageRequestId,
1003
+ error: getErrorMessage(error),
1004
+ });
1005
+ }
1006
+ }
1007
+ if (!finalizedViaLiveMessage && ACP_DEBUG_STREAM) {
1008
+ logInfo('Sending Telegram final response', {
1009
+ requestId: messageRequestId,
1010
+ responseLength: (fullResponse || '').length,
1011
+ });
1012
+ }
1013
+ if (!finalizedViaLiveMessage) {
1014
+ await messageContext.sendText(fullResponse || 'No response received.');
1015
+ }
1016
+ }
1017
+ finally {
1018
+ clearFlushTimer();
1019
+ if (liveMessageId && !finalizedViaLiveMessage && !promptCompleted) {
1020
+ try {
1021
+ await messageContext.removeMessage(liveMessageId);
1022
+ }
1023
+ catch (_) {
1024
+ }
1025
+ }
1026
+ stopTypingIndicator();
1027
+ logInfo('Finished Telegram message processing', {
1028
+ requestId: messageRequestId,
1029
+ chatId: messageContext.chatId,
1030
+ });
1031
+ }
1032
+ }
1033
+ /**
1034
+ * Handles incoming text messages from Telegram
1035
+ */
1036
+ messagingClient.onTextMessage(async (messageContext) => {
1037
+ if (messageContext.chatId !== undefined && messageContext.chatId !== null) {
1038
+ lastIncomingChatId = String(messageContext.chatId);
1039
+ persistCallbackChatId(lastIncomingChatId);
1040
+ }
1041
+ if (isAbortCommand(messageContext.text)) {
1042
+ if (!hasActiveAcpPrompt()) {
1043
+ await messageContext.sendText('ℹ️ No active Gemini action to abort.');
1044
+ return;
1045
+ }
1046
+ manualAbortRequested = true;
1047
+ await messageContext.sendText('⏹️ Abort requested. Stopping current Gemini action...');
1048
+ await cancelActiveAcpPrompt();
1049
+ return;
1050
+ }
1051
+ enqueueMessage(messageContext)
1052
+ .catch(async (error) => {
1053
+ console.error('Error processing message:', error);
1054
+ const errorMessage = getErrorMessage(error);
1055
+ if (errorMessage.toLowerCase().includes('aborted by user')) {
1056
+ await messageContext.sendText('⏹️ Gemini action stopped.');
1057
+ return;
1058
+ }
1059
+ await messageContext.sendText(`❌ Error: ${errorMessage}`);
1060
+ });
1061
+ });
1062
+ // Error handling
1063
+ messagingClient.onError((error, messageContext) => {
1064
+ console.error('Telegram client error:', error);
1065
+ if (messageContext) {
1066
+ messageContext.sendText('⚠️ An error occurred while processing your request.').catch(() => { });
1067
+ }
1068
+ });
1069
+ // Graceful shutdown
1070
+ setupGracefulShutdown();
1071
+ // Launch the bot
1072
+ logInfo('Starting Agent ACP Bridge...');
1073
+ validateGeminiCommandOrExit();
1074
+ ensureBridgeHomeDirectory();
1075
+ ensureMemoryFile();
1076
+ lastIncomingChatId = loadPersistedCallbackChatId();
1077
+ if (lastIncomingChatId) {
1078
+ logInfo('Loaded callback chat binding', { chatId: lastIncomingChatId });
1079
+ }
1080
+ startCallbackServer();
1081
+ scheduleAcpPrewarm('startup');
1082
+ messagingClient.launch()
1083
+ .then(async () => {
1084
+ logInfo('Bot launched successfully', {
1085
+ typingIntervalMs: TYPING_INTERVAL_MS,
1086
+ geminiTimeoutMs: GEMINI_TIMEOUT_MS,
1087
+ geminiNoOutputTimeoutMs: GEMINI_NO_OUTPUT_TIMEOUT_MS,
1088
+ heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
1089
+ acpPrewarmRetryMs: ACP_PREWARM_RETRY_MS,
1090
+ memoryFilePath: MEMORY_FILE_PATH,
1091
+ callbackHost: CALLBACK_HOST,
1092
+ callbackPort: CALLBACK_PORT,
1093
+ mcpSkillsSource: 'local Gemini CLI defaults (no MCP override)',
1094
+ acpMode: `${GEMINI_COMMAND} --experimental-acp`,
1095
+ });
1096
+ scheduleAcpPrewarm('post-launch');
1097
+ if (HEARTBEAT_INTERVAL_MS > 0) {
1098
+ setInterval(() => {
1099
+ logInfo('Heartbeat', {
1100
+ queueLength: messageQueue.length,
1101
+ acpSessionReady: Boolean(acpSessionId),
1102
+ geminiProcessRunning: Boolean(geminiProcess && !geminiProcess.killed),
1103
+ });
1104
+ }, HEARTBEAT_INTERVAL_MS);
1105
+ }
1106
+ })
1107
+ .catch((error) => {
1108
+ if (error?.response?.error_code === 404 && error?.on?.method === 'getMe') {
1109
+ console.error('Failed to launch bot: Telegram token is invalid (getMe returned 404 Not Found).');
1110
+ console.error('Update TELEGRAM_TOKEN in ~/.gemini-bridge/config.json or env and restart.');
1111
+ process.exit(1);
1112
+ }
1113
+ console.error('Failed to launch bot:', error);
1114
+ process.exit(1);
1115
+ });