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.
- 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 +17 -0
- 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 +313 -0
- package/supervisor/workspace-guard.js +3 -3
- 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
|
@@ -518,11 +518,10 @@ export class WhatsAppChannel implements ChannelProvider {
|
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
-
// Skip if no text AND no images
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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't control Anthropic'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 '
|
|
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
|
-
|
|
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));
|
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' }));
|
|
@@ -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>';
|