@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptic/maestro",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Maestro — Autonomous AI agent operating system. Deploy AI employees on dedicated Mac minis.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- const eventTs = data.ts || data.event_ts || "";
647
- const eventRef = data.raw_ref || `slack:${data.channel || ""}:${eventTs}`;
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
- const sender = data.sender || resolveName(data.user || data.user_id || "unknown");
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 || data.event?.text || "";
668
- const channelId = data.channel_id || data.channel || data.event?.channel || "";
669
- const threadTs = data.thread_id || data.thread_ts || data.event?.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
- const content = `type: priority_trigger
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 [ -z "$AGENT_DIR" ]; then
26
- if [ -d "$HOME/sophie-ai" ]; then
27
- AGENT_DIR="$HOME/sophie-ai"
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
- # Detect the agent working directory (prefer sophie-ai)
345
- local boot_agent_dir="$HOME/sophie-ai"
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 "║ Agent directory: $AGENT_DIR"
624
+ echo "║ Maestro: $AGENT_DIR"
625
+ echo "║ Boot target: $CALLER_DIR"
622
626
  echo "║ Current user: $CURRENT_USER"
623
627
  echo "╚══════════════════════════════════════════════════════════════╝"
624
628