@openlife/cli 1.7.12 → 1.7.14

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 CHANGED
@@ -2825,6 +2825,91 @@ profileCmd.command('import <file>').description('Import a profile from a JSON fi
2825
2825
  process.exitCode = 1;
2826
2826
  }
2827
2827
  });
2828
+ // `openlife telegram` — manage Telegram delivery mode (polling vs webhook)
2829
+ const telegramCmd = program
2830
+ .command('telegram')
2831
+ .description('Gerencia o modo de entrega do Telegram (polling vs webhook)');
2832
+ telegramCmd
2833
+ .command('status')
2834
+ .description('Mostra modo atual + webhook info')
2835
+ .action(async () => {
2836
+ const token = (process.env.TELEGRAM_BOT_TOKEN || '').trim();
2837
+ if (!token) {
2838
+ console.log(JSON.stringify({ ok: false, error: 'TELEGRAM_BOT_TOKEN ausente' }, null, 2));
2839
+ return;
2840
+ }
2841
+ const { execFileSync } = require('child_process');
2842
+ try {
2843
+ const raw = String(execFileSync('curl', ['-sS', '--max-time', '8', `https://api.telegram.org/bot${token}/getWebhookInfo`], { encoding: 'utf-8' })).trim();
2844
+ const j = JSON.parse(raw);
2845
+ const info = (j && j.result) || {};
2846
+ const mode = info.url ? 'webhook' : 'polling';
2847
+ const onRailway = !!(process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PUBLIC_DOMAIN || process.env.RAILWAY_STATIC_URL);
2848
+ const configured = String(process.env.OPENLIFE_TELEGRAM_MODE || 'auto').toLowerCase();
2849
+ console.log(JSON.stringify({
2850
+ ok: true,
2851
+ mode_telegram_thinks: mode,
2852
+ mode_configured: configured,
2853
+ railway_detected: onRailway,
2854
+ webhook_url: info.url || null,
2855
+ pending_updates: info.pending_update_count || 0,
2856
+ last_error: info.last_error_message || null,
2857
+ }, null, 2));
2858
+ }
2859
+ catch (e) {
2860
+ console.log(JSON.stringify({ ok: false, error: 'getWebhookInfo_failed', detail: e.message }, null, 2));
2861
+ process.exitCode = 1;
2862
+ }
2863
+ });
2864
+ telegramCmd
2865
+ .command('webhook-set <domain>')
2866
+ .description('Configura webhook (uso típico Railway). Ex: openlife telegram webhook-set https://app.up.railway.app')
2867
+ .option('--path <path>', 'Caminho do webhook (default: /api/v1/telegram/webhook)', '/api/v1/telegram/webhook')
2868
+ .action(async (domain, opts) => {
2869
+ const token = (process.env.TELEGRAM_BOT_TOKEN || '').trim();
2870
+ if (!token) {
2871
+ console.error('TELEGRAM_BOT_TOKEN ausente em .env');
2872
+ process.exitCode = 1;
2873
+ return;
2874
+ }
2875
+ const fullDomain = domain.startsWith('http') ? domain : `https://${domain}`;
2876
+ const url = `${fullDomain.replace(/\/$/, '')}${opts.path}`;
2877
+ const { execFileSync } = require('child_process');
2878
+ try {
2879
+ const raw = String(execFileSync('curl', ['-sS', '--max-time', '8', '-X', 'POST', '-d', `url=${url}`, `https://api.telegram.org/bot${token}/setWebhook`], { encoding: 'utf-8' })).trim();
2880
+ const j = JSON.parse(raw);
2881
+ console.log(JSON.stringify({ ok: !!j.ok, url, telegram_response: j }, null, 2));
2882
+ if (!j.ok)
2883
+ process.exitCode = 1;
2884
+ }
2885
+ catch (e) {
2886
+ console.error('webhook-set falhou:', e.message);
2887
+ process.exitCode = 1;
2888
+ }
2889
+ });
2890
+ telegramCmd
2891
+ .command('webhook-clear')
2892
+ .description('Remove webhook (volta para polling)')
2893
+ .action(async () => {
2894
+ const token = (process.env.TELEGRAM_BOT_TOKEN || '').trim();
2895
+ if (!token) {
2896
+ console.error('TELEGRAM_BOT_TOKEN ausente');
2897
+ process.exitCode = 1;
2898
+ return;
2899
+ }
2900
+ const { execFileSync } = require('child_process');
2901
+ try {
2902
+ const raw = String(execFileSync('curl', ['-sS', '--max-time', '8', '-X', 'POST', `https://api.telegram.org/bot${token}/deleteWebhook`], { encoding: 'utf-8' })).trim();
2903
+ const j = JSON.parse(raw);
2904
+ console.log(JSON.stringify({ ok: !!j.ok, telegram_response: j }, null, 2));
2905
+ if (!j.ok)
2906
+ process.exitCode = 1;
2907
+ }
2908
+ catch (e) {
2909
+ console.error('webhook-clear falhou:', e.message);
2910
+ process.exitCode = 1;
2911
+ }
2912
+ });
2828
2913
  // `openlife config` — interactive configuration TUI (provider, keys, telegram, voice, daemon)
2829
2914
  program
2830
2915
  .command('config')
@@ -2838,8 +2923,19 @@ program
2838
2923
  program
2839
2924
  .command('chat')
2840
2925
  .description('Inicia o chat interativo no terminal (Matrix-themed REPL)')
2841
- .action(async () => {
2842
- const { runChat } = require('./cli/ChatTui');
2926
+ .option('--test', 'executa smoke não-interativo do chat e sai')
2927
+ .action(async (opts) => {
2928
+ const { runChat, runChatSmoke } = require('./cli/ChatTui');
2929
+ if (opts.test) {
2930
+ const result = runChatSmoke({ cwd: process.cwd() });
2931
+ console.log(`${result.marker}: ${result.detail}`);
2932
+ console.log(`sessionId=${result.sessionId}`);
2933
+ console.log(`model=${result.model}`);
2934
+ console.log(`cwd=${result.cwd}`);
2935
+ if (!result.ok)
2936
+ process.exitCode = 1;
2937
+ return;
2938
+ }
2843
2939
  await runChat();
2844
2940
  });
2845
2941
  // Bare invocation (no subcommand + interactive TTY) → launch chat TUI.
@@ -288,7 +288,46 @@ class Brain {
288
288
  const commandArgs = process.platform === 'win32'
289
289
  ? args
290
290
  : ['-k', '2s', `${timeoutSeconds}s`, 'codex', ...args];
291
- const { stdout } = await execFile(command, commandArgs, { maxBuffer: 1024 * 1024 * 10, timeout: timeoutMs + 3000, killSignal: 'SIGKILL' });
291
+ // Codex CLI reads stdin when invoked without a TTY. The promisified
292
+ // execFile doesn't expose stdio, so it keeps the child's stdin
293
+ // attached to our (open) pipe — codex then waits forever for input
294
+ // and our outer timeout fires. Use spawn with `stdio: ['ignore', ...]`
295
+ // to close stdin explicitly. Fixes ~30s phantom timeouts when the
296
+ // daemon (non-TTY) calls codex.
297
+ const stdout = await new Promise((resolve, reject) => {
298
+ const child = child_process.spawn(command, commandArgs, {
299
+ stdio: ['ignore', 'pipe', 'pipe'],
300
+ });
301
+ let stdoutBuf = '';
302
+ let stderrBuf = '';
303
+ let killed = false;
304
+ const watchdog = setTimeout(() => {
305
+ killed = true;
306
+ try {
307
+ child.kill('SIGKILL');
308
+ }
309
+ catch { /* ignore */ }
310
+ }, timeoutMs + 3000);
311
+ child.stdout.on('data', (d) => { stdoutBuf += String(d); });
312
+ child.stderr.on('data', (d) => { stderrBuf += String(d); });
313
+ child.on('error', (err) => { clearTimeout(watchdog); reject(err); });
314
+ child.on('close', (code, signal) => {
315
+ clearTimeout(watchdog);
316
+ if (killed || code === 124 || (signal && killed)) {
317
+ const e = new Error('ETIMEDOUT');
318
+ e.code = 'ETIMEDOUT';
319
+ reject(e);
320
+ return;
321
+ }
322
+ if (code !== 0) {
323
+ const e = new Error(`codex exited with code ${code}: ${stderrBuf.slice(-300)}`);
324
+ e.code = String(code);
325
+ reject(e);
326
+ return;
327
+ }
328
+ resolve(stdoutBuf);
329
+ });
330
+ });
292
331
  const lastMessage = fs.existsSync(outputFile) ? fs.readFileSync(outputFile, 'utf-8').trim() : '';
293
332
  try {
294
333
  fs.unlinkSync(outputFile);
@@ -344,16 +344,56 @@ class Gateway {
344
344
  await ctx.reply("Erro no módulo de visão ao baixar a imagem.");
345
345
  }
346
346
  });
347
- this.bot.launch().catch((err) => {
348
- const msg = err instanceof Error ? err.message : String(err);
349
- if (msg.includes('409') || msg.includes('terminated by other getUpdates request')) {
350
- console.error('[GATEWAY] Telegram 409 Conflict: outra instância está conectada com este bot. Mantendo processo vivo para healthcheck; Telegram polling fica desativado nesta instância.');
351
- return;
347
+ // Decide Telegram delivery mode:
348
+ // - OPENLIFE_TELEGRAM_MODE=polling | webhook | auto (default: auto)
349
+ // - "auto" → webhook if a Railway env var is present, else polling.
350
+ // This is the simplest UX for the "Railway + local" topology: Railway
351
+ // deploys boot in webhook mode (no polling conflict with local), local
352
+ // dev boots in polling mode (no webhook URL needed). User changes
353
+ // nothing.
354
+ const mode = String(process.env.OPENLIFE_TELEGRAM_MODE || 'auto').toLowerCase();
355
+ const onRailway = !!(process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PUBLIC_DOMAIN || process.env.RAILWAY_STATIC_URL);
356
+ const useWebhook = mode === 'webhook' || (mode === 'auto' && onRailway);
357
+ if (useWebhook) {
358
+ const webhookPath = process.env.OPENLIFE_WEBHOOK_PATH || '/api/v1/telegram/webhook';
359
+ // Register webhook handler on the existing Express app — no separate
360
+ // HTTP server, no port conflict. Telegraf's webhookCallback returns
361
+ // an Express-compatible middleware that parses updates.
362
+ this.app.use(webhookPath, this.bot.webhookCallback(webhookPath));
363
+ const rawDomain = process.env.OPENLIFE_WEBHOOK_DOMAIN
364
+ || process.env.RAILWAY_PUBLIC_DOMAIN
365
+ || process.env.RAILWAY_STATIC_URL
366
+ || '';
367
+ if (rawDomain) {
368
+ const domain = rawDomain.startsWith('http') ? rawDomain : `https://${rawDomain}`;
369
+ const fullUrl = `${domain.replace(/\/$/, '')}${webhookPath}`;
370
+ this.bot.telegram.setWebhook(fullUrl).then(() => {
371
+ console.log(`[GATEWAY] Telegram em modo WEBHOOK @ ${fullUrl}`);
372
+ }).catch((err) => {
373
+ console.error('[GATEWAY] setWebhook falhou:', err instanceof Error ? err.message : String(err));
374
+ });
352
375
  }
353
- console.error('[GATEWAY] Falha ao iniciar Telegram bot:', err);
354
- process.exit(1);
355
- });
356
- console.log("[GATEWAY] Telegram Bot escutando com suporte Multimodal (Voz/Visão).");
376
+ else {
377
+ console.warn('[GATEWAY] Webhook mode ativado mas OPENLIFE_WEBHOOK_DOMAIN (ou RAILWAY_PUBLIC_DOMAIN) não está definido. Telegram não saberá onde entregar mensagens. Defina o domínio público antes de subir.');
378
+ }
379
+ console.log("[GATEWAY] Telegram Bot pronto (modo webhook sem polling).");
380
+ }
381
+ else {
382
+ // Default polling mode (local dev, single-instance bots).
383
+ // Clear any previously-registered webhook before polling, otherwise
384
+ // Telegram rejects getUpdates with 409. Best-effort, non-fatal.
385
+ this.bot.telegram.deleteWebhook({ drop_pending_updates: false }).catch(() => { });
386
+ this.bot.launch().catch((err) => {
387
+ const msg = err instanceof Error ? err.message : String(err);
388
+ if (msg.includes('409') || msg.includes('terminated by other getUpdates request')) {
389
+ console.error('[GATEWAY] Telegram 409 Conflict: outra instância está conectada com este bot. Mantendo processo vivo para healthcheck; Telegram polling fica desativado nesta instância.');
390
+ return;
391
+ }
392
+ console.error('[GATEWAY] Falha ao iniciar Telegram bot:', err);
393
+ process.exit(1);
394
+ });
395
+ console.log("[GATEWAY] Telegram Bot escutando (modo polling) com suporte Multimodal (Voz/Visão).");
396
+ }
357
397
  }
358
398
  async shutdown(reason = 'manual') {
359
399
  if (this.isShuttingDown)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlife/cli",
3
- "version": "1.7.12",
3
+ "version": "1.7.14",
4
4
  "description": "OPEN-LIFE Córtex Orquestrador Dual-Core",
5
5
  "main": "dist/index.js",
6
6
  "files": [