fluxy-bot 0.5.3 → 0.5.5
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-fluxy/assets/fluxy-A_p_vv4X.js +37 -0
- package/dist-fluxy/assets/{globals-CoXvncUh.css → globals-GP9z02As.css} +1 -1
- package/dist-fluxy/assets/{onboard-Bt368dtb.js → onboard-BDGESCXd.js} +1 -1
- package/dist-fluxy/fluxy.html +3 -3
- package/dist-fluxy/onboard.html +3 -3
- package/package.json +2 -1
- package/supervisor/chat/fluxy-main.tsx +109 -1
- package/supervisor/index.ts +2 -0
- package/supervisor/scheduler.ts +53 -10
- package/worker/db.ts +24 -0
- package/worker/index.ts +86 -1
- package/worker/prompts/fluxy-system-prompt.txt +9 -1
- package/workspace/client/public/sw.js +37 -1
- package/dist-fluxy/assets/fluxy-D-k6u4dU.js +0 -37
- /package/dist-fluxy/assets/{globals-E_PrLNnY.js → globals-BeMw745s.js} +0 -0
package/worker/index.ts
CHANGED
|
@@ -5,7 +5,8 @@ import path from 'path';
|
|
|
5
5
|
import { loadConfig, saveConfig } from '../shared/config.js';
|
|
6
6
|
import { paths, WORKSPACE_DIR } from '../shared/paths.js';
|
|
7
7
|
import { log } from '../shared/logger.js';
|
|
8
|
-
import { initDb, closeDb, listConversations, createConversation, deleteConversation, getMessages, addMessage, getSetting, getAllSettings, setSetting, createSession, getSession, deleteExpiredSessions, getRecentMessages } from './db.js';
|
|
8
|
+
import { initDb, closeDb, listConversations, createConversation, deleteConversation, getMessages, addMessage, getSetting, getAllSettings, setSetting, createSession, getSession, deleteExpiredSessions, getRecentMessages, addPushSubscription, removePushSubscription, getAllPushSubscriptions, getPushSubscriptionByEndpoint } from './db.js';
|
|
9
|
+
import webpush from 'web-push';
|
|
9
10
|
import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus, readCodexAccessToken } from './codex-auth.js';
|
|
10
11
|
import { startClaudeOAuth, exchangeClaudeCode, getClaudeAuthStatus, readClaudeAccessToken } from './claude-auth.js';
|
|
11
12
|
import { checkAvailability, registerHandle, releaseHandle, updateTunnelUrl, startHeartbeat, stopHeartbeat } from '../shared/relay.js';
|
|
@@ -34,6 +35,30 @@ initDb();
|
|
|
34
35
|
// Ensure file storage directories exist
|
|
35
36
|
ensureFileDirs();
|
|
36
37
|
|
|
38
|
+
// ── VAPID key management (Web Push) ──
|
|
39
|
+
|
|
40
|
+
function getOrCreateVapidKeys() {
|
|
41
|
+
let publicKey = getSetting('vapid_public_key');
|
|
42
|
+
let privateKey = getSetting('vapid_private_key');
|
|
43
|
+
if (!publicKey || !privateKey) {
|
|
44
|
+
const keys = webpush.generateVAPIDKeys();
|
|
45
|
+
publicKey = keys.publicKey;
|
|
46
|
+
privateKey = keys.privateKey;
|
|
47
|
+
setSetting('vapid_public_key', publicKey);
|
|
48
|
+
setSetting('vapid_private_key', privateKey);
|
|
49
|
+
log.ok('Generated new VAPID keys');
|
|
50
|
+
}
|
|
51
|
+
return { publicKey, privateKey };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function initWebPush() {
|
|
55
|
+
const { publicKey, privateKey } = getOrCreateVapidKeys();
|
|
56
|
+
webpush.setVapidDetails('mailto:push@fluxy.bot', publicKey, privateKey);
|
|
57
|
+
log.ok('Web Push initialized');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
initWebPush();
|
|
61
|
+
|
|
37
62
|
// Express
|
|
38
63
|
const app = express();
|
|
39
64
|
app.use(express.json({ limit: '10mb' }));
|
|
@@ -368,6 +393,66 @@ app.post('/api/onboard', (req, res) => {
|
|
|
368
393
|
res.json({ ok: true });
|
|
369
394
|
});
|
|
370
395
|
|
|
396
|
+
// ── Push notifications ──
|
|
397
|
+
|
|
398
|
+
app.get('/api/push/vapid-public-key', (_, res) => {
|
|
399
|
+
const key = getSetting('vapid_public_key');
|
|
400
|
+
if (!key) { res.status(500).json({ error: 'VAPID keys not initialized' }); return; }
|
|
401
|
+
res.json({ publicKey: key });
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
app.post('/api/push/subscribe', (req, res) => {
|
|
405
|
+
const { endpoint, keys } = req.body || {};
|
|
406
|
+
if (!endpoint || !keys?.p256dh || !keys?.auth) {
|
|
407
|
+
res.status(400).json({ error: 'Invalid subscription' });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
addPushSubscription(endpoint, keys.p256dh, keys.auth);
|
|
411
|
+
res.json({ ok: true });
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
app.delete('/api/push/unsubscribe', (req, res) => {
|
|
415
|
+
const { endpoint } = req.body || {};
|
|
416
|
+
if (!endpoint) { res.status(400).json({ error: 'Missing endpoint' }); return; }
|
|
417
|
+
removePushSubscription(endpoint);
|
|
418
|
+
res.json({ ok: true });
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
app.post('/api/push/send', async (req, res) => {
|
|
422
|
+
const { title, body, tag, url } = req.body || {};
|
|
423
|
+
const subs = getAllPushSubscriptions();
|
|
424
|
+
const payload = JSON.stringify({ title: title || 'Fluxy', body: body || '', tag: tag || 'fluxy', url: url || '/' });
|
|
425
|
+
|
|
426
|
+
const results = await Promise.allSettled(
|
|
427
|
+
subs.map(async (sub) => {
|
|
428
|
+
try {
|
|
429
|
+
await webpush.sendNotification(
|
|
430
|
+
{ endpoint: sub.endpoint, keys: { p256dh: sub.keys_p256dh, auth: sub.keys_auth } },
|
|
431
|
+
payload,
|
|
432
|
+
);
|
|
433
|
+
} catch (err: any) {
|
|
434
|
+
if (err.statusCode === 410 || err.statusCode === 404) {
|
|
435
|
+
removePushSubscription(sub.endpoint);
|
|
436
|
+
log.info(`[push] Removed expired subscription: ${sub.endpoint.slice(0, 60)}...`);
|
|
437
|
+
} else {
|
|
438
|
+
log.warn(`[push] Send failed: ${err.message}`);
|
|
439
|
+
}
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const sent = results.filter((r) => r.status === 'fulfilled').length;
|
|
446
|
+
res.json({ sent, total: subs.length });
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
app.get('/api/push/status', (req, res) => {
|
|
450
|
+
const endpoint = req.query.endpoint as string;
|
|
451
|
+
if (!endpoint) { res.json({ subscribed: false }); return; }
|
|
452
|
+
const sub = getPushSubscriptionByEndpoint(endpoint);
|
|
453
|
+
res.json({ subscribed: !!sub });
|
|
454
|
+
});
|
|
455
|
+
|
|
371
456
|
// ── Whisper transcription ──
|
|
372
457
|
|
|
373
458
|
app.post('/api/whisper/transcribe', express.json({ limit: '10mb' }), async (req, res) => {
|
|
@@ -124,6 +124,13 @@ An array of scheduled tasks:
|
|
|
124
124
|
"schedule": "0 9 * * *",
|
|
125
125
|
"task": "Write a daily summary of yesterday's notes",
|
|
126
126
|
"enabled": true
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "remind-meeting",
|
|
130
|
+
"schedule": "30 14 28 2 *",
|
|
131
|
+
"task": "Remind Bruno about the 3pm meeting",
|
|
132
|
+
"enabled": true,
|
|
133
|
+
"oneShot": true
|
|
127
134
|
}
|
|
128
135
|
]
|
|
129
136
|
```
|
|
@@ -133,8 +140,9 @@ Your human can ask you to:
|
|
|
133
140
|
- Remove or disable a cron ("stop the daily summary")
|
|
134
141
|
- Change a schedule ("move the summary to 8am")
|
|
135
142
|
- List active crons ("what's scheduled?")
|
|
143
|
+
- Set a one-time reminder ("remind me at 3pm to call the dentist")
|
|
136
144
|
|
|
137
|
-
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).
|
|
145
|
+
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.
|
|
138
146
|
|
|
139
147
|
When you receive a `<CRON>cron-id</CRON>` message, look up that ID in your CRONS.json (provided in your context above) to find the task description. Execute the task, save results to the appropriate files, finish your turn.
|
|
140
148
|
|
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Service worker — PWA installability + push notifications
|
|
2
2
|
self.addEventListener('install', () => self.skipWaiting());
|
|
3
3
|
self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
|
|
4
4
|
self.addEventListener('fetch', () => {});
|
|
5
|
+
|
|
6
|
+
// Push notification
|
|
7
|
+
self.addEventListener('push', (event) => {
|
|
8
|
+
let data = { title: 'Fluxy', body: 'New message' };
|
|
9
|
+
try {
|
|
10
|
+
data = event.data.json();
|
|
11
|
+
} catch {}
|
|
12
|
+
|
|
13
|
+
event.waitUntil(
|
|
14
|
+
self.registration.showNotification(data.title || 'Fluxy', {
|
|
15
|
+
body: data.body || '',
|
|
16
|
+
icon: '/fluxy-icon-192.png',
|
|
17
|
+
badge: '/fluxy-icon-192.png',
|
|
18
|
+
vibrate: [100, 50, 100],
|
|
19
|
+
tag: data.tag || 'fluxy-default',
|
|
20
|
+
data: { url: data.url || '/' },
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Notification click — focus or open app
|
|
26
|
+
self.addEventListener('notificationclick', (event) => {
|
|
27
|
+
event.notification.close();
|
|
28
|
+
const url = event.notification.data?.url || '/';
|
|
29
|
+
|
|
30
|
+
event.waitUntil(
|
|
31
|
+
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
|
|
32
|
+
for (const client of clients) {
|
|
33
|
+
if (client.url.includes('/fluxy') && 'focus' in client) {
|
|
34
|
+
return client.focus();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return self.clients.openWindow(url);
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
});
|