@adaptic/maestro 1.5.2 → 1.6.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/bin/maestro.mjs +17 -0
- package/package.json +1 -1
- package/scaffold/config/agent.json +52 -0
- package/scaffold/config/agent.ts.example +62 -192
- package/scripts/archive-email.sh +16 -2
- package/scripts/daemon/classifier.mjs +69 -59
- package/scripts/daemon/dispatcher.mjs +6 -6
- package/scripts/daemon/prompt-builder.mjs +30 -12
- package/scripts/daemon/responder.mjs +41 -21
- package/scripts/daemon/session-lock.mjs +4 -4
- package/scripts/daemon/sophie-daemon.mjs +49 -26
- package/scripts/local-triggers/run-trigger.sh +6 -6
- package/scripts/pdf-generation/build-document.mjs +24 -7
- package/scripts/poller/gmail-poller.mjs +32 -18
- package/scripts/poller/utils.mjs +34 -14
- package/scripts/send-email-threaded.py +30 -10
- package/scripts/send-email-with-attachment.py +26 -9
- package/scripts/send-email.sh +23 -10
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/slack-events-server.mjs +78 -44
- package/scripts/slack-responded.sh +5 -2
- package/scripts/slack-send.sh +25 -9
- package/scripts/sms-handler.mjs +32 -18
- package/scripts/whatsapp-handler.mjs +27 -14
package/scripts/slack-send.sh
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# slack-send.sh — Send Slack message as
|
|
3
|
-
# Messages appear as
|
|
2
|
+
# slack-send.sh — Send Slack message as the running agent via User OAuth Token (xoxp-)
|
|
3
|
+
# Messages appear as the agent's own Slack user, NOT the bot app.
|
|
4
4
|
# Usage: ./scripts/slack-send.sh <channel_id> <message> [--thread_ts <ts>]
|
|
5
5
|
set -e
|
|
6
6
|
|
|
7
|
-
source
|
|
7
|
+
# Resolve the agent repo dir so we can source env files regardless of cwd.
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
10
|
+
|
|
11
|
+
# Load identity vars (AGENT_FIRST_NAME, AGENT_FULL_NAME, AGENT_EMAIL, …) so
|
|
12
|
+
# nothing in this script needs to hardcode the agent's name or paths.
|
|
13
|
+
if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
|
|
14
|
+
# shellcheck disable=SC1091
|
|
15
|
+
source "$AGENT_REPO_DIR/config/agent.env"
|
|
16
|
+
fi
|
|
8
17
|
|
|
9
|
-
#
|
|
18
|
+
# Load secrets from the agent's .env (must live alongside agent.env).
|
|
19
|
+
if [ -f "$AGENT_REPO_DIR/.env" ]; then
|
|
20
|
+
# shellcheck disable=SC1091
|
|
21
|
+
source "$AGENT_REPO_DIR/.env"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# ENFORCEMENT: Must use User Token (xoxp-), never Bot Token (xoxb-).
|
|
10
25
|
# Per CEO directive: all Slack sends must come from user scope so messages
|
|
11
|
-
# appear as
|
|
26
|
+
# appear as the running agent, not a generic bot app.
|
|
12
27
|
SLACK_SEND_TOKEN="${SLACK_USER_TOKEN}"
|
|
13
28
|
if [ -z "$SLACK_SEND_TOKEN" ]; then
|
|
14
29
|
echo "ERROR: SLACK_USER_TOKEN not set in .env — cannot send as user scope" >&2
|
|
@@ -58,14 +73,15 @@ if [ -z "$CHANNEL" ] || [ -z "$MESSAGE" ]; then
|
|
|
58
73
|
fi
|
|
59
74
|
|
|
60
75
|
# Dedup check: if --responding_to is set, use atomic locking to prevent concurrent sends
|
|
61
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
62
76
|
DEDUP_ACQUIRED=""
|
|
63
77
|
if [ -n "$RESPONDING_TO" ]; then
|
|
64
|
-
# Atomic lock acquisition (mkdir-based, race-safe)
|
|
78
|
+
# Atomic lock acquisition (mkdir-based, race-safe).
|
|
65
79
|
# This is the primary dedup mechanism. mkdir is atomic on POSIX — if two sessions
|
|
66
80
|
# race to respond to the same message, exactly one will succeed and the other will
|
|
67
81
|
# get LOCKED and skip. This eliminates the check-then-write race condition.
|
|
68
|
-
|
|
82
|
+
# AGENT_SESSION_ID is the per-agent session identifier (used to be SOPHIE_SESSION_ID,
|
|
83
|
+
# RAVI_SESSION_ID, etc.); $$ is the fallback when running outside a daemon-spawned shell.
|
|
84
|
+
SESSION_ID="${AGENT_SESSION_ID:-${SOPHIE_SESSION_ID:-${RAVI_SESSION_ID:-$$}}}"
|
|
69
85
|
ACQUIRE_RESULT=$("$SCRIPT_DIR/slack-responded.sh" acquire "$CHANNEL" "$RESPONDING_TO" "$SESSION_ID" 2>/dev/null) || true
|
|
70
86
|
if [ "$ACQUIRE_RESULT" != "ACQUIRED" ]; then
|
|
71
87
|
echo "DEDUP_SKIP"
|
|
@@ -75,7 +91,7 @@ if [ -n "$RESPONDING_TO" ]; then
|
|
|
75
91
|
fi
|
|
76
92
|
|
|
77
93
|
# Show typing indicator before sending (non-blocking, fire-and-forget)
|
|
78
|
-
# This makes
|
|
94
|
+
# This makes the agent's presence feel human — the "X is typing..." dots appear
|
|
79
95
|
# before the message arrives. Requires rtm:stream scope on user token; degrades gracefully.
|
|
80
96
|
TYPING_DURATION=2500
|
|
81
97
|
MSG_LEN=${#MESSAGE}
|
package/scripts/sms-handler.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Agent SMS Handler — Twilio Inbound SMS Webhook
|
|
4
4
|
*
|
|
5
5
|
* Receives inbound SMS/MMS via Twilio webhook, maps senders to known contacts,
|
|
6
6
|
* writes messages to the inbox for the inbox processor, and logs everything.
|
|
@@ -31,13 +31,27 @@ import { URL } from "node:url";
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
|
|
33
33
|
const PORT = parseInt(process.env.SMS_PORT || "3001", 10);
|
|
34
|
-
const
|
|
35
|
-
process.env.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
const AGENT_REPO_DIR =
|
|
35
|
+
process.env.AGENT_DIR ||
|
|
36
|
+
process.env.AGENT_REPO_DIR ||
|
|
37
|
+
path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
38
|
+
|
|
39
|
+
// Load identity from canonical SOT (config/agent.json).
|
|
40
|
+
let _agent = null;
|
|
41
|
+
function loadAgent() {
|
|
42
|
+
if (_agent) return _agent;
|
|
43
|
+
try {
|
|
44
|
+
_agent = JSON.parse(fs.readFileSync(path.join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
_agent = { firstName: "Agent", phone: "" };
|
|
47
|
+
}
|
|
48
|
+
return _agent;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const INBOX_DIR = path.join(AGENT_REPO_DIR, "state", "inbox", "sms");
|
|
52
|
+
const LOG_DIR = path.join(AGENT_REPO_DIR, "logs", "sms");
|
|
53
|
+
const CALLER_ID_MAP_PATH = path.join(AGENT_REPO_DIR, "config", "caller-id-map.yaml");
|
|
54
|
+
const AGENT_NUMBER = loadAgent().phone || "";
|
|
41
55
|
|
|
42
56
|
// ---------------------------------------------------------------------------
|
|
43
57
|
// Utilities
|
|
@@ -166,10 +180,10 @@ function logSms(entry) {
|
|
|
166
180
|
}
|
|
167
181
|
|
|
168
182
|
/**
|
|
169
|
-
* Log to the main audit trail (
|
|
183
|
+
* Log to the agent's main audit trail (logs/audit/).
|
|
170
184
|
*/
|
|
171
185
|
function logAudit(action, details) {
|
|
172
|
-
const auditDir = path.join(
|
|
186
|
+
const auditDir = path.join(AGENT_REPO_DIR, "logs", "audit");
|
|
173
187
|
ensureDir(auditDir);
|
|
174
188
|
|
|
175
189
|
const auditFile = path.join(auditDir, `${today()}-actions.jsonl`);
|
|
@@ -229,13 +243,13 @@ function writeToInbox(smsData) {
|
|
|
229
243
|
`subject: "SMS from ${senderName}"`,
|
|
230
244
|
`content: |`,
|
|
231
245
|
` ${(smsData.Body || "").replace(/\n/g, "\n ")}`,
|
|
232
|
-
`to: "${smsData.To ||
|
|
246
|
+
`to: "${smsData.To || AGENT_NUMBER}"`,
|
|
233
247
|
`is_reply: false`,
|
|
234
248
|
`priority_signals:`,
|
|
235
249
|
` from_ceo: ${senderPrivilege === "ceo"}`,
|
|
236
250
|
` tagged_urgent: false`,
|
|
237
251
|
` contains_deadline: false`,
|
|
238
|
-
`
|
|
252
|
+
` mentions_agent: false`,
|
|
239
253
|
];
|
|
240
254
|
|
|
241
255
|
if (mediaUrls.length > 0) {
|
|
@@ -283,7 +297,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
283
297
|
res.end(
|
|
284
298
|
JSON.stringify({
|
|
285
299
|
status: "ok",
|
|
286
|
-
service: "
|
|
300
|
+
service: "agent-sms-handler",
|
|
287
301
|
uptime: process.uptime(),
|
|
288
302
|
timestamp: new Date().toISOString(),
|
|
289
303
|
knownContacts: callerMap.size,
|
|
@@ -297,8 +311,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
297
311
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
298
312
|
res.end(
|
|
299
313
|
JSON.stringify({
|
|
300
|
-
service: "
|
|
301
|
-
description: "Twilio inbound SMS webhook for
|
|
314
|
+
service: "Agent SMS Handler",
|
|
315
|
+
description: "Twilio inbound SMS webhook for the agent's inbox",
|
|
302
316
|
endpoints: {
|
|
303
317
|
health: "/health",
|
|
304
318
|
sms: "/sms (POST)",
|
|
@@ -319,7 +333,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
319
333
|
const smsData = parseFormBody(body);
|
|
320
334
|
|
|
321
335
|
const from = smsData.From || "unknown";
|
|
322
|
-
const to = smsData.To ||
|
|
336
|
+
const to = smsData.To || AGENT_NUMBER;
|
|
323
337
|
const messageBody = smsData.Body || "";
|
|
324
338
|
const messageSid = smsData.MessageSid || "unknown";
|
|
325
339
|
const numMedia = parseInt(smsData.NumMedia || "0", 10);
|
|
@@ -357,7 +371,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
357
371
|
|
|
358
372
|
// Write priority trigger
|
|
359
373
|
const priorityDir = path.join(
|
|
360
|
-
|
|
374
|
+
AGENT_REPO_DIR,
|
|
361
375
|
"state",
|
|
362
376
|
"inbox",
|
|
363
377
|
"processed",
|
|
@@ -415,7 +429,7 @@ ensureDir(LOG_DIR);
|
|
|
415
429
|
|
|
416
430
|
server.listen(PORT, "0.0.0.0", () => {
|
|
417
431
|
console.log(`\n========================================`);
|
|
418
|
-
console.log(`
|
|
432
|
+
console.log(` Agent SMS Handler`);
|
|
419
433
|
console.log(` Port: ${PORT}`);
|
|
420
434
|
console.log(` Health: http://0.0.0.0:${PORT}/health`);
|
|
421
435
|
console.log(` Webhook: POST /sms`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Agent WhatsApp Handler — Twilio Inbound WhatsApp Webhook
|
|
4
4
|
*
|
|
5
5
|
* Receives inbound WhatsApp messages via Twilio webhook, maps senders to known
|
|
6
6
|
* contacts, writes messages to the inbox for the inbox processor, and logs everything.
|
|
@@ -37,13 +37,26 @@ import { URL } from "node:url";
|
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
|
|
39
39
|
const PORT = parseInt(process.env.WHATSAPP_PORT || "3002", 10);
|
|
40
|
-
const
|
|
41
|
-
process.env.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
const AGENT_REPO_DIR =
|
|
41
|
+
process.env.AGENT_DIR ||
|
|
42
|
+
process.env.AGENT_REPO_DIR ||
|
|
43
|
+
path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
44
|
+
|
|
45
|
+
let _agent = null;
|
|
46
|
+
function loadAgent() {
|
|
47
|
+
if (_agent) return _agent;
|
|
48
|
+
try {
|
|
49
|
+
_agent = JSON.parse(fs.readFileSync(path.join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
50
|
+
} catch {
|
|
51
|
+
_agent = { firstName: "Agent", phone: "" };
|
|
52
|
+
}
|
|
53
|
+
return _agent;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const INBOX_DIR = path.join(AGENT_REPO_DIR, "state", "inbox", "whatsapp");
|
|
57
|
+
const LOG_DIR = path.join(AGENT_REPO_DIR, "logs", "whatsapp");
|
|
58
|
+
const CALLER_ID_MAP_PATH = path.join(AGENT_REPO_DIR, "config", "caller-id-map.yaml");
|
|
59
|
+
const AGENT_NUMBER = loadAgent().phone || "";
|
|
47
60
|
|
|
48
61
|
// ---------------------------------------------------------------------------
|
|
49
62
|
// Utilities
|
|
@@ -190,7 +203,7 @@ function logWhatsApp(entry) {
|
|
|
190
203
|
* Log to the main audit trail.
|
|
191
204
|
*/
|
|
192
205
|
function logAudit(action, details) {
|
|
193
|
-
const auditDir = path.join(
|
|
206
|
+
const auditDir = path.join(AGENT_REPO_DIR, "logs", "audit");
|
|
194
207
|
ensureDir(auditDir);
|
|
195
208
|
|
|
196
209
|
const auditFile = path.join(auditDir, `${today()}-actions.jsonl`);
|
|
@@ -267,7 +280,7 @@ function writeToInbox(msgData) {
|
|
|
267
280
|
` from_ceo: ${senderPrivilege === "ceo"}`,
|
|
268
281
|
` tagged_urgent: ${body.toLowerCase().includes("urgent")}`,
|
|
269
282
|
` contains_deadline: false`,
|
|
270
|
-
`
|
|
283
|
+
` mentions_agent: ${body.toLowerCase().includes((loadAgent().firstName || "agent").toLowerCase())}`,
|
|
271
284
|
];
|
|
272
285
|
|
|
273
286
|
if (mediaUrls.length > 0) {
|
|
@@ -311,7 +324,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
311
324
|
res.end(
|
|
312
325
|
JSON.stringify({
|
|
313
326
|
status: "ok",
|
|
314
|
-
service: "
|
|
327
|
+
service: "agent-whatsapp-handler",
|
|
315
328
|
uptime: process.uptime(),
|
|
316
329
|
timestamp: new Date().toISOString(),
|
|
317
330
|
knownContacts: callerMap.size,
|
|
@@ -325,8 +338,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
325
338
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
326
339
|
res.end(
|
|
327
340
|
JSON.stringify({
|
|
328
|
-
service: "
|
|
329
|
-
description: "Twilio inbound WhatsApp webhook for
|
|
341
|
+
service: "Agent WhatsApp Handler",
|
|
342
|
+
description: "Twilio inbound WhatsApp webhook for the agent's inbox",
|
|
330
343
|
endpoints: {
|
|
331
344
|
health: "/health",
|
|
332
345
|
whatsapp: "/whatsapp (POST)",
|
|
@@ -503,7 +516,7 @@ ensureDir(LOG_DIR);
|
|
|
503
516
|
|
|
504
517
|
server.listen(PORT, "0.0.0.0", () => {
|
|
505
518
|
console.log(`\n========================================`);
|
|
506
|
-
console.log(`
|
|
519
|
+
console.log(` Agent WhatsApp Handler`);
|
|
507
520
|
console.log(` Port: ${PORT}`);
|
|
508
521
|
console.log(` Health: http://0.0.0.0:${PORT}/health`);
|
|
509
522
|
console.log(` Webhook: POST /whatsapp`);
|