@adaptic/maestro 1.5.1 → 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 +35 -16
- 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/send-email.sh
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# send-email.sh — Send HTML email via SMTP as
|
|
2
|
+
# send-email.sh — Send HTML email via SMTP as the running agent
|
|
3
3
|
# Usage: ./scripts/send-email.sh <to> <subject> <body> [cc] [in-reply-to] [references]
|
|
4
4
|
set -e
|
|
5
5
|
|
|
6
|
+
SCRIPT_DIR_INIT="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR_INIT/.." && pwd)}"
|
|
8
|
+
if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "$AGENT_REPO_DIR/config/agent.env"
|
|
11
|
+
fi
|
|
12
|
+
|
|
6
13
|
TO="$1"
|
|
7
14
|
SUBJECT="$2"
|
|
8
15
|
BODY="$3"
|
|
@@ -25,7 +32,7 @@ if [ -x "$SCRIPT_DIR/outbound-dedup.sh" ]; then
|
|
|
25
32
|
# Generate content-hash key and acquire lock
|
|
26
33
|
CONTENT_KEY=$("$SCRIPT_DIR/outbound-dedup.sh" generate-key email "$TO" "$SUBJECT" "$BODY" 2>/dev/null) || true
|
|
27
34
|
if [ -n "$CONTENT_KEY" ]; then
|
|
28
|
-
ACQUIRE_RESULT=$("$SCRIPT_DIR/outbound-dedup.sh" acquire email "$CONTENT_KEY" "${SOPHIE_SESSION_ID:-$$}" 2>/dev/null) || true
|
|
35
|
+
ACQUIRE_RESULT=$("$SCRIPT_DIR/outbound-dedup.sh" acquire email "$CONTENT_KEY" "${AGENT_SESSION_ID:-${SOPHIE_SESSION_ID:-${RAVI_SESSION_ID:-$$}}}" 2>/dev/null) || true
|
|
29
36
|
if [ "$ACQUIRE_RESULT" = "DEDUP_SKIP" ]; then
|
|
30
37
|
echo "DEDUP_SKIP"
|
|
31
38
|
exit 0
|
|
@@ -39,7 +46,7 @@ fi
|
|
|
39
46
|
|
|
40
47
|
# Build headers
|
|
41
48
|
HEADERS="To: ${TO}
|
|
42
|
-
From:
|
|
49
|
+
From: ${AGENT_FULL_NAME:-Agent} <${AGENT_EMAIL:-agent@example.com}>
|
|
43
50
|
Subject: ${SUBJECT}
|
|
44
51
|
MIME-Version: 1.0
|
|
45
52
|
Content-Type: text/html; charset=UTF-8"
|
|
@@ -61,23 +68,29 @@ References: <${IN_REPLY_TO}>"
|
|
|
61
68
|
fi
|
|
62
69
|
fi
|
|
63
70
|
|
|
64
|
-
LOGO_URL="https://ci3.googleusercontent.com/mail-sig/AIorK4xNDnPN6a0MjSHV5urb9g11u2CzHIzwdJbwN21quGjzQFdbOdegCq1Wp6lq3NCqsARbi-ooqPfE9E1D"
|
|
71
|
+
LOGO_URL="${EMAIL_SIGNATURE_LOGO_URL:-https://ci3.googleusercontent.com/mail-sig/AIorK4xNDnPN6a0MjSHV5urb9g11u2CzHIzwdJbwN21quGjzQFdbOdegCq1Wp6lq3NCqsARbi-ooqPfE9E1D}"
|
|
72
|
+
SIG_ADDRESS="${EMAIL_SIGNATURE_ADDRESS:-Level 1, Innovation One, Dubai International Financial Centre, Dubai, UAE}"
|
|
73
|
+
SIG_PHONE_HTML=""
|
|
74
|
+
if [ -n "${AGENT_PHONE:-}" ]; then
|
|
75
|
+
PHONE_TEL=$(echo "$AGENT_PHONE" | tr -d ' ')
|
|
76
|
+
SIG_PHONE_HTML="<div style=\"font-size:small;\"><a href=\"tel:${PHONE_TEL}\" style=\"color:#1155cc;\">${AGENT_PHONE}</a></div>
|
|
77
|
+
<div><br></div>
|
|
78
|
+
"
|
|
79
|
+
fi
|
|
65
80
|
|
|
66
|
-
# Build HTML body
|
|
81
|
+
# Build HTML body
|
|
67
82
|
HTML_BODY="<html><body>
|
|
68
83
|
<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:small;color:#000;\">
|
|
69
84
|
$(echo "$BODY" | sed 's/$/<br>/g')
|
|
70
85
|
</div>
|
|
71
86
|
<br>
|
|
72
87
|
<div style=\"font-family:Arial,Helvetica,sans-serif;color:#000;\">
|
|
73
|
-
<div style=\"font-size:small;\"><b
|
|
74
|
-
<div style=\"font-size:small;\"
|
|
88
|
+
<div style=\"font-size:small;\"><b>${AGENT_FULL_NAME:-Agent}</b></div>
|
|
89
|
+
<div style=\"font-size:small;\">${AGENT_TITLE:-}</div>
|
|
75
90
|
<div><br></div>
|
|
76
91
|
<div><img src=\"${LOGO_URL}\" width=\"125\"></div>
|
|
77
92
|
<div><br></div>
|
|
78
|
-
<div style=\"font-size:small
|
|
79
|
-
<div><br></div>
|
|
80
|
-
<div style=\"font-size:x-small;color:#000;\">Level 1, Innovation One, Dubai International Financial Centre, Dubai, UAE</div>
|
|
93
|
+
${SIG_PHONE_HTML}<div style=\"font-size:x-small;color:#000;\">${SIG_ADDRESS}</div>
|
|
81
94
|
<div style=\"font-size:x-small;color:#666;\">_________________________________________</div>
|
|
82
95
|
<div><br></div>
|
|
83
96
|
<div style=\"font-size:x-small;color:#999;\">This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error please notify the sender. This message contains confidential information and is intended only for the individual named. If you are not the named addressee you should not disseminate, distribute or copy this email. Please notify the sender immediately by email if you have received this email by mistake and delete this email from your system. If you are not the intended recipient you are notified that disclosing, copying, distributing, or taking any action in reliance on the contents of this information is strictly prohibited.</div>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generate-agent-env.mjs — Generate config/agent.env from config/agent.json
|
|
4
|
+
*
|
|
5
|
+
* Shell scripts can't import TypeScript or parse JSON cheaply, so we
|
|
6
|
+
* pre-derive a flat env file that any bash script can source:
|
|
7
|
+
*
|
|
8
|
+
* source "$AGENT_DIR/config/agent.env"
|
|
9
|
+
* echo "Hello, $AGENT_FIRST_NAME"
|
|
10
|
+
*
|
|
11
|
+
* The generator is invoked:
|
|
12
|
+
* - During `npm run init-agent` (initial setup)
|
|
13
|
+
* - At the end of every `maestro upgrade` run
|
|
14
|
+
* - On demand: `node scripts/setup/generate-agent-env.mjs`
|
|
15
|
+
*
|
|
16
|
+
* No TypeScript runtime required — we read agent.json directly.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
20
|
+
import { resolve, dirname } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
|
|
25
|
+
const JSON_PATH = resolve(AGENT_DIR, "config/agent.json");
|
|
26
|
+
const ENV_PATH = resolve(AGENT_DIR, "config/agent.env");
|
|
27
|
+
|
|
28
|
+
if (!existsSync(JSON_PATH)) {
|
|
29
|
+
console.error(`[generate-agent-env] config/agent.json not found at ${JSON_PATH}`);
|
|
30
|
+
console.error("[generate-agent-env] Run /init-maestro to configure identity, or migrate from config/agent.ts");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const agent = JSON.parse(readFileSync(JSON_PATH, "utf-8"));
|
|
35
|
+
|
|
36
|
+
// Shell-quote: wrap in single quotes; replace ' with '\'' (close, escape, reopen).
|
|
37
|
+
const q = (v) => {
|
|
38
|
+
const s = v === null || v === undefined ? "" : String(v);
|
|
39
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const repoDir = `$HOME/${agent.repoSlug}`;
|
|
43
|
+
const firstUpper = (agent.firstName || "").toUpperCase();
|
|
44
|
+
const firstLower = (agent.firstName || "").toLowerCase();
|
|
45
|
+
const signature = `${agent.fullName} | ${agent.title} | ${agent.company}`;
|
|
46
|
+
|
|
47
|
+
const lines = [
|
|
48
|
+
`# Auto-generated from config/agent.json by @adaptic/maestro.`,
|
|
49
|
+
`# DO NOT EDIT — regenerate with: node scripts/setup/generate-agent-env.mjs`,
|
|
50
|
+
`# Sourced by every shell script that needs agent identity.`,
|
|
51
|
+
``,
|
|
52
|
+
`# ── Agent identity ─────────────────────────────────────────────────`,
|
|
53
|
+
`export AGENT_FIRST_NAME=${q(agent.firstName)}`,
|
|
54
|
+
`export AGENT_LAST_NAME=${q(agent.lastName)}`,
|
|
55
|
+
`export AGENT_FULL_NAME=${q(agent.fullName)}`,
|
|
56
|
+
`export AGENT_TITLE=${q(agent.title)}`,
|
|
57
|
+
`export AGENT_ARCHETYPE=${q(agent.archetype)}`,
|
|
58
|
+
`export AGENT_EMAIL=${q(agent.email)}`,
|
|
59
|
+
`export AGENT_PHONE=${q(agent.phone)}`,
|
|
60
|
+
`export AGENT_SLACK_USER_ID=${q(agent.slackMemberId)}`,
|
|
61
|
+
`export AGENT_REPO_SLUG=${q(agent.repoSlug)}`,
|
|
62
|
+
`export AGENT_REPO_DIR="${repoDir}"`,
|
|
63
|
+
`export AGENT_MACHINE_NAME=${q(agent.machineName)}`,
|
|
64
|
+
`export AGENT_LAUNCHD_PREFIX=${q(agent.launchdLabelPrefix)}`,
|
|
65
|
+
`export AGENT_TIMEZONE=${q(agent.timezone)}`,
|
|
66
|
+
`export AGENT_LOCALE=${q(agent.locale)}`,
|
|
67
|
+
`export AGENT_UPPER=${q(firstUpper)}`,
|
|
68
|
+
`export AGENT_LOWER=${q(firstLower)}`,
|
|
69
|
+
`export AGENT_EMAIL_SIGNATURE=${q(signature)}`,
|
|
70
|
+
``,
|
|
71
|
+
`# ── Principal ──────────────────────────────────────────────────────`,
|
|
72
|
+
`export PRINCIPAL_FIRST_NAME=${q(agent.principal?.firstName)}`,
|
|
73
|
+
`export PRINCIPAL_LAST_NAME=${q(agent.principal?.lastName)}`,
|
|
74
|
+
`export PRINCIPAL_FULL_NAME=${q(agent.principal?.fullName)}`,
|
|
75
|
+
`export PRINCIPAL_TITLE=${q(agent.principal?.title)}`,
|
|
76
|
+
`export PRINCIPAL_EMAIL=${q(agent.principal?.email)}`,
|
|
77
|
+
`export PRINCIPAL_SLACK_USER_ID=${q(agent.principal?.slackMemberId)}`,
|
|
78
|
+
``,
|
|
79
|
+
`# ── Company ────────────────────────────────────────────────────────`,
|
|
80
|
+
`export COMPANY_NAME=${q(agent.company)}`,
|
|
81
|
+
`export COMPANY_DOMAIN=${q(agent.companyDomain)}`,
|
|
82
|
+
`export COMPANY_DESCRIPTION=${q(agent.companyDescription)}`,
|
|
83
|
+
``,
|
|
84
|
+
`# ── Schedule ───────────────────────────────────────────────────────`,
|
|
85
|
+
`export SCHEDULE_MORNING_BRIEF=${q(agent.schedule?.morningBrief)}`,
|
|
86
|
+
`export SCHEDULE_EVENING_WRAP=${q(agent.schedule?.eveningWrap)}`,
|
|
87
|
+
`export SCHEDULE_OVERNIGHT_MONITORING=${q(agent.schedule?.overnightMonitoring)}`,
|
|
88
|
+
``,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
writeFileSync(ENV_PATH, lines.join("\n"));
|
|
92
|
+
console.log(`[generate-agent-env] wrote ${ENV_PATH}`);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* migrate-agent-to-sot.mjs — Migrate an existing maestro agent repo to the
|
|
4
|
+
* Source-of-Truth layout (`config/agent.json` canonical + `config/agent.ts`
|
|
5
|
+
* thin wrapper + `config/agent.env` generated).
|
|
6
|
+
*
|
|
7
|
+
* Before:
|
|
8
|
+
* config/agent.ts ← contains inline `export const agent = { firstName: "Ravi", ... }`
|
|
9
|
+
*
|
|
10
|
+
* After:
|
|
11
|
+
* config/agent.json ← canonical data extracted from agent.ts
|
|
12
|
+
* config/agent.ts ← thin wrapper that imports agent.json + provides types
|
|
13
|
+
* config/agent.env ← generated for shell scripts
|
|
14
|
+
*
|
|
15
|
+
* Run from inside an agent repo:
|
|
16
|
+
* npx tsx scripts/setup/migrate-agent-to-sot.mjs
|
|
17
|
+
*
|
|
18
|
+
* Requires `tsx` in the agent's devDependencies (it is — the scaffold sets it).
|
|
19
|
+
*
|
|
20
|
+
* Safety:
|
|
21
|
+
* - Backs up the existing config/agent.ts to config/agent.ts.bak
|
|
22
|
+
* - Refuses to run if config/agent.json already exists
|
|
23
|
+
* - Refuses to run if config/agent.ts can't be imported
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
|
|
27
|
+
import { resolve, dirname } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { execFileSync } from "node:child_process";
|
|
30
|
+
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
|
|
33
|
+
const TS_PATH = resolve(AGENT_DIR, "config/agent.ts");
|
|
34
|
+
const JSON_PATH = resolve(AGENT_DIR, "config/agent.json");
|
|
35
|
+
const BAK_PATH = resolve(AGENT_DIR, "config/agent.ts.pre-sot-migration.bak");
|
|
36
|
+
|
|
37
|
+
if (!existsSync(TS_PATH)) {
|
|
38
|
+
console.error(`[migrate] config/agent.ts not found at ${TS_PATH}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (existsSync(JSON_PATH)) {
|
|
43
|
+
console.error(`[migrate] config/agent.json already exists — already migrated?`);
|
|
44
|
+
console.error(`[migrate] Delete it manually if you want to re-run.`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Use tsx to load the existing TS module. This is the most reliable way to
|
|
49
|
+
// extract the data without writing a fragile parser.
|
|
50
|
+
let agent;
|
|
51
|
+
try {
|
|
52
|
+
const tsxBin = execFileSync("npx", ["--yes", "tsx", "-e", "console.log(0)"], {
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
55
|
+
});
|
|
56
|
+
// tsx is available — load via inline eval.
|
|
57
|
+
const json = execFileSync(
|
|
58
|
+
"npx",
|
|
59
|
+
[
|
|
60
|
+
"--yes",
|
|
61
|
+
"tsx",
|
|
62
|
+
"-e",
|
|
63
|
+
`import("${TS_PATH}").then(m => { console.log(JSON.stringify(m.agent)); }).catch(e => { console.error(e.message); process.exit(1); });`,
|
|
64
|
+
],
|
|
65
|
+
{ encoding: "utf-8" }
|
|
66
|
+
);
|
|
67
|
+
agent = JSON.parse(json.trim());
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error(`[migrate] failed to import config/agent.ts: ${e.message}`);
|
|
70
|
+
console.error(`[migrate] ensure tsx is installed (npm install --save-dev tsx) and agent.ts has a default \`export const agent = ...\``);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Sanity check
|
|
75
|
+
if (!agent || typeof agent !== "object" || !agent.firstName) {
|
|
76
|
+
console.error("[migrate] extracted config doesn't look right (missing firstName)");
|
|
77
|
+
console.error("[migrate] received:", JSON.stringify(agent).slice(0, 200));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Backup
|
|
82
|
+
copyFileSync(TS_PATH, BAK_PATH);
|
|
83
|
+
console.log(`[migrate] backed up agent.ts → ${BAK_PATH}`);
|
|
84
|
+
|
|
85
|
+
// Write canonical JSON
|
|
86
|
+
writeFileSync(JSON_PATH, JSON.stringify(agent, null, 2) + "\n");
|
|
87
|
+
console.log(`[migrate] wrote config/agent.json (canonical)`);
|
|
88
|
+
|
|
89
|
+
// Replace agent.ts with thin wrapper template (read from scaffold).
|
|
90
|
+
// The wrapper is shipped alongside this migration script in the maestro
|
|
91
|
+
// package, but we also fall back to writing it inline so the migration
|
|
92
|
+
// works even without access to the scaffold.
|
|
93
|
+
const wrapperTemplate = inlineWrapper();
|
|
94
|
+
writeFileSync(TS_PATH, wrapperTemplate);
|
|
95
|
+
console.log(`[migrate] rewrote config/agent.ts as thin JSON wrapper`);
|
|
96
|
+
|
|
97
|
+
// Generate agent.env
|
|
98
|
+
try {
|
|
99
|
+
execFileSync(
|
|
100
|
+
"node",
|
|
101
|
+
[resolve(__dirname, "generate-agent-env.mjs")],
|
|
102
|
+
{ encoding: "utf-8", stdio: "inherit" }
|
|
103
|
+
);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn(`[migrate] could not auto-generate agent.env: ${e.message}`);
|
|
106
|
+
console.warn(`[migrate] run manually: node scripts/setup/generate-agent-env.mjs`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log();
|
|
110
|
+
console.log("[migrate] ✓ migration complete");
|
|
111
|
+
console.log(`[migrate] config/agent.json canonical identity data`);
|
|
112
|
+
console.log(`[migrate] config/agent.ts thin TS wrapper (imports agent.json)`);
|
|
113
|
+
console.log(`[migrate] config/agent.env shell-sourceable identity vars`);
|
|
114
|
+
console.log(`[migrate] ${BAK_PATH.split("/").slice(-2).join("/")} original (delete after verifying)`);
|
|
115
|
+
|
|
116
|
+
function inlineWrapper() {
|
|
117
|
+
return `/**
|
|
118
|
+
* Maestro — Agent Configuration (TypeScript wrapper)
|
|
119
|
+
*
|
|
120
|
+
* The canonical source of truth is \`config/agent.json\`. Edit that file,
|
|
121
|
+
* then run \`node scripts/setup/generate-agent-env.mjs\` to regenerate the
|
|
122
|
+
* shell environment file.
|
|
123
|
+
*
|
|
124
|
+
* This wrapper exposes typed access plus a few computed convenience values.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
import agentData from './agent.json' with { type: 'json' };
|
|
128
|
+
|
|
129
|
+
export interface VoiceMode {
|
|
130
|
+
id: string;
|
|
131
|
+
label: string;
|
|
132
|
+
description: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface PrincipalConfig {
|
|
136
|
+
firstName: string;
|
|
137
|
+
lastName: string;
|
|
138
|
+
fullName: string;
|
|
139
|
+
title: string;
|
|
140
|
+
email: string;
|
|
141
|
+
slackMemberId: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface AgentConfig {
|
|
145
|
+
firstName: string;
|
|
146
|
+
lastName: string;
|
|
147
|
+
fullName: string;
|
|
148
|
+
title: string;
|
|
149
|
+
archetype: string;
|
|
150
|
+
|
|
151
|
+
email: string;
|
|
152
|
+
phone: string;
|
|
153
|
+
slackMemberId: string;
|
|
154
|
+
|
|
155
|
+
company: string;
|
|
156
|
+
companyDomain: string;
|
|
157
|
+
companyDescription: string;
|
|
158
|
+
|
|
159
|
+
principal: PrincipalConfig;
|
|
160
|
+
|
|
161
|
+
machineName: string;
|
|
162
|
+
repoSlug: string;
|
|
163
|
+
launchdLabelPrefix: string;
|
|
164
|
+
|
|
165
|
+
timezone: string;
|
|
166
|
+
locale: string;
|
|
167
|
+
|
|
168
|
+
schedule: {
|
|
169
|
+
morningBrief: string;
|
|
170
|
+
commsTriage: string[];
|
|
171
|
+
eveningWrap: string;
|
|
172
|
+
overnightMonitoring: string;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
communication: {
|
|
176
|
+
defaultTone: string;
|
|
177
|
+
externalTone: string;
|
|
178
|
+
voiceModes: VoiceMode[];
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
responsibilities: string[];
|
|
182
|
+
operatingPrinciples: string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const agent: AgentConfig = agentData as AgentConfig;
|
|
186
|
+
|
|
187
|
+
export const AGENT_UPPER = agent.firstName.toUpperCase();
|
|
188
|
+
export const AGENT_LOWER = agent.firstName.toLowerCase();
|
|
189
|
+
export const REPO_DIR = \`~/\${agent.repoSlug}\`;
|
|
190
|
+
export const EMAIL_SIGNATURE = \`\${agent.fullName} | \${agent.title} | \${agent.company}\`;
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Slack Events API Server — Real-Time Event Receiver
|
|
3
|
+
* Slack Events API Server — Real-Time Event Receiver
|
|
4
4
|
*
|
|
5
5
|
* Receives real-time events from Slack via the bot's Event Subscriptions.
|
|
6
6
|
* Writes inbound events to state/inbox/slack/ in YAML format (matching poller output).
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Events handled:
|
|
12
12
|
* - message (DM, channel messages, thread replies — all channel types)
|
|
13
|
-
* - app_mention (
|
|
13
|
+
* - app_mention (the agent @mentioned)
|
|
14
14
|
* - reaction_added (emoji reactions)
|
|
15
15
|
*
|
|
16
16
|
* Deduplication:
|
|
@@ -72,15 +72,45 @@ const POLLER_TRACKER_DIR = path.join(ROOT, "state/slack-thread-tracker");
|
|
|
72
72
|
const TRIGGER_DIR = path.join(ROOT, "state/triggers/priority");
|
|
73
73
|
const LOG_DIR = path.join(ROOT, "logs/polling");
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
// Load the running agent's identity (canonical SOT) so we can filter
|
|
76
|
+
// self-messages and resolve the principal's user ID without hardcoding.
|
|
77
|
+
let _agent = null;
|
|
78
|
+
function loadAgent() {
|
|
79
|
+
if (_agent) return _agent;
|
|
80
|
+
try {
|
|
81
|
+
_agent = JSON.parse(fs.readFileSync(path.join(ROOT, "config/agent.json"), "utf-8"));
|
|
82
|
+
} catch {
|
|
83
|
+
_agent = { firstName: "Agent", slackMemberId: "", fullName: "Agent", principal: {} };
|
|
84
|
+
}
|
|
85
|
+
return _agent;
|
|
86
|
+
}
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
const AGENT_USER_ID = loadAgent().slackMemberId || "";
|
|
89
|
+
const PRINCIPAL_USER_ID = loadAgent().principal?.slackMemberId || "";
|
|
90
|
+
// DM channel between the principal and the agent — resolved at runtime from
|
|
91
|
+
// Slack API on first DM event. The hardcoded MEHRAN_DM_CHANNEL constant was
|
|
92
|
+
// removed; the server detects DM channels via the Slack channel ID prefix
|
|
93
|
+
// (channels starting with "D" are IM channels).
|
|
94
|
+
|
|
95
|
+
// Known users for name/privilege resolution — built from agent.json so each
|
|
96
|
+
// agent's principal gets `privilege: ceo` and the agent itself gets `system`.
|
|
97
|
+
const KNOWN_USERS = (() => {
|
|
98
|
+
const a = loadAgent();
|
|
99
|
+
const map = {};
|
|
100
|
+
if (a.principal?.slackMemberId) {
|
|
101
|
+
map[a.principal.slackMemberId] = {
|
|
102
|
+
name: (a.principal.fullName || "").toLowerCase().replace(/\s+/g, "-"),
|
|
103
|
+
privilege: "ceo",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (a.slackMemberId) {
|
|
107
|
+
map[a.slackMemberId] = {
|
|
108
|
+
name: (a.fullName || "").toLowerCase().replace(/\s+/g, "-"),
|
|
109
|
+
privilege: "system",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return map;
|
|
113
|
+
})();
|
|
84
114
|
|
|
85
115
|
// Monitored channels (matches poller/slack-poller.mjs)
|
|
86
116
|
const CHANNEL_INFO = {
|
|
@@ -385,7 +415,7 @@ priority_signals:
|
|
|
385
415
|
from_ceo: ${item.priority_signals?.from_ceo || false}
|
|
386
416
|
tagged_urgent: ${item.priority_signals?.tagged_urgent || false}
|
|
387
417
|
contains_deadline: ${item.priority_signals?.contains_deadline || false}
|
|
388
|
-
|
|
418
|
+
mentions_agent: ${item.priority_signals?.mentions_agent || false}
|
|
389
419
|
raw_ref: "${item.raw_ref || ""}"
|
|
390
420
|
source: "events-api"
|
|
391
421
|
`;
|
|
@@ -446,20 +476,24 @@ content: |
|
|
|
446
476
|
*/
|
|
447
477
|
function handleMessageEvent(event) {
|
|
448
478
|
const userId = event.user || "";
|
|
449
|
-
// Skip
|
|
479
|
+
// Skip the agent's own messages — defensive check (outer dispatch also filters,
|
|
450
480
|
// but message_changed normalization can bypass it)
|
|
451
|
-
if (userId ===
|
|
481
|
+
if (userId === AGENT_USER_ID) return;
|
|
452
482
|
|
|
453
483
|
const msgText = event.text || "";
|
|
454
484
|
const channelId = event.channel || "";
|
|
455
485
|
const channelType = event.channel_type || "";
|
|
456
486
|
const isThread = !!event.thread_ts && event.thread_ts !== event.ts;
|
|
457
487
|
const isDM = channelType === "im";
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
488
|
+
const isPrincipal = userId === PRINCIPAL_USER_ID;
|
|
489
|
+
// The principal's DM channel is whichever IM channel they happen to use to
|
|
490
|
+
// contact the agent — Slack assigns DM channel IDs dynamically. Detect
|
|
491
|
+
// principal-DM context from `isPrincipal && isDM` rather than a hardcode.
|
|
492
|
+
const isPrincipalDmChannel = isPrincipal && isDM;
|
|
493
|
+
const agentFirstName = (loadAgent().firstName || "").toLowerCase();
|
|
494
|
+
const mentionsAgent =
|
|
495
|
+
(agentFirstName && msgText.toLowerCase().includes(agentFirstName)) ||
|
|
496
|
+
(AGENT_USER_ID && msgText.includes(`<@${AGENT_USER_ID}>`));
|
|
463
497
|
const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
|
|
464
498
|
msgText,
|
|
465
499
|
);
|
|
@@ -482,8 +516,8 @@ function handleMessageEvent(event) {
|
|
|
482
516
|
channel: channelName,
|
|
483
517
|
isDM,
|
|
484
518
|
isThread,
|
|
485
|
-
|
|
486
|
-
|
|
519
|
+
isPrincipal,
|
|
520
|
+
mentionsAgent,
|
|
487
521
|
isUrgent,
|
|
488
522
|
});
|
|
489
523
|
|
|
@@ -505,10 +539,10 @@ function handleMessageEvent(event) {
|
|
|
505
539
|
is_reply: isThread,
|
|
506
540
|
attachments: attachments.length > 0 ? attachments : undefined,
|
|
507
541
|
priority_signals: {
|
|
508
|
-
from_ceo:
|
|
542
|
+
from_ceo: isPrincipal,
|
|
509
543
|
tagged_urgent: isUrgent,
|
|
510
544
|
contains_deadline: false,
|
|
511
|
-
|
|
545
|
+
mentions_agent: mentionsAgent || isDM,
|
|
512
546
|
},
|
|
513
547
|
raw_ref: `slack:${channelId}:${msgTs}`,
|
|
514
548
|
_eventType: isThread ? "thread_reply" : isDM ? "dm" : "channel_message",
|
|
@@ -520,25 +554,25 @@ function handleMessageEvent(event) {
|
|
|
520
554
|
markInPollerTracker(msgTs);
|
|
521
555
|
|
|
522
556
|
// Write priority trigger for CEO DMs, @mentions, and urgent messages
|
|
523
|
-
if (
|
|
557
|
+
if (isPrincipal && (isDM || isPrincipalDmChannel)) {
|
|
524
558
|
writePriorityTrigger(event, "CEO DM via Events API");
|
|
525
|
-
} else if (
|
|
559
|
+
} else if (isPrincipal && !isDM) {
|
|
526
560
|
writePriorityTrigger(event, "CEO channel message via Events API");
|
|
527
|
-
} else if (
|
|
528
|
-
writePriorityTrigger(event, "
|
|
561
|
+
} else if (mentionsAgent) {
|
|
562
|
+
writePriorityTrigger(event, "Agent @mentioned via Events API");
|
|
529
563
|
} else if (isUrgent) {
|
|
530
564
|
writePriorityTrigger(event, "Urgent message via Events API");
|
|
531
565
|
}
|
|
532
566
|
}
|
|
533
567
|
|
|
534
568
|
/**
|
|
535
|
-
* Process an app_mention event (
|
|
569
|
+
* Process an app_mention event (@-mention of the running agent).
|
|
536
570
|
*/
|
|
537
571
|
function handleMentionEvent(event) {
|
|
538
572
|
const msgText = event.text || "";
|
|
539
573
|
const userId = event.user || "";
|
|
540
574
|
const channelId = event.channel || "";
|
|
541
|
-
const
|
|
575
|
+
const isPrincipal = userId === PRINCIPAL_USER_ID;
|
|
542
576
|
const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
|
|
543
577
|
msgText,
|
|
544
578
|
);
|
|
@@ -557,15 +591,15 @@ function handleMentionEvent(event) {
|
|
|
557
591
|
sender: resolveName(userId),
|
|
558
592
|
sender_privilege: resolvePrivilege(userId),
|
|
559
593
|
timestamp: new Date(parseFloat(msgTs) * 1000).toISOString(),
|
|
560
|
-
subject:
|
|
594
|
+
subject: `@${loadAgent().firstName || "Agent"} in #${channelName}`,
|
|
561
595
|
content: msgText,
|
|
562
596
|
thread_id: event.thread_ts || "",
|
|
563
597
|
is_reply: !!event.thread_ts && event.thread_ts !== event.ts,
|
|
564
598
|
priority_signals: {
|
|
565
|
-
from_ceo:
|
|
599
|
+
from_ceo: isPrincipal,
|
|
566
600
|
tagged_urgent: isUrgent,
|
|
567
601
|
contains_deadline: false,
|
|
568
|
-
|
|
602
|
+
mentions_agent: true,
|
|
569
603
|
},
|
|
570
604
|
raw_ref: `slack:${channelId}:${msgTs}`,
|
|
571
605
|
_eventType: "mention",
|
|
@@ -577,9 +611,9 @@ function handleMentionEvent(event) {
|
|
|
577
611
|
// Mentions always get a priority trigger
|
|
578
612
|
writePriorityTrigger(
|
|
579
613
|
event,
|
|
580
|
-
|
|
614
|
+
isPrincipal
|
|
581
615
|
? "CEO @mention via Events API"
|
|
582
|
-
: "
|
|
616
|
+
: "Agent @mentioned via Events API",
|
|
583
617
|
);
|
|
584
618
|
}
|
|
585
619
|
|
|
@@ -591,7 +625,7 @@ function handleReactionEvent(event) {
|
|
|
591
625
|
const reaction = event.reaction || "";
|
|
592
626
|
const itemTs = event.item?.ts || "";
|
|
593
627
|
const itemChannel = event.item?.channel || "";
|
|
594
|
-
const
|
|
628
|
+
const isPrincipal = userId === PRINCIPAL_USER_ID;
|
|
595
629
|
const channelName = resolveChannelName(itemChannel, "channel");
|
|
596
630
|
|
|
597
631
|
log("INFO", `Reaction event: :${reaction}: from ${resolveName(userId)}`, {
|
|
@@ -612,10 +646,10 @@ function handleReactionEvent(event) {
|
|
|
612
646
|
thread_id: "",
|
|
613
647
|
is_reply: false,
|
|
614
648
|
priority_signals: {
|
|
615
|
-
from_ceo:
|
|
649
|
+
from_ceo: isPrincipal,
|
|
616
650
|
tagged_urgent: false,
|
|
617
651
|
contains_deadline: false,
|
|
618
|
-
|
|
652
|
+
mentions_agent: false,
|
|
619
653
|
},
|
|
620
654
|
raw_ref: `slack:${itemChannel}:${itemTs}:reaction:${reaction}`,
|
|
621
655
|
_eventType: "reaction",
|
|
@@ -634,8 +668,8 @@ function handleHuddleEvent(event) {
|
|
|
634
668
|
const userId = event.user || "";
|
|
635
669
|
const huddleState = event.huddle_state || "";
|
|
636
670
|
|
|
637
|
-
// Don't react to
|
|
638
|
-
if (userId ===
|
|
671
|
+
// Don't react to the agent's own huddle changes
|
|
672
|
+
if (userId === AGENT_USER_ID) return;
|
|
639
673
|
|
|
640
674
|
const userName = resolveName(userId);
|
|
641
675
|
|
|
@@ -690,7 +724,7 @@ function handleEvent(payload) {
|
|
|
690
724
|
stats.events_received++;
|
|
691
725
|
stats.last_event_at = new Date().toISOString();
|
|
692
726
|
|
|
693
|
-
// Skip bot messages (including
|
|
727
|
+
// Skip bot messages (including the agent's own messages)
|
|
694
728
|
if (event.bot_id || event.subtype === "bot_message") {
|
|
695
729
|
stats.events_skipped_bot++;
|
|
696
730
|
log("DEBUG", `Bot message skipped`, {
|
|
@@ -701,8 +735,8 @@ function handleEvent(payload) {
|
|
|
701
735
|
return;
|
|
702
736
|
}
|
|
703
737
|
|
|
704
|
-
// Skip
|
|
705
|
-
if (event.user ===
|
|
738
|
+
// Skip the agent's own messages
|
|
739
|
+
if (event.user === AGENT_USER_ID) {
|
|
706
740
|
stats.events_skipped_bot++;
|
|
707
741
|
markProcessed(eventId);
|
|
708
742
|
return;
|
|
@@ -743,9 +777,9 @@ function handleEvent(payload) {
|
|
|
743
777
|
|
|
744
778
|
// Re-check after normalization: the original event.user for
|
|
745
779
|
// message_changed is often empty/system; the real author is in
|
|
746
|
-
// event.message.user. Without this,
|
|
747
|
-
// bypass the earlier
|
|
748
|
-
if (event.user ===
|
|
780
|
+
// event.message.user. Without this, the agent's own edited messages
|
|
781
|
+
// bypass the earlier AGENT_USER_ID filter and trigger self-replies.
|
|
782
|
+
if (event.user === AGENT_USER_ID) {
|
|
749
783
|
stats.events_skipped_bot++;
|
|
750
784
|
markProcessed(eventId);
|
|
751
785
|
return;
|
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
# → purge entries older than 24h
|
|
25
25
|
set -e
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
28
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
29
|
+
|
|
30
|
+
LOCK_BASE_DIR="$AGENT_REPO_DIR/state/locks/slack-response"
|
|
31
|
+
LEGACY_DIR="$AGENT_REPO_DIR/state/slack-responded"
|
|
29
32
|
LOCK_TTL_MINUTES=1440 # 24 hours — thread-level dedup needs long TTL to prevent cross-session duplicates
|
|
30
33
|
|
|
31
34
|
mkdir -p "$LOCK_BASE_DIR"
|