@adaptic/maestro 1.1.6 → 1.1.8
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/.claude/commands/init-maestro.md +225 -279
- package/README.md +19 -2
- package/docs/guides/email-setup.md +399 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/outbound-governance-setup.md +438 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +550 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/slack-setup.md +348 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/whatsapp-setup.md +282 -0
- package/docs/runbooks/mac-mini-bootstrap.md +21 -0
- package/package.json +1 -1
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scripts/media-generation/README.md +2 -0
- package/scripts/pdf-generation/README.md +2 -0
- package/scripts/poller/slack-poller.mjs +22 -7
- package/scripts/poller/trigger.mjs +12 -1
- package/scripts/setup/boot-claude-session.sh +4 -8
- package/scripts/setup/configure-macos.sh +8 -4
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# WhatsApp Setup Guide
|
|
2
|
+
|
|
3
|
+
How to enable WhatsApp messaging for a Maestro agent: Twilio WhatsApp sandbox/production configuration, inbound webhook handler, outbound messaging (free-form and templates), media support, and Cloudflare tunnel exposure.
|
|
4
|
+
|
|
5
|
+
**Prerequisites**: Complete the [Voice & SMS Setup](voice-sms-setup.md) sections 1.1–1.3 (Twilio account and credentials). WhatsApp uses the same Twilio account as SMS.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Architecture Overview
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
13
|
+
│ INBOUND │
|
|
14
|
+
│ │
|
|
15
|
+
│ WhatsApp user ──▶ Twilio Cloud ──▶ Cloudflare Tunnel │
|
|
16
|
+
│ (webhook) (port 3002) │
|
|
17
|
+
│ │ │
|
|
18
|
+
│ ▼ │
|
|
19
|
+
│ whatsapp-handler.mjs │
|
|
20
|
+
│ │ │
|
|
21
|
+
│ ▼ │
|
|
22
|
+
│ state/inbox/whatsapp/*.yaml │
|
|
23
|
+
│ (inbox processor routes) │
|
|
24
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
25
|
+
│ OUTBOUND │
|
|
26
|
+
│ │
|
|
27
|
+
│ send-whatsapp.sh ──▶ Twilio Messages API ──▶ WhatsApp user │
|
|
28
|
+
│ (with dedup + (whatsapp: prefix) │
|
|
29
|
+
│ validation) │
|
|
30
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
31
|
+
│ CONFIGURATION │
|
|
32
|
+
│ │
|
|
33
|
+
│ configure-whatsapp-sandbox.sh │
|
|
34
|
+
│ (prints manual steps for Twilio Console webhook setup) │
|
|
35
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 1. WhatsApp Modes
|
|
41
|
+
|
|
42
|
+
Twilio offers two WhatsApp integration modes:
|
|
43
|
+
|
|
44
|
+
| Mode | Use Case | Sender Number | Limitations |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| **Sandbox** | Testing and development | +1 415 523 8886 (shared Twilio number) | Recipients must opt-in by sending "join \<keyword\>" |
|
|
47
|
+
| **Production** | Live operations | Agent's own Twilio number | Requires approved WhatsApp Business Account (WABA) |
|
|
48
|
+
|
|
49
|
+
**Start with sandbox mode** — it's free and requires no approval. Switch to production when ready for live operations.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. Sandbox Setup
|
|
54
|
+
|
|
55
|
+
### 2.1 Activate the Sandbox
|
|
56
|
+
|
|
57
|
+
1. Go to Twilio Console → Messaging → Try it Out → Send a WhatsApp Message
|
|
58
|
+
2. Follow the on-screen instructions to activate the sandbox
|
|
59
|
+
3. Note the **join keyword** (e.g. "join example-word")
|
|
60
|
+
|
|
61
|
+
### 2.2 Add Recipients to Sandbox
|
|
62
|
+
|
|
63
|
+
Each person who wants to message the agent must opt-in:
|
|
64
|
+
|
|
65
|
+
1. Save +1 415 523 8886 as a WhatsApp contact
|
|
66
|
+
2. Send the message: `join <keyword>` (the keyword from step 2.1)
|
|
67
|
+
3. They'll receive a confirmation reply
|
|
68
|
+
|
|
69
|
+
### 2.3 Configure Environment Variables
|
|
70
|
+
|
|
71
|
+
Add to `.env`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# WhatsApp mode
|
|
75
|
+
WHATSAPP_MODE=sandbox
|
|
76
|
+
|
|
77
|
+
# Twilio sandbox number (don't change)
|
|
78
|
+
WHATSAPP_SANDBOX_NUMBER=+14155238886
|
|
79
|
+
|
|
80
|
+
# Local port for webhook handler
|
|
81
|
+
WHATSAPP_PORT=3002
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 3. Production Setup
|
|
87
|
+
|
|
88
|
+
### 3.1 Apply for WhatsApp Business Account
|
|
89
|
+
|
|
90
|
+
1. Go to Twilio Console → Messaging → Senders → WhatsApp Senders
|
|
91
|
+
2. Submit a request to connect your Twilio number as a WhatsApp Business sender
|
|
92
|
+
3. This requires Meta/WhatsApp approval (can take days to weeks)
|
|
93
|
+
|
|
94
|
+
### 3.2 Configure for Production
|
|
95
|
+
|
|
96
|
+
Once approved, update `.env`:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
WHATSAPP_MODE=production
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
In production mode, `send-whatsapp.sh` uses the agent's Twilio number instead of the sandbox number. Business-initiated messages (outside the 24-hour conversation window) require approved message templates.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 4. Inbound — Webhook Handler
|
|
107
|
+
|
|
108
|
+
### 4.1 Start the Handler
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
node scripts/whatsapp-handler.mjs
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The handler listens on port 3002 (configurable via `WHATSAPP_PORT`) and provides:
|
|
115
|
+
|
|
116
|
+
| Endpoint | Method | Purpose |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `/whatsapp` | POST | Twilio inbound message webhook |
|
|
119
|
+
| `/whatsapp/status` | POST | Delivery status callbacks |
|
|
120
|
+
| `/health` | GET | Health check |
|
|
121
|
+
|
|
122
|
+
### 4.2 How It Works
|
|
123
|
+
|
|
124
|
+
1. Twilio receives a WhatsApp message to your number (or sandbox)
|
|
125
|
+
2. Twilio POSTs to your webhook URL at `/whatsapp`
|
|
126
|
+
3. Handler parses the message: strips `whatsapp:` prefix from From/To, extracts body, media
|
|
127
|
+
4. Looks up sender in `config/caller-id-map.yaml` for identity and access level
|
|
128
|
+
5. Writes a YAML inbox item to `state/inbox/whatsapp/`
|
|
129
|
+
6. CEO messages create priority trigger files for immediate processing
|
|
130
|
+
7. Returns empty TwiML (no auto-reply)
|
|
131
|
+
|
|
132
|
+
### 4.3 Expose via Cloudflare Tunnel
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Start a tunnel to the WhatsApp handler
|
|
136
|
+
cloudflared tunnel --url http://localhost:3002
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Copy the generated URL (e.g. `https://random-words.trycloudflare.com`).
|
|
140
|
+
|
|
141
|
+
### 4.4 Configure Twilio Webhook
|
|
142
|
+
|
|
143
|
+
Use the configuration helper:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Show current sandbox config
|
|
147
|
+
./scripts/configure-whatsapp-sandbox.sh --status
|
|
148
|
+
|
|
149
|
+
# Generate instructions for manual Twilio Console setup
|
|
150
|
+
./scripts/configure-whatsapp-sandbox.sh --url https://your-tunnel-url.trycloudflare.com
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The script prints manual steps since Twilio's WhatsApp sandbox webhooks must be configured in the Console:
|
|
154
|
+
|
|
155
|
+
1. Go to Twilio Console → Messaging → Try it Out → WhatsApp → Sandbox Settings
|
|
156
|
+
2. Set "When a message comes in": `https://your-tunnel-url/whatsapp` (HTTP POST)
|
|
157
|
+
3. Set "Status callback URL": `https://your-tunnel-url/whatsapp/status` (HTTP POST)
|
|
158
|
+
4. Save
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 5. Outbound — Sending Messages
|
|
163
|
+
|
|
164
|
+
### 5.1 Free-Form Messages
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Sandbox mode (default)
|
|
168
|
+
./scripts/send-whatsapp.sh --to "+971501234567" --body "Hello from the agent"
|
|
169
|
+
|
|
170
|
+
# Production mode
|
|
171
|
+
./scripts/send-whatsapp.sh --to "+971501234567" --body "Hello" --production
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 5.2 Template Messages
|
|
175
|
+
|
|
176
|
+
For business-initiated messages outside the 24-hour window (production mode only):
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
./scripts/send-whatsapp.sh --to "+971501234567" --template "intro_greeting" --vars "AgentName,Company"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 5.3 Media Messages
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
./scripts/send-whatsapp.sh --to "+971501234567" --body "Here's the report" --media "https://example.com/report.pdf"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 5.4 Pre-Send Pipeline
|
|
189
|
+
|
|
190
|
+
Outbound WhatsApp messages go through:
|
|
191
|
+
|
|
192
|
+
1. **Content-hash dedup** (`outbound-dedup.sh`) — prevents identical concurrent sends
|
|
193
|
+
2. **Factual validation** (`validate-outbound.py`) — blocks if critical issues found
|
|
194
|
+
3. **Audit logging** — every send logged to `logs/whatsapp/` and `logs/audit/`
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 6. Process Management
|
|
199
|
+
|
|
200
|
+
### 6.1 launchd Plist
|
|
201
|
+
|
|
202
|
+
A template plist exists at `scripts/poller-launchd/ai.adaptic.sophie-whatsapp-handler.plist`. Copy and customize for your agent:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Replace agent-specific values and install
|
|
206
|
+
cp scripts/poller-launchd/ai.adaptic.sophie-whatsapp-handler.plist \
|
|
207
|
+
~/Library/LaunchAgents/com.adaptic.AGENT.whatsapp-handler.plist
|
|
208
|
+
|
|
209
|
+
# Edit the plist to update paths and labels
|
|
210
|
+
# Then load:
|
|
211
|
+
launchctl load ~/Library/LaunchAgents/com.adaptic.AGENT.whatsapp-handler.plist
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 6.2 Cloudflare Tunnel Persistence
|
|
215
|
+
|
|
216
|
+
For production, configure a named tunnel (see [Voice & SMS Setup](voice-sms-setup.md) section 7.2) with an ingress rule for port 3002.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 7. Testing
|
|
221
|
+
|
|
222
|
+
| # | Test | How to Verify |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| 1 | Handler running | `curl http://localhost:3002/health` |
|
|
225
|
+
| 2 | Tunnel accessible | `curl https://your-tunnel-url/health` |
|
|
226
|
+
| 3 | Sandbox opt-in | Send "join \<keyword\>" from your phone to +1 415 523 8886 |
|
|
227
|
+
| 4 | Inbound message | Send WhatsApp to sandbox; check `state/inbox/whatsapp/` |
|
|
228
|
+
| 5 | Outbound message | `./scripts/send-whatsapp.sh --to "+YOUR_NUMBER" --body "Test"` |
|
|
229
|
+
| 6 | Caller ID lookup | Inbound from known number should show correct name in YAML |
|
|
230
|
+
| 7 | CEO priority | Message from principal should create priority trigger file |
|
|
231
|
+
| 8 | Dedup working | Send identical outbound twice; second should `DEDUP_SKIP` |
|
|
232
|
+
| 9 | Media outbound | Send with `--media` flag; verify media arrives |
|
|
233
|
+
| 10 | Audit logging | Check `logs/whatsapp/` and `logs/audit/` for entries |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 8. Troubleshooting
|
|
238
|
+
|
|
239
|
+
### Sandbox: "Recipient has not opted in"
|
|
240
|
+
|
|
241
|
+
Error code 63007 from Twilio means the recipient hasn't joined the sandbox:
|
|
242
|
+
1. Have them send "join \<keyword\>" to +1 415 523 8886
|
|
243
|
+
2. Verify the keyword matches what's shown in Twilio Console
|
|
244
|
+
3. They must send from the exact WhatsApp number you're trying to reach
|
|
245
|
+
|
|
246
|
+
### Messages not arriving at handler
|
|
247
|
+
|
|
248
|
+
1. Check tunnel: `curl https://your-tunnel-url/health`
|
|
249
|
+
2. Check Twilio webhook config matches your tunnel URL
|
|
250
|
+
3. Check handler is running: `curl http://localhost:3002/health`
|
|
251
|
+
4. Check handler logs: `tail -f logs/whatsapp-handler-stderr.log`
|
|
252
|
+
|
|
253
|
+
### "Template not found" in production mode
|
|
254
|
+
|
|
255
|
+
1. Templates must be approved by WhatsApp/Meta before use
|
|
256
|
+
2. Verify template name matches exactly (case-sensitive)
|
|
257
|
+
3. Verify template variables match the approved format
|
|
258
|
+
|
|
259
|
+
### Media not sending
|
|
260
|
+
|
|
261
|
+
1. Media URL must be publicly accessible (Twilio fetches it)
|
|
262
|
+
2. Supported types: images (JPEG, PNG), documents (PDF), audio (OGG)
|
|
263
|
+
3. Check Twilio's [media size limits](https://www.twilio.com/docs/whatsapp/guidance-whatsapp-media-messages)
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Key Files
|
|
268
|
+
|
|
269
|
+
| File | Purpose |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `scripts/send-whatsapp.sh` | Outbound WhatsApp sender (sandbox + production) |
|
|
272
|
+
| `scripts/whatsapp-handler.mjs` | Inbound WhatsApp webhook handler |
|
|
273
|
+
| `scripts/configure-whatsapp-sandbox.sh` | Sandbox webhook configuration helper |
|
|
274
|
+
| `config/caller-id-map.yaml` | WhatsApp number → identity mapping |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Related Documents
|
|
279
|
+
|
|
280
|
+
- [Voice & SMS Setup](voice-sms-setup.md) — Twilio account setup, SMS, and voice calls
|
|
281
|
+
- [Outbound Governance Setup](outbound-governance-setup.md) — Dedup and validation pipeline
|
|
282
|
+
- [Poller & Daemon Setup](poller-daemon-setup.md) — How WhatsApp events integrate with the event loop
|
|
@@ -393,6 +393,17 @@ echo "=============================="
|
|
|
393
393
|
- [ ] Log rotation configured
|
|
394
394
|
- [ ] Health check runs cleanly
|
|
395
395
|
- [ ] First morning brief generated successfully
|
|
396
|
+
- [ ] (Optional) Voice/SMS configured -- see `docs/guides/voice-sms-setup.md`
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Optional: Voice & SMS Setup
|
|
401
|
+
|
|
402
|
+
If this agent needs phone call or SMS capabilities (Slack huddle voice participation, inbound/outbound SMS via Twilio), follow the dedicated guide:
|
|
403
|
+
|
|
404
|
+
**[Voice & SMS Setup Guide](../guides/voice-sms-setup.md)**
|
|
405
|
+
|
|
406
|
+
This covers Twilio phone number purchase, inbound SMS webhook, outbound SMS, Slack huddle voice (Deepgram STT + ElevenLabs TTS), Cloudflare tunnel exposure, caller ID mapping, and launchd process management for the voice/SMS services.
|
|
396
407
|
|
|
397
408
|
---
|
|
398
409
|
|
|
@@ -400,5 +411,15 @@ Related documents:
|
|
|
400
411
|
|
|
401
412
|
- `docs/runbooks/perpetual-operations.md` -- How the system runs continuously
|
|
402
413
|
- `docs/runbooks/recovery-and-failover.md` -- Recovery procedures
|
|
414
|
+
- `docs/guides/agent-persona-setup.md` -- Agent identity and configuration
|
|
415
|
+
- `docs/guides/email-setup.md` -- Gmail IMAP and SMTP setup
|
|
416
|
+
- `docs/guides/slack-setup.md` -- Slack app, tokens, and events
|
|
417
|
+
- `docs/guides/whatsapp-setup.md` -- WhatsApp Business integration
|
|
418
|
+
- `docs/guides/voice-sms-setup.md` -- Voice calls and SMS setup
|
|
419
|
+
- `docs/guides/poller-daemon-setup.md` -- Event loop, daemon, triggers, watchdog
|
|
420
|
+
- `docs/guides/outbound-governance-setup.md` -- Hooks, dedup, information barriers
|
|
421
|
+
- `docs/guides/rag-context-setup.md` -- Search index and context retrieval
|
|
422
|
+
- `docs/guides/pdf-generation-setup.md` -- Branded PDF documents
|
|
423
|
+
- `docs/guides/media-generation-setup.md` -- AI-generated visual assets
|
|
403
424
|
- `docs/workflows/executive-cadence.md` -- Full operating rhythm
|
|
404
425
|
- `config/environment.yaml` -- Environment configuration reference
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Caller ID to user mapping for voice/SMS authorization
|
|
2
|
+
#
|
|
3
|
+
# Used by: sms-handler.mjs, parse-voice-transcript.mjs, user-context-search.py
|
|
4
|
+
# Maps phone numbers, Slack IDs, and emails to user identities with access levels.
|
|
5
|
+
#
|
|
6
|
+
# Access levels:
|
|
7
|
+
# ceo — Full access to all paths (memory, knowledge, state, outputs, docs, logs, all interactions)
|
|
8
|
+
# leadership — Company knowledge + docs + research/briefs + own interaction logs
|
|
9
|
+
# partner — Public knowledge + research + own interaction logs
|
|
10
|
+
# default — Unknown callers — public company knowledge only (knowledge/sources/)
|
|
11
|
+
#
|
|
12
|
+
# Hot-reloaded every 60 seconds by sms-handler.mjs — no restart needed after edits.
|
|
13
|
+
#
|
|
14
|
+
# Setup: See docs/guides/voice-sms-setup.md for full configuration instructions.
|
|
15
|
+
|
|
16
|
+
users:
|
|
17
|
+
# ── Principal (the person this agent reports to) ──────────────────────────
|
|
18
|
+
# Replace with your principal's details. This entry should have access_level: ceo.
|
|
19
|
+
principal:
|
|
20
|
+
name: "Principal Full Name"
|
|
21
|
+
phone: ["+1XXXXXXXXXX"]
|
|
22
|
+
whatsapp: ["+1XXXXXXXXXX"]
|
|
23
|
+
slack_id: "UXXXXXXXXXX"
|
|
24
|
+
email: "principal@company.com"
|
|
25
|
+
access_level: ceo
|
|
26
|
+
|
|
27
|
+
# ── Leadership / Team ─────────────────────────────────────────────────────
|
|
28
|
+
# Add team members who should be recognized by the SMS handler and voice parser.
|
|
29
|
+
# Each entry needs at minimum: name, slack_id, email, and access_level.
|
|
30
|
+
# Phone numbers are only needed if the person will contact the agent via SMS/voice.
|
|
31
|
+
|
|
32
|
+
# example-leader:
|
|
33
|
+
# name: "Example Leader"
|
|
34
|
+
# phone: []
|
|
35
|
+
# slack_id: "UXXXXXXXXXX"
|
|
36
|
+
# email: "leader@company.com"
|
|
37
|
+
# access_level: leadership
|
|
38
|
+
|
|
39
|
+
# ── Partners / External ───────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
# example-partner:
|
|
42
|
+
# name: "External Partner"
|
|
43
|
+
# phone: []
|
|
44
|
+
# slack_id: ""
|
|
45
|
+
# email: "partner@external.com"
|
|
46
|
+
# access_level: partner
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Media Generation Pipeline
|
|
2
2
|
|
|
3
|
+
> **Full setup guide**: See [docs/guides/media-generation-setup.md](../../docs/guides/media-generation-setup.md) for complete API setup, prompt spec authoring, brand alignment, testing, and troubleshooting.
|
|
4
|
+
|
|
3
5
|
Generates branded illustrations, diagrams, and video assets using Google Gemini and Veo APIs.
|
|
4
6
|
Ported from `~/adapticai/app/scripts/media-generation/` and `~/ai-born/`.
|
|
5
7
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# PDF Generation Pipeline
|
|
2
2
|
|
|
3
|
+
> **Full setup guide**: See [docs/guides/pdf-generation-setup.md](../../docs/guides/pdf-generation-setup.md) for complete installation, template customisation, brand integration, testing, and troubleshooting.
|
|
4
|
+
|
|
3
5
|
Generates professional PDF documents from Markdown using Pandoc + XeLaTeX.
|
|
4
6
|
Ported from the `~/ai-born` book generation pipeline, adapted for corporate documents.
|
|
5
7
|
|
|
@@ -643,8 +643,11 @@ export async function pollSlack() {
|
|
|
643
643
|
const data = JSON.parse(raw);
|
|
644
644
|
|
|
645
645
|
// Dedup: skip if we already have this item from API polling
|
|
646
|
-
|
|
647
|
-
const
|
|
646
|
+
// Events-server JSON uses `slack_event` key; also check `event` for compatibility
|
|
647
|
+
const evtPre = data.slack_event || data.event || {};
|
|
648
|
+
const eventTs = data.ts || data.event_ts || data.received_at || evtPre.ts || "";
|
|
649
|
+
const evtChannel = data.channel_id || data.channel || evtPre.channel || "";
|
|
650
|
+
const eventRef = data.raw_ref || `slack:${evtChannel}:${eventTs}`;
|
|
648
651
|
const alreadyHave = items.some((i) => i.raw_ref === eventRef);
|
|
649
652
|
if (alreadyHave) {
|
|
650
653
|
// Mark as processed since the API poll already got it
|
|
@@ -653,20 +656,22 @@ export async function pollSlack() {
|
|
|
653
656
|
}
|
|
654
657
|
|
|
655
658
|
// Convert events-server JSON to daemon item format
|
|
656
|
-
|
|
659
|
+
// Events-server JSON uses `slack_event` key; also check `event` for compatibility
|
|
660
|
+
const evt = data.slack_event || data.event || {};
|
|
661
|
+
const sender = data.sender || resolveName(data.user || data.user_id || evt.user || "unknown");
|
|
657
662
|
|
|
658
663
|
// Skip Sophie's own messages — the events server should filter these,
|
|
659
664
|
// but if any slip through (e.g. message_changed edge cases), catch here
|
|
660
|
-
const userId = data.user || data.user_id || "";
|
|
665
|
+
const userId = data.user || data.user_id || evt.user || "";
|
|
661
666
|
if (userId === SOPHIE_USER_ID || sender === "sophie-nguyen") {
|
|
662
667
|
try { renameSync(join(SLACK_INBOX_DIR, file), join(SLACK_INBOX_DIR, file + ".processed")); } catch {}
|
|
663
668
|
continue;
|
|
664
669
|
}
|
|
665
670
|
|
|
666
671
|
const privilege = data.sender_privilege || resolvePrivilege(userId);
|
|
667
|
-
const content = data.content || data.text ||
|
|
668
|
-
const channelId = data.channel_id || data.channel ||
|
|
669
|
-
const threadTs = data.thread_id || data.thread_ts ||
|
|
672
|
+
const content = data.content || data.text || evt.text || "";
|
|
673
|
+
const channelId = data.channel_id || data.channel || evt.channel || "";
|
|
674
|
+
const threadTs = data.thread_id || data.thread_ts || evt.thread_ts || "";
|
|
670
675
|
|
|
671
676
|
// Fetch thread context if this is a thread reply
|
|
672
677
|
let threadContext = data.thread_context || null;
|
|
@@ -674,6 +679,15 @@ export async function pollSlack() {
|
|
|
674
679
|
threadContext = await fetchThreadContext(channelId, threadTs, eventTs);
|
|
675
680
|
}
|
|
676
681
|
|
|
682
|
+
// Extract and download attachments from events-server items
|
|
683
|
+
const evtFiles = evt.files || data.files || [];
|
|
684
|
+
const evtAttachments = extractSlackAttachments(evtFiles);
|
|
685
|
+
for (const att of evtAttachments) {
|
|
686
|
+
const fileObj = evtFiles.find((f) => f.id === att.id);
|
|
687
|
+
const localPath = await downloadSlackAttachment(fileObj, SLACK_TOKEN);
|
|
688
|
+
if (localPath) att.local_path = localPath;
|
|
689
|
+
}
|
|
690
|
+
|
|
677
691
|
items.push({
|
|
678
692
|
id: data.id || eventTs.replace(".", "-") || file.replace(".json", ""),
|
|
679
693
|
service: "slack",
|
|
@@ -687,6 +701,7 @@ export async function pollSlack() {
|
|
|
687
701
|
thread_id: threadTs,
|
|
688
702
|
thread_context: threadContext,
|
|
689
703
|
is_reply: data.is_reply || (!!threadTs && threadTs !== eventTs),
|
|
704
|
+
attachments: evtAttachments.length > 0 ? evtAttachments : undefined,
|
|
690
705
|
priority_signals: data.priority_signals || {
|
|
691
706
|
from_ceo: privilege === "ceo",
|
|
692
707
|
tagged_urgent: /\b(urgent|emergency|asap|blocker|critical)\b/i.test(content),
|
|
@@ -28,7 +28,7 @@ export function triggerSophie(item) {
|
|
|
28
28
|
const dir = join(SOPHIE_AI_DIR, "state", "inbox", "internal");
|
|
29
29
|
mkdirSync(dir, { recursive: true });
|
|
30
30
|
const taskFile = join(dir, `priority-${Date.now()}.yaml`);
|
|
31
|
-
|
|
31
|
+
let content = `type: priority_trigger
|
|
32
32
|
reason: "${reason}"
|
|
33
33
|
source_item: "${item.raw_ref}"
|
|
34
34
|
timestamp: "${new Date().toISOString()}"
|
|
@@ -37,6 +37,17 @@ channel: "${item.channel}"
|
|
|
37
37
|
content: |
|
|
38
38
|
${(item.content || "").replace(/\n/g, "\n ")}
|
|
39
39
|
`;
|
|
40
|
+
// Include attachment metadata so downstream sessions can view files
|
|
41
|
+
if (item.attachments && item.attachments.length > 0) {
|
|
42
|
+
content += `attachments:\n`;
|
|
43
|
+
for (const att of item.attachments) {
|
|
44
|
+
content += ` - name: "${att.name || "unnamed"}"\n`;
|
|
45
|
+
content += ` mimetype: "${att.mimetype || "unknown"}"\n`;
|
|
46
|
+
content += ` size: ${att.size || 0}\n`;
|
|
47
|
+
if (att.local_path) content += ` local_path: "${att.local_path}"\n`;
|
|
48
|
+
if (att.url_private) content += ` url_private: "${att.url_private}"\n`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
40
51
|
writeFileSync(taskFile, content);
|
|
41
52
|
console.log(`[trigger] Priority task written to ${taskFile}`);
|
|
42
53
|
return true;
|
|
@@ -21,14 +21,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
21
21
|
MAESTRO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
22
22
|
|
|
23
23
|
# Detect agent directory — prefer sophie-ai if it exists
|
|
24
|
-
AGENT_DIR="${1
|
|
25
|
-
if [ -
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
else
|
|
29
|
-
echo "ERROR: No agent directory specified and ~/sophie-ai not found" >&2
|
|
30
|
-
exit 1
|
|
31
|
-
fi
|
|
24
|
+
AGENT_DIR="${1:?Agent directory required as first argument (e.g. ~/sophie-ai)}"
|
|
25
|
+
if [ ! -d "$AGENT_DIR" ]; then
|
|
26
|
+
echo "ERROR: Agent directory does not exist: $AGENT_DIR" >&2
|
|
27
|
+
exit 1
|
|
32
28
|
fi
|
|
33
29
|
|
|
34
30
|
AGENT_NAME=$(basename "$AGENT_DIR")
|
|
@@ -29,6 +29,10 @@ AGENT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
29
29
|
AGENT_NAME=$(basename "$AGENT_DIR")
|
|
30
30
|
CURRENT_USER=$(whoami)
|
|
31
31
|
|
|
32
|
+
# The directory from which this script was invoked — used as the boot-time
|
|
33
|
+
# Claude Code working directory (e.g. ~/sophie-ai, ~/wundr, etc.)
|
|
34
|
+
CALLER_DIR="${PWD}"
|
|
35
|
+
|
|
32
36
|
# Colours
|
|
33
37
|
RED='\033[0;31m'
|
|
34
38
|
GREEN='\033[0;32m'
|
|
@@ -341,9 +345,8 @@ configure_app_launches() {
|
|
|
341
345
|
local real_home
|
|
342
346
|
real_home=$(eval echo "~$real_user")
|
|
343
347
|
|
|
344
|
-
#
|
|
345
|
-
local boot_agent_dir="$
|
|
346
|
-
[ -d "$boot_agent_dir" ] || boot_agent_dir="$AGENT_DIR"
|
|
348
|
+
# Use the directory from which configure-macos.sh was invoked
|
|
349
|
+
local boot_agent_dir="$CALLER_DIR"
|
|
347
350
|
|
|
348
351
|
cat > "$PLIST_PATH" << PLIST_EOF
|
|
349
352
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -618,7 +621,8 @@ main() {
|
|
|
618
621
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
|
619
622
|
echo "║ Maestro — Mac Mini Configuration ║"
|
|
620
623
|
echo "╠══════════════════════════════════════════════════════════════╣"
|
|
621
|
-
echo "║
|
|
624
|
+
echo "║ Maestro: $AGENT_DIR"
|
|
625
|
+
echo "║ Boot target: $CALLER_DIR"
|
|
622
626
|
echo "║ Current user: $CURRENT_USER"
|
|
623
627
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
|
624
628
|
|