bloby-bot 0.60.1 → 0.61.0

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.
@@ -518,11 +518,10 @@ export class WhatsAppChannel implements ChannelProvider {
518
518
  }
519
519
  }
520
520
 
521
- // Skip if no text AND no images
522
- if (!rawText && images.length === 0) continue;
523
-
524
- // Use a default text for image-only messages
525
- if (!rawText && images.length > 0) {
521
+ // Skip if no text AND no images; otherwise default text for image-only
522
+ // messages. Collapsing both branches also narrows `rawText` to `string`.
523
+ if (!rawText) {
524
+ if (images.length === 0) continue;
526
525
  rawText = '(image)';
527
526
  }
528
527
 
@@ -60,6 +60,8 @@ const PROVIDERS = [
60
60
 
61
61
  const MODELS: Record<string, { id: string; label: string }[]> = {
62
62
  anthropic: [
63
+ { id: 'claude-opus-4-8[1m]', label: 'Opus 4.8 (1M context)' },
64
+ { id: 'claude-opus-4-8', label: 'Opus 4.8' },
63
65
  { id: 'claude-opus-4-7[1m]', label: 'Opus 4.7 (1M context)' },
64
66
  { id: 'claude-opus-4-7', label: 'Opus 4.7' },
65
67
  { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6 (1M context)' },
@@ -3513,6 +3515,21 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
3513
3515
  {/* ── Auth flow: Anthropic ── */}
3514
3516
  {provider === 'anthropic' && (
3515
3517
  <div className="space-y-2.5">
3518
+ {/* Anthropic third-party usage policy notice — shown to anyone considering Claude. */}
3519
+ <div className="rounded-xl border border-amber-500/20 bg-amber-500/[0.06] px-4 py-3.5">
3520
+ <div className="flex items-center gap-2 mb-2">
3521
+ <TriangleAlert className="h-4 w-4 text-amber-400 shrink-0" />
3522
+ <h3 className="text-[12.5px] font-semibold text-amber-200/90">Anthropic Third-Party App Policy Update</h3>
3523
+ </div>
3524
+ <div className="space-y-2 text-amber-100/70 text-[12px] leading-relaxed">
3525
+ <p>Starting June 15, 2026, Anthropic will provide a separate Third-Party App credit equal to the amount you pay for your subscription.</p>
3526
+ <p>For example, if you have the Max 5x plan at $100/month, you will receive $100 in credits to use with third-party tools like Bloby.</p>
3527
+ <p>Unfortunately, this is only a fraction of the usage Bloby users had before. We don&apos;t control Anthropic&apos;s rules, but we do need to follow them.</p>
3528
+ <p>The best alternative right now is a <span className="font-medium text-amber-100/90">ChatGPT subscription</span>, which also offers $100 and $200 plans with much higher usage limits for Bloby.</p>
3529
+ <p>In the short term, Bloby will be optimized for ChatGPT. In the long term, we are building our own model harness so Bloby has more control, more flexibility, and does not depend too heavily on providers that can change their rules at any moment.</p>
3530
+ </div>
3531
+ </div>
3532
+
3516
3533
  {isConnected && (
3517
3534
  <div className="space-y-2.5">
3518
3535
  <div className="bg-emerald-500/8 border border-emerald-500/15 rounded-lg px-3.5 py-2.5">
@@ -213,6 +213,11 @@ async function buildConversationOptions(
213
213
 
214
214
  return {
215
215
  model,
216
+ // Reasoning effort. 'high' = deep reasoning while staying more token-efficient
217
+ // than the CLI's xhigh default on Opus 4.7/4.8 — meaningful given Anthropic's
218
+ // tighter third-party usage limits. Supported on Opus 4.6+/Sonnet 4.6; silently
219
+ // ignored by models without effort support.
220
+ effort: 'high',
216
221
  cwd: WORKSPACE_DIR,
217
222
  permissionMode: 'bypassPermissions',
218
223
  allowDangerouslySkipPermissions: true,
@@ -648,6 +653,7 @@ export async function startBlobyAgentQuery(
648
653
  prompt: sdkPrompt,
649
654
  options: {
650
655
  model,
656
+ effort: 'high', // see buildConversationOptions — token-efficient deep reasoning
651
657
  cwd: WORKSPACE_DIR,
652
658
  permissionMode: 'bypassPermissions',
653
659
  allowDangerouslySkipPermissions: true,
@@ -762,6 +768,7 @@ export async function runAgentQuery(req: AgentQueryRequest): Promise<AgentQueryR
762
768
  prompt: req.message,
763
769
  options: {
764
770
  cwd: WORKSPACE_DIR,
771
+ effort: 'high', // see buildConversationOptions — token-efficient deep reasoning
765
772
  permissionMode: 'bypassPermissions',
766
773
  allowDangerouslySkipPermissions: true,
767
774
  maxTurns,
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { log } from '../../../shared/logger.js';
14
14
  import { WORKSPACE_DIR } from '../../../shared/paths.js';
15
- import type { SavedFile } from '../file-saver.js';
15
+ import type { SavedFile } from '../../file-saver.js';
16
16
  import { assembleSystemPrompt } from '../../../worker/prompts/prompt-assembler.js';
17
17
  import fs from 'fs';
18
18
  import path from 'path';
@@ -12,7 +12,14 @@ export function safeResolve(cwd: string, requested: string): string {
12
12
  if (!requested || typeof requested !== 'string') {
13
13
  throw new Error('Missing file path');
14
14
  }
15
- const root = fs.realpathSync.native ? fs.realpathSync(cwd) : path.resolve(cwd);
15
+ // Canonicalize cwd (resolves symlinks) so the traversal check below compares real
16
+ // paths. Falls back to a plain resolve when cwd doesn't exist yet (realpath throws).
17
+ let root: string;
18
+ try {
19
+ root = fs.realpathSync.native(cwd);
20
+ } catch {
21
+ root = path.resolve(cwd);
22
+ }
16
23
  const abs = path.isAbsolute(requested)
17
24
  ? path.normalize(requested)
18
25
  : path.normalize(path.join(root, requested));
@@ -551,6 +551,14 @@ export async function startSupervisor() {
551
551
  'POST /api/channels/whatsapp/react',
552
552
  'POST /api/channels/send',
553
553
  'POST /api/channels/alexa/handle',
554
+ 'POST /api/channels/telegram/connect',
555
+ 'GET /api/channels/telegram/poll',
556
+ 'GET /api/channels/telegram/pair-page',
557
+ 'GET /api/channels/telegram/status',
558
+ 'POST /api/channels/telegram/configure',
559
+ 'POST /api/channels/telegram/disconnect',
560
+ 'POST /api/channels/telegram/reconnect',
561
+ 'POST /api/channels/telegram/logout',
554
562
  ];
555
563
  // Method-specific public PREFIXES — onboarding namespaces with sub-paths / params that carry no
556
564
  // private chat data: provider OAuth setup/status (all of /api/auth/*), and handle availability
@@ -677,6 +685,11 @@ export async function startSupervisor() {
677
685
  'POST /api/channels/whatsapp/logout',
678
686
  'POST /api/channels/whatsapp/pairing-code',
679
687
  'POST /api/channels/send',
688
+ // Telegram mutation endpoints that seize/alter the agent — agent-driven over loopback only.
689
+ 'POST /api/channels/telegram/configure',
690
+ 'POST /api/channels/telegram/disconnect',
691
+ 'POST /api/channels/telegram/reconnect',
692
+ 'POST /api/channels/telegram/logout',
680
693
  ]);
681
694
  if (WA_MUTATION_ROUTES.has(`${req.method} ${channelPath}`)) {
682
695
  const remoteIp = req.socket.remoteAddress || '';
@@ -1348,6 +1361,306 @@ mint();
1348
1361
  return;
1349
1362
  }
1350
1363
 
1364
+ // ── Telegram (relay provisions a per-user bot via "Managed Bots"; the Bloby then
1365
+ // long-polls Telegram DIRECTLY — the relay is only in the pairing path) ──
1366
+
1367
+ // POST /api/channels/telegram/connect — ask the relay to start provisioning a child bot.
1368
+ // Returns a t.me/newbot deep link the user taps + a sessionId to poll. No token yet.
1369
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/connect') {
1370
+ (async () => {
1371
+ try {
1372
+ const cfg = loadConfig();
1373
+ if (!cfg.relay?.token) {
1374
+ res.writeHead(400);
1375
+ res.end(JSON.stringify({ ok: false, error: 'no-relay-token' }));
1376
+ return;
1377
+ }
1378
+ const resp = await fetch('https://api.bloby.bot/api/telegram/provision', {
1379
+ method: 'POST',
1380
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.relay.token}` },
1381
+ body: JSON.stringify({ name: cfg.username || 'Bloby' }),
1382
+ });
1383
+ const data = await resp.json() as { sessionId?: string; deepLink?: string; botUsername?: string; expiresAt?: string; error?: string };
1384
+ if (!resp.ok || !data.sessionId || !data.deepLink) {
1385
+ res.writeHead(resp.status || 500);
1386
+ res.end(JSON.stringify({ ok: false, error: data.error || 'provision-failed' }));
1387
+ return;
1388
+ }
1389
+ res.writeHead(200);
1390
+ res.end(JSON.stringify({ ok: true, sessionId: data.sessionId, deepLink: data.deepLink, botUsername: data.botUsername, expiresAt: data.expiresAt }));
1391
+ } catch (err: any) {
1392
+ log.warn(`[telegram/connect] ${err.message}`);
1393
+ res.writeHead(500);
1394
+ res.end(JSON.stringify({ ok: false, error: err.message }));
1395
+ }
1396
+ })();
1397
+ return;
1398
+ }
1399
+
1400
+ // GET /api/channels/telegram/poll?sessionId=… — poll the relay for the provisioned token.
1401
+ // When ready, persist the child token locally and start the long-poll provider.
1402
+ if (req.method === 'GET' && channelPath === '/api/channels/telegram/poll') {
1403
+ (async () => {
1404
+ try {
1405
+ const sessionId = new URL(req.url || '', 'http://localhost').searchParams.get('sessionId') || '';
1406
+ if (!sessionId) {
1407
+ res.writeHead(400);
1408
+ res.end(JSON.stringify({ ok: false, error: 'missing-sessionId' }));
1409
+ return;
1410
+ }
1411
+ const cfg = loadConfig();
1412
+ if (!cfg.relay?.token) {
1413
+ res.writeHead(400);
1414
+ res.end(JSON.stringify({ ok: false, error: 'no-relay-token' }));
1415
+ return;
1416
+ }
1417
+ const resp = await fetch(`https://api.bloby.bot/api/telegram/provision/${encodeURIComponent(sessionId)}`, {
1418
+ headers: { Authorization: `Bearer ${cfg.relay.token}` },
1419
+ });
1420
+ const data = await resp.json() as { ready?: boolean; token?: string; botUsername?: string; ownerUserId?: string | number; error?: string };
1421
+ if (!resp.ok) {
1422
+ res.writeHead(resp.status);
1423
+ res.end(JSON.stringify({ ok: false, error: data.error || 'poll-failed' }));
1424
+ return;
1425
+ }
1426
+ if (!data.ready || !data.token) {
1427
+ res.writeHead(200);
1428
+ res.end(JSON.stringify({ ok: true, ready: false }));
1429
+ return;
1430
+ }
1431
+
1432
+ // Provisioned — persist the child bot token and bring the channel up.
1433
+ const fresh = loadConfig();
1434
+ if (!fresh.channels) fresh.channels = {};
1435
+ fresh.channels.telegram = {
1436
+ ...(fresh.channels.telegram || {}),
1437
+ enabled: true,
1438
+ mode: fresh.channels.telegram?.mode || 'channel',
1439
+ botToken: data.token,
1440
+ botUsername: data.botUsername || fresh.channels.telegram?.botUsername,
1441
+ ownerUserId: data.ownerUserId != null ? String(data.ownerUserId) : fresh.channels.telegram?.ownerUserId,
1442
+ };
1443
+ saveConfig(fresh);
1444
+ // Re-pairing replaces the token — tear down any existing provider so init() reconnects
1445
+ // with the NEW token (init() is a no-op when a provider is already in the map).
1446
+ await channelManager.disconnectChannel('telegram').catch(() => {});
1447
+ await channelManager.init().catch(() => {});
1448
+
1449
+ res.writeHead(200);
1450
+ res.end(JSON.stringify({ ok: true, ready: true, botUsername: data.botUsername }));
1451
+ } catch (err: any) {
1452
+ log.warn(`[telegram/poll] ${err.message}`);
1453
+ res.writeHead(500);
1454
+ res.end(JSON.stringify({ ok: false, error: err.message }));
1455
+ }
1456
+ })();
1457
+ return;
1458
+ }
1459
+
1460
+ // POST /api/channels/telegram/configure — set mode + admins + skill (loopback-only).
1461
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/configure') {
1462
+ let body = '';
1463
+ req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
1464
+ req.on('end', () => {
1465
+ try {
1466
+ const data = JSON.parse(body || '{}');
1467
+ const cfg = loadConfig();
1468
+ if (!cfg.channels) cfg.channels = {};
1469
+ if (!cfg.channels.telegram) cfg.channels.telegram = { enabled: false, mode: 'channel' };
1470
+ if (data.mode) cfg.channels.telegram.mode = data.mode;
1471
+ if (data.admins !== undefined) cfg.channels.telegram.admins = data.admins;
1472
+ if (data.skill !== undefined) cfg.channels.telegram.skill = data.skill;
1473
+ if (data.allowGroups !== undefined) cfg.channels.telegram.allowGroups = !!data.allowGroups;
1474
+ if (data.allowOthersToTrigger !== undefined) cfg.channels.telegram.allowOthersToTrigger = !!data.allowOthersToTrigger;
1475
+ saveConfig(cfg);
1476
+ res.writeHead(200);
1477
+ res.end(JSON.stringify({ ok: true, config: { ...cfg.channels.telegram, botToken: cfg.channels.telegram.botToken ? '***' : undefined } }));
1478
+ } catch (err: any) {
1479
+ res.writeHead(400);
1480
+ res.end(JSON.stringify({ error: err.message }));
1481
+ }
1482
+ });
1483
+ return;
1484
+ }
1485
+
1486
+ // POST /api/channels/telegram/disconnect — stop polling, KEEP the token (loopback-only).
1487
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/disconnect') {
1488
+ (async () => {
1489
+ try {
1490
+ await channelManager.disconnectChannel('telegram');
1491
+ res.writeHead(200);
1492
+ res.end(JSON.stringify({ ok: true }));
1493
+ } catch (err: any) {
1494
+ res.writeHead(500);
1495
+ res.end(JSON.stringify({ ok: false, error: err.message }));
1496
+ }
1497
+ })();
1498
+ return;
1499
+ }
1500
+
1501
+ // POST /api/channels/telegram/logout — stop polling + forget the token (loopback-only).
1502
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/logout') {
1503
+ (async () => {
1504
+ try {
1505
+ await channelManager.disconnectChannel('telegram');
1506
+ const cfg = loadConfig();
1507
+ if (cfg.channels?.telegram) {
1508
+ delete cfg.channels.telegram.botToken;
1509
+ cfg.channels.telegram.enabled = false;
1510
+ saveConfig(cfg);
1511
+ }
1512
+ res.writeHead(200);
1513
+ res.end(JSON.stringify({ ok: true }));
1514
+ } catch (err: any) {
1515
+ res.writeHead(500);
1516
+ res.end(JSON.stringify({ ok: false, error: err.message }));
1517
+ }
1518
+ })();
1519
+ return;
1520
+ }
1521
+
1522
+ // POST /api/channels/telegram/reconnect — resume polling an already-provisioned bot
1523
+ // after a disconnect, without re-pairing (loopback-only).
1524
+ if (req.method === 'POST' && channelPath === '/api/channels/telegram/reconnect') {
1525
+ (async () => {
1526
+ try {
1527
+ const cfg = loadConfig();
1528
+ if (!cfg.channels?.telegram?.botToken) {
1529
+ res.writeHead(400);
1530
+ res.end(JSON.stringify({ ok: false, error: 'no-bot-token' }));
1531
+ return;
1532
+ }
1533
+ if (cfg.channels.telegram.enabled !== true) {
1534
+ cfg.channels.telegram.enabled = true;
1535
+ saveConfig(cfg);
1536
+ }
1537
+ await channelManager.init().catch(() => {});
1538
+ res.writeHead(200);
1539
+ res.end(JSON.stringify({ ok: true }));
1540
+ } catch (err: any) {
1541
+ res.writeHead(500);
1542
+ res.end(JSON.stringify({ ok: false, error: err.message }));
1543
+ }
1544
+ })();
1545
+ return;
1546
+ }
1547
+
1548
+ // GET /api/channels/telegram/status — current telegram channel status
1549
+ if (req.method === 'GET' && channelPath === '/api/channels/telegram/status') {
1550
+ const status = channelManager.getStatus('telegram');
1551
+ res.writeHead(200);
1552
+ res.end(JSON.stringify(status || { channel: 'telegram', connected: false }));
1553
+ return;
1554
+ }
1555
+
1556
+ // GET /api/channels/telegram/pair-page — inline page: tap to create the bot, then auto-detects link.
1557
+ if (req.method === 'GET' && channelPath === '/api/channels/telegram/pair-page') {
1558
+ res.setHeader('Content-Type', 'text/html');
1559
+ const tgStatus = channelManager.getStatus('telegram');
1560
+ const alreadyLinked = !!(tgStatus?.info as any)?.linked;
1561
+ const linkedUsername = (tgStatus?.info as any)?.botUsername || '';
1562
+ const confettiHTML = Array.from({ length: 30 }, (_, i) => {
1563
+ const colors = ['#0166FF', '#009AFE', '#4AEEFF', '#4ade80', '#facc15', '#818cf8'];
1564
+ const color = colors[Math.floor(Math.random() * colors.length)];
1565
+ const left = Math.random() * 100;
1566
+ const delay = i * 0.04;
1567
+ const drift = (Math.random() - 0.5) * 120;
1568
+ const duration = 1.8 + Math.random() * 0.8;
1569
+ return `<div class="confetti-dot" style="left:${left}%;background:${color};animation-delay:${delay}s;animation-duration:${duration}s;--drift:${drift}px"></div>`;
1570
+ }).join('');
1571
+ res.writeHead(200);
1572
+ res.end(`<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Connect Telegram</title>
1573
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1574
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1575
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
1576
+ <style>
1577
+ *{margin:0;padding:0;box-sizing:border-box}
1578
+ body{background:#212121;color:#f5f5f5;font-family:'Inter',system-ui,-apple-system,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100dvh;margin:0;overflow-x:hidden}
1579
+ .container{display:flex;flex-direction:column;align-items:center;max-width:380px;width:100%;padding:20px}
1580
+ .card{background:#2a2a2a;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:28px 24px;width:100%;box-shadow:0 0 0 1px rgba(0,105,254,0.1),0 0 20px -5px rgba(0,105,254,0.15);animation:fade-up .5s ease-out both;text-align:center}
1581
+ .header{display:flex;flex-direction:column;align-items:center;gap:6px;margin-bottom:18px}
1582
+ .badge{display:inline-flex;align-items:center;gap:6px;background:#1a1a1a;border:1px solid rgba(255,255,255,0.08);border-radius:9999px;padding:4px 10px;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:0.6px}
1583
+ .badge::before{content:'';width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#0166FF,#009AFE);box-shadow:0 0 8px rgba(74,238,255,0.5)}
1584
+ .title{font-family:'Space Grotesk',sans-serif;font-size:22px;font-weight:700;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-top:6px}
1585
+ .sub{font-size:13px;color:#999;line-height:1.6;margin-top:6px}
1586
+ .steps{text-align:left;margin:18px 0 6px;animation:fade-up .5s ease-out .2s both}
1587
+ .step{display:flex;gap:12px;font-size:13px;color:#bbb;line-height:1.5;padding:6px 0}
1588
+ .step-num{flex-shrink:0;width:22px;height:22px;border-radius:50%;background:#1a1a1a;border:1px solid rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:#888}
1589
+ .step b{color:#f5f5f5;font-weight:600}
1590
+ .btn{display:inline-flex;align-items:center;justify-content:center;width:100%;border:none;border-radius:10px;padding:13px 16px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s;text-decoration:none;margin-top:8px}
1591
+ .btn-primary{background:linear-gradient(135deg,#0166FF,#009AFE);color:#fff}
1592
+ .btn-primary:hover{opacity:.92}
1593
+ .btn-disabled{opacity:.5;pointer-events:none}
1594
+ .status{font-size:12px;color:#666;margin-top:14px;display:inline-flex;align-items:center;gap:6px;min-height:1.2em}
1595
+ .status .dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:pulse 1.6s ease-in-out infinite;opacity:.6}
1596
+ .err{color:#FB4072;font-size:13px;margin-top:12px;min-height:1.2em}
1597
+ .confetti-wrap{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:0}
1598
+ .confetti-dot{position:absolute;width:8px;height:8px;border-radius:50%;top:-10px;animation:confetti-fall 2s ease-out forwards}
1599
+ @keyframes confetti-fall{0%{opacity:1;transform:translateY(0) translateX(0) rotate(0) scale(1)}100%{opacity:0;transform:translateY(100vh) translateX(var(--drift)) rotate(360deg) scale(.5)}}
1600
+ .video-wrap{margin-bottom:8px;animation:pop-in .5s cubic-bezier(.34,1.56,.64,1) forwards}
1601
+ .video-wrap video{width:200px;object-fit:contain;pointer-events:none}
1602
+ @keyframes pop-in{0%{transform:scale(0);opacity:0}100%{transform:scale(1);opacity:1}}
1603
+ .text-wrap{text-align:center;animation:fade-up .5s ease-out .3s both}
1604
+ .success-sub{font-size:14px;color:#999;line-height:1.5;margin-top:6px}
1605
+ @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}
1606
+ @keyframes fade-up{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}
1607
+ </style></head><body>
1608
+ <div class="container" id="root">
1609
+ ${alreadyLinked
1610
+ ? `<div class="confetti-wrap">${confettiHTML}</div>
1611
+ <div class="video-wrap"><video autoplay muted playsinline><source src="/bloby_happy_reappearing.mov" type='video/mp4; codecs="hvc1"'><source src="/bloby_happy_reappearing.webm" type="video/webm"></video></div>
1612
+ <div class="text-wrap">
1613
+ <div class="title">Connected!</div>
1614
+ <p class="success-sub">Telegram is linked${linkedUsername ? ` to <b style="color:#f5f5f5">@${linkedUsername}</b>` : ''}. Open Telegram and message your bot to start chatting.</p>
1615
+ <p class="success-sub" style="margin-top:14px;font-size:12px;color:#666">You can close this page.</p>
1616
+ </div>`
1617
+ : `<div class="card" id="pendingCard">
1618
+ <div class="header">
1619
+ <span class="badge">Telegram</span>
1620
+ <div class="title">Connect Telegram</div>
1621
+ <p class="sub">Create your own private Telegram bot in two taps — no token, no copy-paste.</p>
1622
+ </div>
1623
+ <div class="steps">
1624
+ <div class="step"><div class="step-num">1</div><div>Tap <b>Create my bot</b> below — Telegram opens with a confirm screen</div></div>
1625
+ <div class="step"><div class="step-num">2</div><div>Tap <b>Create Bot</b> inside Telegram to confirm</div></div>
1626
+ <div class="step"><div class="step-num">3</div><div>Come back here — it links automatically</div></div>
1627
+ </div>
1628
+ <a class="btn btn-primary btn-disabled" id="cta" href="#" target="_blank" rel="noopener">Preparing…</a>
1629
+ <div class="status" id="st"><span class="dot"></span><span id="stText">Getting your link ready…</span></div>
1630
+ <p class="err" id="err"></p>
1631
+ </div>`}
1632
+ </div>
1633
+ <script>
1634
+ ${alreadyLinked ? `` : `
1635
+ let sessionId=null,poller=null;
1636
+ async function start(){
1637
+ try{
1638
+ const r=await fetch('/api/channels/telegram/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
1639
+ const d=await r.json();
1640
+ if(!d.ok){document.getElementById('err').textContent=d.error||'Could not start. Try again.';return}
1641
+ sessionId=d.sessionId;
1642
+ const cta=document.getElementById('cta');
1643
+ cta.href=d.deepLink;cta.classList.remove('btn-disabled');cta.textContent='Create my bot';
1644
+ document.getElementById('stText').textContent='Waiting for you to confirm in Telegram…';
1645
+ if(poller)clearInterval(poller);
1646
+ poller=setInterval(poll,2500);
1647
+ }catch(e){document.getElementById('err').textContent=e.message}
1648
+ }
1649
+ async function poll(){
1650
+ if(!sessionId)return;
1651
+ try{
1652
+ const r=await fetch('/api/channels/telegram/poll?sessionId='+encodeURIComponent(sessionId));
1653
+ const d=await r.json();
1654
+ if(d.ok&&d.ready){clearInterval(poller);location.reload()}
1655
+ }catch(e){/* keep polling */}
1656
+ }
1657
+ start();
1658
+ `}
1659
+ </script>
1660
+ </body></html>`);
1661
+ return;
1662
+ }
1663
+
1351
1664
  // Fallback for unknown channel routes
1352
1665
  res.writeHead(404);
1353
1666
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -60,9 +60,9 @@
60
60
  '<source src="/what-happened.webm" type="video/webm"><source src="/what-happened.mp4" type="video/mp4">' +
61
61
  '</video>' +
62
62
  '</div>' +
63
- '<h1 style="font-size:1.5rem;font-weight:700;margin:0 0 .6rem;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">A screen in your app has an error</h1>' +
64
- '<p style="color:#e4e4e7;font-size:1rem;line-height:1.6;margin:0 0 .4rem">Your latest change didn\'t compile.</p>' +
65
- '<p style="color:#a1a1aa;font-size:.93rem;line-height:1.6;margin:0 0 1.2rem">Ask your agent to fix it the chat is in the bottom corner. Copy the error so it can debug faster.</p>' +
63
+ '<h1 style="font-size:1.5rem;font-weight:700;margin:0 0 .6rem;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">Workspace error</h1>' +
64
+ '<p style="color:#e4e4e7;font-size:1rem;line-height:1.6;margin:0 0 .4rem">Your latest frontend change didnt compile. If your Bloby is currently building or editing something, this is usually normal while the work is still in progress.</p>' +
65
+ '<p style="color:#a1a1aa;font-size:.93rem;line-height:1.6;margin:0 0 1.2rem">When your agent finishes, this may fix itself. If it doesn’t, ask your Bloby to fix it and copy the error so it can debug faster.</p>' +
66
66
  '<div><button id="__bloby_fe_copy" style="font:inherit;cursor:pointer;border:none;border-radius:10px;padding:.65rem 1.2rem;font-size:.9rem;font-weight:600;background:linear-gradient(135deg,#0166FF,#0069FE);color:#fff">Copy error for your agent</button> ' +
67
67
  '<button id="__bloby_fe_dismiss" style="font:inherit;cursor:pointer;border-radius:10px;padding:.65rem 1.1rem;font-size:.9rem;font-weight:600;border:1px solid #27272a;background:#18181b;color:#e4e4e7">Dismiss</button></div>' +
68
68
  '</div>';
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "telegram",
3
+ "version": "1.0.0",
4
+ "description": "Telegram channel via a relay-provisioned private bot. Two-tap pairing, direct long-poll, voice/photo, channel/business/assistant modes.",
5
+ "skills": "./"
6
+ }