bloby-bot 0.60.0 → 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.
- package/dist-bloby/assets/{bloby-8GjzRxjC.js → bloby-DO7g-v11.js} +4 -4
- package/dist-bloby/assets/globals-CF0bs396.css +2 -0
- package/dist-bloby/assets/{globals-D-b6XZqk.js → globals-CwR3dDCz.js} +2 -2
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-DrKKm93B.js → highlighted-body-OFNGDK62-C2Wmb17B.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-CILe07ZG.js +1 -0
- package/dist-bloby/assets/{onboard-DJNuzfZA.js → onboard-DcGLkITd.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +4 -3
- package/shared/config.ts +25 -0
- package/supervisor/channels/manager.ts +112 -12
- package/supervisor/channels/telegram.ts +361 -0
- package/supervisor/channels/types.ts +5 -1
- package/supervisor/channels/whatsapp.ts +4 -5
- package/supervisor/chat/OnboardWizard.tsx +163 -110
- package/supervisor/harnesses/claude.ts +7 -0
- package/supervisor/harnesses/pi/index.ts +1 -1
- package/supervisor/harnesses/pi/tools/path-safety.ts +8 -1
- package/supervisor/index.ts +334 -7
- package/supervisor/workspace-guard.js +3 -3
- package/worker/prompts/bloby-system-prompt-codex.txt +2 -2
- package/worker/prompts/bloby-system-prompt-pi.txt +2 -2
- package/worker/prompts/bloby-system-prompt.txt +2 -2
- package/workspace/skills/telegram/.claude-plugin/plugin.json +6 -0
- package/workspace/skills/telegram/SKILL.md +230 -0
- package/workspace/skills/telegram/skill.json +15 -0
- package/dist-bloby/assets/globals-eJ7lScsq.css +0 -2
- package/dist-bloby/assets/mermaid-GHXKKRXX-CxqocSKs.js +0 -1
package/supervisor/index.ts
CHANGED
|
@@ -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' }));
|
|
@@ -1447,8 +1760,8 @@ mint();
|
|
|
1447
1760
|
req.on('end', () => {
|
|
1448
1761
|
if (tooLarge) return;
|
|
1449
1762
|
try {
|
|
1450
|
-
const { vars } = JSON.parse(body) as { vars
|
|
1451
|
-
if (!vars || typeof vars !== 'object') {
|
|
1763
|
+
const { vars, remove } = JSON.parse(body) as { vars?: Record<string, string>; remove?: string[] };
|
|
1764
|
+
if ((!vars || typeof vars !== 'object') && !Array.isArray(remove)) {
|
|
1452
1765
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1453
1766
|
res.end(JSON.stringify({ error: 'Missing vars object' }));
|
|
1454
1767
|
return;
|
|
@@ -1460,7 +1773,20 @@ mint();
|
|
|
1460
1773
|
lines = fs.readFileSync(envPath, 'utf-8').split('\n');
|
|
1461
1774
|
}
|
|
1462
1775
|
|
|
1463
|
-
|
|
1776
|
+
// Delete requested keys first (so removing then re-adding the same key in one request
|
|
1777
|
+
// behaves predictably). A key line is `KEY=...` / `KEY =...`.
|
|
1778
|
+
if (Array.isArray(remove)) {
|
|
1779
|
+
for (const rawKey of remove) {
|
|
1780
|
+
const key = String(rawKey).trim();
|
|
1781
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
1782
|
+
lines = lines.filter((l) => {
|
|
1783
|
+
const t = l.trim();
|
|
1784
|
+
return !(t.startsWith(`${key}=`) || t.startsWith(`${key} =`));
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
for (const [rawKey, rawValue] of Object.entries(vars || {})) {
|
|
1464
1790
|
const key = rawKey.trim();
|
|
1465
1791
|
// Validate the key as a real env var name; reject anything that could inject extra
|
|
1466
1792
|
// lines or break .env parsing.
|
|
@@ -1552,7 +1878,6 @@ mint();
|
|
|
1552
1878
|
id: c.id,
|
|
1553
1879
|
schedule: c.schedule,
|
|
1554
1880
|
task: typeof c.task === 'string' ? c.task : '',
|
|
1555
|
-
enabled: !!c.enabled, // mirror the scheduler's `!cron.enabled` skip (undefined → disabled)
|
|
1556
1881
|
oneShot: !!c.oneShot,
|
|
1557
1882
|
paused: c.paused === true,
|
|
1558
1883
|
hasTaskFile: CRON_ID_RE.test(c.id || '') && fs.existsSync(path.join(WORKSPACE_DIR, 'tasks', `${c.id}.md`)),
|
|
@@ -2166,7 +2491,9 @@ mint();
|
|
|
2166
2491
|
(!req.headers['sec-fetch-dest'] && String(req.headers['accept'] || '').includes('text/html'))
|
|
2167
2492
|
);
|
|
2168
2493
|
if (wantsHtml && isBackendDead()) {
|
|
2169
|
-
|
|
2494
|
+
// X-Bloby-Origin marks this as the agent's OWN branded page so the relay passes it through
|
|
2495
|
+
// (and never mistakes it for a Cloudflare tunnel error to be replaced).
|
|
2496
|
+
res.writeHead(503, { 'Content-Type': 'text/html', 'X-Bloby-Origin': 'supervisor', 'Cache-Control': 'no-store, no-cache, must-revalidate' });
|
|
2170
2497
|
res.end(backendDownPage(readBackendLogTail(100)));
|
|
2171
2498
|
return;
|
|
2172
2499
|
}
|
|
@@ -2174,7 +2501,7 @@ mint();
|
|
|
2174
2501
|
// Vite failed to boot (sentinel port) → serve the recovering page directly instead of
|
|
2175
2502
|
// proxying to a dead port. Chat (/bloby/*) is served earlier, so the lifeline stays up.
|
|
2176
2503
|
if (vitePorts.dashboard < 0) {
|
|
2177
|
-
res.writeHead(503, { 'Content-Type': 'text/html', 'Cache-Control': 'no-store, no-cache, must-revalidate' });
|
|
2504
|
+
res.writeHead(503, { 'Content-Type': 'text/html', 'X-Bloby-Origin': 'supervisor', 'Cache-Control': 'no-store, no-cache, must-revalidate' });
|
|
2178
2505
|
res.end(RECOVERING_HTML);
|
|
2179
2506
|
return;
|
|
2180
2507
|
}
|
|
@@ -2216,7 +2543,7 @@ mint();
|
|
|
2216
2543
|
);
|
|
2217
2544
|
proxy.on('error', (e) => {
|
|
2218
2545
|
console.error(`[supervisor] Dashboard Vite proxy error: ${req.url}`, e.message);
|
|
2219
|
-
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
2546
|
+
res.writeHead(503, { 'Content-Type': 'text/html', 'X-Bloby-Origin': 'supervisor' });
|
|
2220
2547
|
res.end(RECOVERING_HTML);
|
|
2221
2548
|
});
|
|
2222
2549
|
req.pipe(proxy);
|
|
@@ -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">
|
|
64
|
-
'<p style="color:#e4e4e7;font-size:1rem;line-height:1.6;margin:0 0 .4rem">Your latest change didn
|
|
65
|
-
'<p style="color:#a1a1aa;font-size:.93rem;line-height:1.6;margin:0 0 1.2rem">
|
|
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 didn’t 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>';
|
|
@@ -146,14 +146,14 @@ An array of scheduled tasks:
|
|
|
146
146
|
|
|
147
147
|
Your human can ask you to:
|
|
148
148
|
- Add a cron ("every morning at 9, summarize my notes")
|
|
149
|
-
- Remove
|
|
149
|
+
- Remove a cron ("stop the daily summary") — delete its entry from CRONS.json
|
|
150
150
|
- Change a schedule ("move the summary to 8am")
|
|
151
151
|
- List active crons ("what's scheduled?")
|
|
152
152
|
- Set a one-time reminder ("remind me at 3pm to call the dentist")
|
|
153
153
|
|
|
154
154
|
Just edit `CRONS.json` with the Write or Edit tool when asked. Each cron needs: `id` (unique slug), `schedule` (cron expression), `task` (what to do), `enabled` (boolean). Optionally add `"oneShot": true` for tasks that should run once and auto-delete — the scheduler removes them after they fire. Use `oneShot` for reminders, one-time alerts, and any task that doesn't repeat. The `paused` field is user-controlled — leave it alone (see below).
|
|
155
155
|
|
|
156
|
-
The `paused` field is **user-controlled** — it appears when your human pauses a cron from the Settings → Pulse & Crons screen. A paused cron has `"paused": true` and the scheduler skips it (it will not fire) while keeping the entry and its task file intact. Treat `paused` as read-only: **never set, change, or remove it yourself
|
|
156
|
+
The `paused` field is **user-controlled** — it appears when your human pauses a cron from the Settings → Pulse & Crons screen. A paused cron has `"paused": true` and the scheduler skips it (it will not fire) while keeping the entry and its task file intact. Treat `paused` as read-only: **never set, change, or remove it yourself** — preserve whatever `paused` value is already there when you edit a cron. If your human asks you in chat to pause or resume a cron, set `"paused": true` or `"paused": false`. To stop a cron permanently, delete its entry from CRONS.json (and its `tasks/{id}.md` file if it has one).
|
|
157
157
|
|
|
158
158
|
**Timezone: all cron schedules use system local time.** The scheduler evaluates crons against the system clock — no UTC conversion needed. When creating a cron, run `date` to check the current local time and write the schedule accordingly. If your human says "remind me at 3pm", use `0 15 * * *` — that's 3pm in whatever timezone the system is set to.
|
|
159
159
|
|
|
@@ -146,14 +146,14 @@ An array of scheduled tasks:
|
|
|
146
146
|
|
|
147
147
|
Your human can ask you to:
|
|
148
148
|
- Add a cron ("every morning at 9, summarize my notes")
|
|
149
|
-
- Remove
|
|
149
|
+
- Remove a cron ("stop the daily summary") — delete its entry from CRONS.json
|
|
150
150
|
- Change a schedule ("move the summary to 8am")
|
|
151
151
|
- List active crons ("what's scheduled?")
|
|
152
152
|
- Set a one-time reminder ("remind me at 3pm to call the dentist")
|
|
153
153
|
|
|
154
154
|
Just edit `CRONS.json` with the Write or Edit tool when asked. Each cron needs: `id` (unique slug), `schedule` (cron expression), `task` (what to do), `enabled` (boolean). Optionally add `"oneShot": true` for tasks that should run once and auto-delete — the scheduler removes them after they fire. Use `oneShot` for reminders, one-time alerts, and any task that doesn't repeat. The `paused` field is user-controlled — leave it alone (see below).
|
|
155
155
|
|
|
156
|
-
The `paused` field is **user-controlled** — it appears when your human pauses a cron from the Settings → Pulse & Crons screen. A paused cron has `"paused": true` and the scheduler skips it (it will not fire) while keeping the entry and its task file intact. Treat `paused` as read-only: **never set, change, or remove it yourself
|
|
156
|
+
The `paused` field is **user-controlled** — it appears when your human pauses a cron from the Settings → Pulse & Crons screen. A paused cron has `"paused": true` and the scheduler skips it (it will not fire) while keeping the entry and its task file intact. Treat `paused` as read-only: **never set, change, or remove it yourself** — preserve whatever `paused` value is already there when you edit a cron. If your human asks you in chat to pause or resume a cron, set `"paused": true` or `"paused": false`. To stop a cron permanently, delete its entry from CRONS.json (and its `tasks/{id}.md` file if it has one).
|
|
157
157
|
|
|
158
158
|
**Timezone: all cron schedules use system local time.** The scheduler evaluates crons against the system clock — no UTC conversion needed. When creating a cron, run `date` to check the current local time and write the schedule accordingly. If your human says "remind me at 3pm", use `0 15 * * *` — that's 3pm in whatever timezone the system is set to.
|
|
159
159
|
|
|
@@ -146,14 +146,14 @@ An array of scheduled tasks:
|
|
|
146
146
|
|
|
147
147
|
Your human can ask you to:
|
|
148
148
|
- Add a cron ("every morning at 9, summarize my notes")
|
|
149
|
-
- Remove
|
|
149
|
+
- Remove a cron ("stop the daily summary") — delete its entry from CRONS.json
|
|
150
150
|
- Change a schedule ("move the summary to 8am")
|
|
151
151
|
- List active crons ("what's scheduled?")
|
|
152
152
|
- Set a one-time reminder ("remind me at 3pm to call the dentist")
|
|
153
153
|
|
|
154
154
|
Just edit `CRONS.json` with the Write or Edit tool when asked. Each cron needs: `id` (unique slug), `schedule` (cron expression), `task` (what to do), `enabled` (boolean). Optionally add `"oneShot": true` for tasks that should run once and auto-delete — the scheduler removes them after they fire. Use `oneShot` for reminders, one-time alerts, and any task that doesn't repeat. The `paused` field is user-controlled — leave it alone (see below).
|
|
155
155
|
|
|
156
|
-
The `paused` field is **user-controlled** — it appears when your human pauses a cron from the Settings → Pulse & Crons screen. A paused cron has `"paused": true` and the scheduler skips it (it will not fire) while keeping the entry and its task file intact. Treat `paused` as read-only: **never set, change, or remove it yourself
|
|
156
|
+
The `paused` field is **user-controlled** — it appears when your human pauses a cron from the Settings → Pulse & Crons screen. A paused cron has `"paused": true` and the scheduler skips it (it will not fire) while keeping the entry and its task file intact. Treat `paused` as read-only: **never set, change, or remove it yourself** — preserve whatever `paused` value is already there when you edit a cron. If your human asks you in chat to pause or resume a cron, set `"paused": true` or `"paused": false`. To stop a cron permanently, delete its entry from CRONS.json (and its `tasks/{id}.md` file if it has one).
|
|
157
157
|
|
|
158
158
|
**Timezone: all cron schedules use system local time.** The scheduler evaluates crons against the system clock — no UTC conversion needed. When creating a cron, run `date` to check the current local time and write the schedule accordingly. If your human says "remind me at 3pm", use `0 15 * * *` — that's 3pm in whatever timezone the system is set to.
|
|
159
159
|
|