417 lines
12 KiB
Markdown
417 lines
12 KiB
Markdown
|
|
# Agent Tools - Mobile Notifications & Alarms
|
||
|
|
|
||
|
|
**Alfred can send notifications, alerts, and alarms directly to your mobile device using FCM!**
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
The Alfred mobile app supports receiving notifications even when the app is closed, screen is locked, or device is asleep. This enables:
|
||
|
|
|
||
|
|
- **Alarms** - High-priority notifications that wake the device
|
||
|
|
- **Instant alerts** - Get notified when tasks complete
|
||
|
|
- **Timers** - Set countdown notifications
|
||
|
|
- **Reminders** - Schedule notifications for specific times
|
||
|
|
- **Background work** - Alfred can notify you when long-running tasks finish
|
||
|
|
|
||
|
|
**Key Features:**
|
||
|
|
- ✅ **FCM Push Notifications** - Delivery guaranteed even when app is closed
|
||
|
|
- ✅ **Token Persistence** - Works after proxy restarts
|
||
|
|
- ✅ **Dual Delivery** - WebSocket (if connected) + FCM (always)
|
||
|
|
- ✅ **Wake on Alarm** - Device wakes for high-priority alarms
|
||
|
|
|
||
|
|
## Features
|
||
|
|
|
||
|
|
### ✅ What's Working
|
||
|
|
|
||
|
|
1. **Instant Notifications** - Alfred can send alerts immediately
|
||
|
|
2. **System Notifications** - Show even when app is backgrounded
|
||
|
|
3. **In-App Display** - Notifications appear in chat when app is open
|
||
|
|
4. **Multiple Notification Types** - Alert (⚠️), Timer (⏰), Reminder (🔔)
|
||
|
|
5. **Optional TTS** - Speak notifications when TTS is enabled (foreground only)
|
||
|
|
6. **WebSocket Broadcast** - All connected mobile devices receive notifications
|
||
|
|
|
||
|
|
### 📱 Mobile App Features
|
||
|
|
|
||
|
|
- **Background Notifications** - Receive notifications even when app is closed
|
||
|
|
- **Notification Icons** - Visual indicators for different notification types
|
||
|
|
- **System Tray** - Notifications appear in Android notification shade
|
||
|
|
- **Tap to Open** - Tapping notification opens the app
|
||
|
|
- **Auto-dismiss** - Notifications clear when tapped
|
||
|
|
|
||
|
|
## Usage from Alfred
|
||
|
|
|
||
|
|
### Send Alarms
|
||
|
|
|
||
|
|
**Use `alfred-notify` for all notifications and alarms.**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# High-priority alarm (wakes device)
|
||
|
|
alfred-notify --alarm "Wake up!"
|
||
|
|
|
||
|
|
# Alarm with custom title
|
||
|
|
alfred-notify --alarm --title "⏰ Morning Alarm" "Time to get up!"
|
||
|
|
|
||
|
|
# Silent alarm (vibrate only)
|
||
|
|
alfred-notify --alarm --no-sound "Silent wake-up"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Send Notifications
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Basic notification
|
||
|
|
alfred-notify "Task completed successfully"
|
||
|
|
|
||
|
|
# Notification with custom title
|
||
|
|
alfred-notify --title "Build System" "Compilation finished"
|
||
|
|
|
||
|
|
# Silent notification
|
||
|
|
alfred-notify --no-sound --no-vibrate "Background update complete"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Schedule via Cron
|
||
|
|
|
||
|
|
For scheduled alarms/reminders, use the `cron` tool:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Set alarm for 5 minutes from now
|
||
|
|
{
|
||
|
|
"name": "5 minute alarm",
|
||
|
|
"schedule": {
|
||
|
|
"kind": "at",
|
||
|
|
"atMs": Date.now() + (5 * 60 * 1000)
|
||
|
|
},
|
||
|
|
"payload": {
|
||
|
|
"kind": "agentTurn",
|
||
|
|
"message": "Run: alfred-notify --alarm '⏰ 5 minute alarm!'",
|
||
|
|
"deliver": false
|
||
|
|
},
|
||
|
|
"sessionTarget": "isolated"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Important:** Always use `sessionTarget: "isolated"` + `agentTurn` for alarms, never `systemEvent`.
|
||
|
|
|
||
|
|
## Alfred Integration
|
||
|
|
|
||
|
|
Alfred uses `alfred-notify` from cron jobs or via exec tool:
|
||
|
|
|
||
|
|
**Example conversation:**
|
||
|
|
```
|
||
|
|
You: "Set an alarm for 5 minutes"
|
||
|
|
Alfred: *schedules cron job* "I'll send an alarm in 5 minutes!"
|
||
|
|
```
|
||
|
|
|
||
|
|
Behind the scenes, Alfred creates a cron job:
|
||
|
|
```javascript
|
||
|
|
cron.add({
|
||
|
|
schedule: { kind: "at", atMs: Date.now() + 300000 },
|
||
|
|
payload: {
|
||
|
|
kind: "agentTurn",
|
||
|
|
message: "Run: alfred-notify --alarm '⏰ 5 minute alarm!'"
|
||
|
|
},
|
||
|
|
sessionTarget: "isolated"
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
**Another example:**
|
||
|
|
```
|
||
|
|
You: "Let me know when the build finishes"
|
||
|
|
Alfred: "Sure, I'll notify you when it completes"
|
||
|
|
```
|
||
|
|
|
||
|
|
Alfred monitors the build and executes:
|
||
|
|
```javascript
|
||
|
|
exec(['alfred-notify', '--title', 'Build System', 'Build completed! ✅'])
|
||
|
|
```
|
||
|
|
|
||
|
|
## Technical Details
|
||
|
|
|
||
|
|
### Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────┐
|
||
|
|
│ Alfred │ (OpenClaw agent)
|
||
|
|
│ exec/cron │
|
||
|
|
└────────┬────────┘
|
||
|
|
│ executes
|
||
|
|
▼
|
||
|
|
┌─────────────────┐
|
||
|
|
│ alfred-notify │ (CLI wrapper)
|
||
|
|
│ bash script │
|
||
|
|
└────────┬────────┘
|
||
|
|
│ HTTP POST
|
||
|
|
▼
|
||
|
|
┌─────────────────┐
|
||
|
|
│ Alfred Proxy │ (port 18790)
|
||
|
|
│ + Firebase │
|
||
|
|
│ Admin SDK │
|
||
|
|
└────────┬────────┘
|
||
|
|
│
|
||
|
|
├──────────────────────┬─────────────────────┐
|
||
|
|
│ WebSocket │ FCM Push │
|
||
|
|
│ (if connected) │ (always) │
|
||
|
|
▼ ▼ ▼
|
||
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||
|
|
│ Mobile App(s) │ │ Firebase Cloud │ │ fcm-tokens │
|
||
|
|
│ GatewayClient │ │ Messaging │ │ .json (disk) │
|
||
|
|
└────────┬────────┘ └────────┬────────┘ └─────────────────┘
|
||
|
|
│ │
|
||
|
|
│ ▼
|
||
|
|
│ ┌─────────────────┐
|
||
|
|
│ │ Mobile Device │
|
||
|
|
│ │ (even asleep) │
|
||
|
|
│ └────────┬────────┘
|
||
|
|
│ │
|
||
|
|
└─────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────┐
|
||
|
|
│ Android OS │ (system notifications)
|
||
|
|
└─────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**Key Features:**
|
||
|
|
- **Dual Delivery:** WebSocket (instant if connected) + FCM (guaranteed)
|
||
|
|
- **Token Persistence:** FCM tokens saved to `fcm-tokens.json`, survive restarts
|
||
|
|
- **Wake Device:** FCM can wake locked/sleeping devices for alarms
|
||
|
|
- **Firebase Admin SDK:** Uses `cloudmessaging.messages.create` permission
|
||
|
|
|
||
|
|
### Event Format
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "event",
|
||
|
|
"event": "mobile.notification",
|
||
|
|
"payload": {
|
||
|
|
"notificationType": "alert|timer|reminder",
|
||
|
|
"title": "Alfred",
|
||
|
|
"message": "Notification text",
|
||
|
|
"priority": "default",
|
||
|
|
"sound": true,
|
||
|
|
"vibrate": true,
|
||
|
|
"timestamp": 1706918400000,
|
||
|
|
"action": null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Mobile App Handling
|
||
|
|
|
||
|
|
1. **GatewayClient** receives `mobile.notification` event via WebSocket
|
||
|
|
2. **MainScreen** `onNotification()` callback processes the event
|
||
|
|
3. **Notification icons** added based on type (⏰ ⚠️ 🔔 📢)
|
||
|
|
4. **System notification** shown if timer/reminder OR if app is backgrounded
|
||
|
|
5. **Chat message** added if app is in foreground
|
||
|
|
6. **Optional TTS** speaks the notification if enabled (foreground only)
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
### Test Instant Notification
|
||
|
|
|
||
|
|
From terminal:
|
||
|
|
```bash
|
||
|
|
cd ~/.openclaw/workspace/alfred-proxy
|
||
|
|
./alfred-notify "Test notification!"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test Alarm
|
||
|
|
|
||
|
|
```bash
|
||
|
|
./alfred-notify --alarm "Test alarm!"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test with Custom Title
|
||
|
|
|
||
|
|
```bash
|
||
|
|
./alfred-notify --title "Kitchen Timer" "Oven is ready!"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test Silent Notification
|
||
|
|
|
||
|
|
```bash
|
||
|
|
./alfred-notify --no-sound --no-vibrate "Silent test"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test via Alfred
|
||
|
|
|
||
|
|
Ask Alfred:
|
||
|
|
```
|
||
|
|
"Send me a test alarm"
|
||
|
|
"Set an alarm for 1 minute"
|
||
|
|
"Notify me when this finishes"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Monitoring
|
||
|
|
|
||
|
|
### Check Proxy Status
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Service status
|
||
|
|
systemctl --user status alfred-proxy.service
|
||
|
|
|
||
|
|
# Watch logs
|
||
|
|
tail -f /tmp/alfred-proxy.log
|
||
|
|
|
||
|
|
# Check for FCM token registration
|
||
|
|
grep "fcm.*Registering" /tmp/alfred-proxy.log
|
||
|
|
|
||
|
|
# Check for successful sends
|
||
|
|
grep "fcm.*Successfully" /tmp/alfred-proxy.log
|
||
|
|
```
|
||
|
|
|
||
|
|
### Check Connected Clients & Tokens
|
||
|
|
|
||
|
|
When app connects, you'll see:
|
||
|
|
```
|
||
|
|
[proxy] New connection from ::ffff:192.168.1.20
|
||
|
|
[auth] Token validated for user: shadow@dao-mail.com
|
||
|
|
[fcm] Registering token for user a2b49b91...: ewqRvIsOTuiWJk...
|
||
|
|
[fcm] Saved tokens to disk
|
||
|
|
```
|
||
|
|
|
||
|
|
### Check Token Persistence
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# View persisted FCM tokens
|
||
|
|
cat ~/.openclaw/workspace/alfred-proxy/fcm-tokens.json
|
||
|
|
|
||
|
|
# Should show:
|
||
|
|
{
|
||
|
|
"user-id-hash": [
|
||
|
|
"fcm-token-string"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test Notification Delivery
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Via CLI (recommended)
|
||
|
|
alfred-notify --alarm "Test"
|
||
|
|
|
||
|
|
# Via HTTP API
|
||
|
|
curl -X POST http://localhost:18790/api/notify \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{
|
||
|
|
"notificationType": "alarm",
|
||
|
|
"title": "Test",
|
||
|
|
"message": "Hello!",
|
||
|
|
"priority": "high",
|
||
|
|
"sound": true,
|
||
|
|
"vibrate": true
|
||
|
|
}'
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected response:
|
||
|
|
```json
|
||
|
|
{"success":true,"clients":0,"fcm":1}
|
||
|
|
```
|
||
|
|
- `clients`: WebSocket connections (0 if app closed)
|
||
|
|
- `fcm`: FCM devices notified (should be 1+)
|
||
|
|
|
||
|
|
## Troubleshooting
|
||
|
|
|
||
|
|
### No notifications appearing
|
||
|
|
|
||
|
|
1. **Check FCM tokens persisted**:
|
||
|
|
```bash
|
||
|
|
cat alfred-proxy/fcm-tokens.json
|
||
|
|
# Should show registered tokens
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Check proxy logs for FCM success**:
|
||
|
|
```bash
|
||
|
|
tail -f /tmp/alfred-proxy.log | grep fcm
|
||
|
|
# Should show: [fcm] Successfully sent X message(s)
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **If seeing "Permission denied" error**:
|
||
|
|
```
|
||
|
|
[fcm] Error: Permission 'cloudmessaging.messages.create' denied
|
||
|
|
```
|
||
|
|
- Check service account has correct role: **Firebase Admin SDK Administrator Service Agent**
|
||
|
|
- Verify FCM API is enabled in Google Cloud Console
|
||
|
|
- Regenerate service account key if needed
|
||
|
|
|
||
|
|
4. **If "No FCM tokens registered"**:
|
||
|
|
- Open Alfred app to trigger token registration
|
||
|
|
- Check logs for `[fcm] Registering token`
|
||
|
|
- Verify app sends token on connect (should happen automatically)
|
||
|
|
|
||
|
|
5. **Test directly**:
|
||
|
|
```bash
|
||
|
|
alfred-notify --alarm "Test"
|
||
|
|
```
|
||
|
|
|
||
|
|
### WebSocket vs FCM
|
||
|
|
|
||
|
|
- **WebSocket**: Instant delivery when app is open and connected
|
||
|
|
- **FCM**: Always works, even when app is closed or device is asleep
|
||
|
|
- Both should show in response: `{"clients":1,"fcm":1}`
|
||
|
|
|
||
|
|
If `clients:0, fcm:1` → App closed but FCM working (normal)
|
||
|
|
If `clients:0, fcm:0` → No tokens registered (open app to fix)
|
||
|
|
|
||
|
|
### Cron alarms not firing
|
||
|
|
|
||
|
|
1. **Verify cron job uses correct format**:
|
||
|
|
```javascript
|
||
|
|
// ✅ CORRECT
|
||
|
|
{
|
||
|
|
"sessionTarget": "isolated",
|
||
|
|
"payload": {
|
||
|
|
"kind": "agentTurn",
|
||
|
|
"message": "Run: alfred-notify --alarm 'Message'"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ❌ WRONG
|
||
|
|
{
|
||
|
|
"sessionTarget": "main",
|
||
|
|
"payload": {
|
||
|
|
"kind": "systemEvent",
|
||
|
|
"text": "Alarm message"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Check cron job exists**:
|
||
|
|
```bash
|
||
|
|
# Ask Alfred in chat
|
||
|
|
/cron list
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Check cron execution logs**:
|
||
|
|
```bash
|
||
|
|
tail -f /tmp/alfred-proxy.log
|
||
|
|
# Look for cron execution and alfred-notify calls
|
||
|
|
```
|
||
|
|
|
||
|
|
### Firebase Permission Errors
|
||
|
|
|
||
|
|
**Error:** `Permission 'cloudmessaging.messages.create' denied`
|
||
|
|
|
||
|
|
**Solution:**
|
||
|
|
1. Verify service account role: **Firebase Admin SDK Administrator Service Agent** (NOT "Firebase Cloud Messaging Admin")
|
||
|
|
2. Enable FCM API: https://console.cloud.google.com/apis/library/fcm.googleapis.com
|
||
|
|
3. Download fresh service account key
|
||
|
|
4. Place at: `alfred-proxy/service-account.json`
|
||
|
|
5. Restart proxy: `systemctl --user restart alfred-proxy.service`
|
||
|
|
|
||
|
|
See `FCM_SETUP.md` for full setup instructions.
|
||
|
|
|
||
|
|
## Future Enhancements
|
||
|
|
|
||
|
|
Potential additions:
|
||
|
|
|
||
|
|
- [ ] **Custom sounds** - Per-notification sound selection
|
||
|
|
- [ ] **Action buttons** - "Snooze", "Mark as done", etc.
|
||
|
|
- [ ] **Deep links** - Open specific app screens from notification
|
||
|
|
- [ ] **Rich content** - Images, progress bars, etc.
|
||
|
|
- [ ] **Notification groups** - Collapse similar notifications
|
||
|
|
- [ ] **Do Not Disturb** - Respect user quiet hours
|
||
|
|
- [ ] **Multi-device sync** - Mark as read across devices
|
||
|
|
|
||
|
|
## Version
|
||
|
|
|
||
|
|
1.0.0 - Initial release with alert/timer/reminder support
|