@adaptic/maestro 1.6.0 → 1.7.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/package.json +1 -1
- package/scripts/continuous-monitor.sh +11 -6
- package/scripts/daemon/context-compiler.mjs +8 -8
- package/scripts/daemon/health.mjs +2 -2
- package/scripts/daemon/maestro-daemon.mjs +4 -3
- package/scripts/email_thread_dedup.py +4 -3
- package/scripts/huddle/huddle-server.mjs +50 -29
- package/scripts/llm_email_dedup.py +23 -15
- package/scripts/local-triggers/generate-plists.sh +2 -1
- package/scripts/media-generation/README.md +1 -1
- package/scripts/outbound-dedup-cleanup.sh +4 -4
- package/scripts/outbound-dedup.sh +4 -3
- package/scripts/pdf-generation/README.md +1 -1
- package/scripts/pdf-generation/templates/memo.latex +1 -1
- package/scripts/poll-slack-events.sh +4 -2
- package/scripts/poller/imap-client.mjs +11 -10
- package/scripts/poller/index.mjs +6 -6
- package/scripts/poller/intra-session-check.mjs +35 -18
- package/scripts/poller/mehran-gmail-poller.mjs +63 -29
- package/scripts/poller/slack-poller.mjs +45 -31
- package/scripts/poller/trigger.mjs +22 -5
- package/scripts/pre-draft-context.py +2 -2
- package/scripts/rag-indexer.py +3 -3
- package/scripts/send-sms.sh +7 -7
- package/scripts/send-whatsapp.sh +11 -11
- package/scripts/setup/configure-macos.sh +4 -2
- package/scripts/setup/init-agent.sh +1 -1
- package/scripts/slack-react.mjs +1 -1
- package/scripts/slack-typing.mjs +3 -3
- package/scripts/system-verify.sh +28 -15
- package/scripts/user-context-search.py +4 -4
- package/scripts/validate-outbound.py +29 -18
- package/scripts/sophie-inbox-poller.py +0 -406
package/package.json
CHANGED
|
@@ -8,18 +8,23 @@
|
|
|
8
8
|
set -e
|
|
9
9
|
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
-
|
|
11
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(dirname "$SCRIPT_DIR")}"
|
|
12
|
+
# Load agent identity for the LLM prompt
|
|
13
|
+
if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
|
|
14
|
+
# shellcheck disable=SC1091
|
|
15
|
+
source "$AGENT_REPO_DIR/config/agent.env"
|
|
16
|
+
fi
|
|
12
17
|
CHANNEL="${1:---channel all}"
|
|
13
18
|
CHANNEL="${CHANNEL#--channel }"
|
|
14
|
-
LOG_DIR="$
|
|
15
|
-
QUEUE_DIR="$
|
|
19
|
+
LOG_DIR="$AGENT_REPO_DIR/logs/monitor"
|
|
20
|
+
QUEUE_DIR="$AGENT_REPO_DIR/outputs/inbound/queue"
|
|
16
21
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
17
22
|
|
|
18
23
|
mkdir -p "$LOG_DIR" "$QUEUE_DIR"
|
|
19
24
|
|
|
20
25
|
# Check emergency stop
|
|
21
26
|
check_stop() {
|
|
22
|
-
if [ -f "$
|
|
27
|
+
if [ -f "$AGENT_REPO_DIR/.emergency-stop" ]; then
|
|
23
28
|
echo "[$TIMESTAMP] Monitor halted: emergency stop active" >> "$LOG_DIR/monitor.log"
|
|
24
29
|
exit 0
|
|
25
30
|
fi
|
|
@@ -40,14 +45,14 @@ monitor_channel() {
|
|
|
40
45
|
|
|
41
46
|
# Use Claude to observe the channel via desktop control
|
|
42
47
|
# This invokes the appropriate operator agent in observe mode
|
|
43
|
-
claude -p "You are the $channel monitoring agent for
|
|
48
|
+
claude -p "You are the $channel monitoring agent for ${AGENT_FULL_NAME:-the agent} at ${COMPANY_NAME:-the company}.
|
|
44
49
|
OBSERVE ONLY — do not send any messages.
|
|
45
50
|
Check for new messages since the last check.
|
|
46
51
|
If there are new messages, write a YAML summary to: $QUEUE_DIR/${channel}-$(date +%s).yaml
|
|
47
52
|
Include: sender, timestamp, channel/thread, subject/preview, urgency_hint.
|
|
48
53
|
If no new messages, do nothing.
|
|
49
54
|
Check the desktop $channel app using screenshot observation.
|
|
50
|
-
Working directory: $
|
|
55
|
+
Working directory: $AGENT_REPO_DIR" \
|
|
51
56
|
--output-format text \
|
|
52
57
|
>> "$log_file" 2>&1 || true
|
|
53
58
|
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
import { readFileSync, readdirSync } from "fs";
|
|
15
15
|
import { join } from "path";
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
const SESSION_DIR = process.env.__TEST_SESSION_DIR || join(
|
|
17
|
+
const AGENT_REPO_DIR = process.env.__TEST_AGENT_DIR || process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
18
|
+
const SESSION_DIR = process.env.__TEST_SESSION_DIR || join(AGENT_REPO_DIR, "state", "sessions");
|
|
19
19
|
const CHARS_PER_TOKEN = 4;
|
|
20
20
|
|
|
21
21
|
// ── Token budgets by priority ──────────────────────────────────────
|
|
@@ -60,7 +60,7 @@ function extractChannelId(item) {
|
|
|
60
60
|
function loadUserProfile(senderSlug, budgetTokens) {
|
|
61
61
|
if (!senderSlug) return null;
|
|
62
62
|
try {
|
|
63
|
-
const filePath = join(
|
|
63
|
+
const filePath = join(AGENT_REPO_DIR, "memory", "profiles", "users", `${senderSlug}.yaml`);
|
|
64
64
|
let raw = readFileSync(filePath, "utf-8");
|
|
65
65
|
const charBudget = budgetTokens * CHARS_PER_TOKEN;
|
|
66
66
|
if (raw.length > charBudget) {
|
|
@@ -80,7 +80,7 @@ function loadUserProfile(senderSlug, budgetTokens) {
|
|
|
80
80
|
*/
|
|
81
81
|
function loadDomainDescriptions() {
|
|
82
82
|
try {
|
|
83
|
-
const filePath = join(
|
|
83
|
+
const filePath = join(AGENT_REPO_DIR, "policies", "information-barriers.yaml");
|
|
84
84
|
const raw = readFileSync(filePath, "utf-8");
|
|
85
85
|
const map = new Map();
|
|
86
86
|
const regex = /^\s{2}([\w-]+):\s*\n\s+description:\s*"(.+?)"/gm;
|
|
@@ -164,7 +164,7 @@ function loadDisclosureBoundaries(senderSlug, budgetTokens) {
|
|
|
164
164
|
|
|
165
165
|
if (senderSlug) {
|
|
166
166
|
try {
|
|
167
|
-
const filePath = join(
|
|
167
|
+
const filePath = join(AGENT_REPO_DIR, "memory", "profiles", "users", `${senderSlug}.yaml`);
|
|
168
168
|
const raw = readFileSync(filePath, "utf-8");
|
|
169
169
|
|
|
170
170
|
// Extract privilege_level via regex
|
|
@@ -226,10 +226,10 @@ function loadDisclosureBoundaries(senderSlug, budgetTokens) {
|
|
|
226
226
|
*/
|
|
227
227
|
function loadConversationHistory(senderSlug, channelId, budgetTokens, forbiddenDomains) {
|
|
228
228
|
const candidateDirs = [];
|
|
229
|
-
if (channelId) candidateDirs.push(join(
|
|
229
|
+
if (channelId) candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", channelId));
|
|
230
230
|
if (senderSlug) {
|
|
231
|
-
candidateDirs.push(join(
|
|
232
|
-
candidateDirs.push(join(
|
|
231
|
+
candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", `dm-${senderSlug}`));
|
|
232
|
+
candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", senderSlug));
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
const entries = [];
|
|
@@ -5,7 +5,7 @@ import { writeFileSync, mkdirSync } from "fs";
|
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { getStatus } from "./dispatcher.mjs";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
9
9
|
|
|
10
10
|
let stats = {
|
|
11
11
|
daemon_start: new Date().toISOString(),
|
|
@@ -51,7 +51,7 @@ export function writeHealthDashboard() {
|
|
|
51
51
|
dispatcher,
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
const dir = join(
|
|
54
|
+
const dir = join(AGENT_REPO_DIR, "state", "dashboards");
|
|
55
55
|
mkdirSync(dir, { recursive: true });
|
|
56
56
|
const path = join(dir, "daemon-health.yaml");
|
|
57
57
|
|
|
@@ -10,15 +10,16 @@
|
|
|
10
10
|
// Install: launchd plist with KeepAlive: true
|
|
11
11
|
// =============================================================================
|
|
12
12
|
|
|
13
|
-
// The core daemon
|
|
13
|
+
// The core daemon implementation lives in sophie-daemon.mjs (filename
|
|
14
|
+
// preserved for launchd-plist compatibility; the content is fully
|
|
15
|
+
// agent-neutral and reads identity from config/agent.json).
|
|
14
16
|
// It uses AGENT_DIR (resolved from this file's location) as the base path.
|
|
15
|
-
// The init-maestro wizard renames sophie references in that file.
|
|
16
17
|
|
|
17
18
|
import { resolve, dirname } from "node:path";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
20
|
|
|
20
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const AGENT_DIR = resolve(__dirname, "../..");
|
|
22
|
+
const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
|
|
22
23
|
|
|
23
24
|
// Set AGENT_DIR as env var so all modules can use it
|
|
24
25
|
process.env.AGENT_DIR = AGENT_DIR;
|
|
@@ -34,9 +34,10 @@ import sys
|
|
|
34
34
|
import time
|
|
35
35
|
from datetime import datetime, timezone
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
AGENT_REPO_DIR = os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent))
|
|
39
|
+
LOCK_DIR = os.path.join(AGENT_REPO_DIR, "state", "locks", "outbound", "email-threads")
|
|
40
|
+
AUDIT_DIR = os.path.join(AGENT_REPO_DIR, "logs", "audit")
|
|
40
41
|
TTL_SECONDS = 24 * 60 * 60 # 24 hours
|
|
41
42
|
|
|
42
43
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Agent Huddle — Main Server
|
|
4
4
|
*
|
|
5
5
|
* Orchestrates Slack huddle voice participation by coordinating:
|
|
6
6
|
* - HuddleController (CDP-based Slack UI automation)
|
|
@@ -24,7 +24,7 @@ import dotenv from "dotenv";
|
|
|
24
24
|
import { fileURLToPath as _fu } from "node:url";
|
|
25
25
|
import { dirname as _dn, join as _jn } from "node:path";
|
|
26
26
|
|
|
27
|
-
// Load .env from
|
|
27
|
+
// Load .env from agent repo root (where all API keys live), then huddle-specific .env
|
|
28
28
|
const _d = _dn(_fu(import.meta.url));
|
|
29
29
|
dotenv.config({ path: _jn(_d, "../..", ".env") });
|
|
30
30
|
dotenv.config({ path: _jn(_d, ".env"), override: true });
|
|
@@ -42,8 +42,21 @@ import { HuddleController } from "./huddle-controller.mjs";
|
|
|
42
42
|
import { AudioBridge } from "./audio-bridge.mjs";
|
|
43
43
|
|
|
44
44
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
45
|
-
const
|
|
46
|
-
const VOICE_AI_ROOT = join(process.env.HOME || "
|
|
45
|
+
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(__dirname, "../..");
|
|
46
|
+
const VOICE_AI_ROOT = process.env.VOICE_AI_ROOT || join(process.env.HOME || "", "voice-ai");
|
|
47
|
+
|
|
48
|
+
// Load agent identity for transcript speaker tags + display names.
|
|
49
|
+
let _agent = null;
|
|
50
|
+
function loadAgent() {
|
|
51
|
+
if (_agent) return _agent;
|
|
52
|
+
try {
|
|
53
|
+
_agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
54
|
+
} catch {
|
|
55
|
+
_agent = { firstName: "Agent" };
|
|
56
|
+
}
|
|
57
|
+
return _agent;
|
|
58
|
+
}
|
|
59
|
+
const AGENT_SLUG = (loadAgent().firstName || "agent").toLowerCase();
|
|
47
60
|
|
|
48
61
|
// ---------------------------------------------------------------------------
|
|
49
62
|
// Configuration
|
|
@@ -90,7 +103,7 @@ const HuddleState = {
|
|
|
90
103
|
};
|
|
91
104
|
|
|
92
105
|
/**
|
|
93
|
-
* HuddleServer is the top-level orchestrator for
|
|
106
|
+
* HuddleServer is the top-level orchestrator for the agent's huddle participation.
|
|
94
107
|
*/
|
|
95
108
|
class HuddleServer extends EventEmitter {
|
|
96
109
|
constructor() {
|
|
@@ -114,7 +127,7 @@ class HuddleServer extends EventEmitter {
|
|
|
114
127
|
|
|
115
128
|
async start() {
|
|
116
129
|
console.log("\n==========================================");
|
|
117
|
-
console.log(
|
|
130
|
+
console.log(` ${loadAgent().firstName || "Agent"} Huddle Server`);
|
|
118
131
|
console.log(" " + new Date().toISOString());
|
|
119
132
|
console.log("==========================================\n");
|
|
120
133
|
|
|
@@ -265,7 +278,7 @@ class HuddleServer extends EventEmitter {
|
|
|
265
278
|
lastThreadTs: null, // Last seen thread message timestamp
|
|
266
279
|
};
|
|
267
280
|
|
|
268
|
-
// Load live context from
|
|
281
|
+
// Load live context from the agent repo for Claude
|
|
269
282
|
this.session.liveContext = this._loadLiveContext();
|
|
270
283
|
|
|
271
284
|
// In webaudio capture mode, set Slack's speaker to physical output
|
|
@@ -289,7 +302,7 @@ class HuddleServer extends EventEmitter {
|
|
|
289
302
|
await this.bridge.speak(greeting);
|
|
290
303
|
if (this.session) {
|
|
291
304
|
this.session.messages.push({ role: "assistant", content: greeting });
|
|
292
|
-
this.session.transcript.push({ speaker:
|
|
305
|
+
this.session.transcript.push({ speaker: AGENT_SLUG, text: greeting, ts: Date.now() });
|
|
293
306
|
}
|
|
294
307
|
|
|
295
308
|
// Start polling the huddle thread for text messages
|
|
@@ -329,7 +342,7 @@ class HuddleServer extends EventEmitter {
|
|
|
329
342
|
// -------------------------------------------------------------------------
|
|
330
343
|
|
|
331
344
|
/**
|
|
332
|
-
* Process a transcript from the huddle and generate
|
|
345
|
+
* Process a transcript from the huddle and generate the agent's response.
|
|
333
346
|
* @param {string} userText - What someone said in the huddle
|
|
334
347
|
*/
|
|
335
348
|
async _handleTranscript(userText) {
|
|
@@ -356,7 +369,7 @@ class HuddleServer extends EventEmitter {
|
|
|
356
369
|
|
|
357
370
|
// Add to history
|
|
358
371
|
this.session.messages.push({ role: "assistant", content: cleanResponse });
|
|
359
|
-
this.session.transcript.push({ speaker:
|
|
372
|
+
this.session.transcript.push({ speaker: AGENT_SLUG, text: cleanResponse, ts: Date.now() });
|
|
360
373
|
|
|
361
374
|
// Speak the response via TTS → BlackHole 16ch → Slack mic
|
|
362
375
|
await this.bridge.speak(cleanResponse);
|
|
@@ -380,7 +393,7 @@ class HuddleServer extends EventEmitter {
|
|
|
380
393
|
/**
|
|
381
394
|
* Start polling the huddle thread for text messages via the Slack API.
|
|
382
395
|
* Text messages in the huddle thread are injected into the conversation
|
|
383
|
-
* so
|
|
396
|
+
* so the agent can respond to both voice and text.
|
|
384
397
|
*/
|
|
385
398
|
_startThreadPolling() {
|
|
386
399
|
const slackToken = process.env.SLACK_USER_TOKEN || process.env.SLACK_BOT_TOKEN;
|
|
@@ -431,7 +444,7 @@ class HuddleServer extends EventEmitter {
|
|
|
431
444
|
|
|
432
445
|
/**
|
|
433
446
|
* Poll the Slack channel for recent messages in the huddle thread.
|
|
434
|
-
* Only processes text messages from other users (not
|
|
447
|
+
* Only processes text messages from other users (not the agent's bot).
|
|
435
448
|
*/
|
|
436
449
|
async _pollThreadMessages(channelId, token) {
|
|
437
450
|
if (!this.session) return;
|
|
@@ -445,12 +458,14 @@ class HuddleServer extends EventEmitter {
|
|
|
445
458
|
const data = await resp.json();
|
|
446
459
|
if (!data.ok || !data.messages || data.messages.length === 0) return;
|
|
447
460
|
|
|
448
|
-
//
|
|
449
|
-
|
|
461
|
+
// The agent's Slack user ID (skip its own messages). Hardcoded fallback
|
|
462
|
+
// removed — agents must set SLACK_BOT_USER_ID in .env if they want this
|
|
463
|
+
// filter active, otherwise the agent's slackMemberId from agent.json is used.
|
|
464
|
+
const agentUserId = process.env.SLACK_BOT_USER_ID || loadAgent().slackMemberId || "";
|
|
450
465
|
|
|
451
466
|
for (const msg of data.messages.reverse()) { // oldest first
|
|
452
|
-
// Skip bot messages,
|
|
453
|
-
if (msg.user ===
|
|
467
|
+
// Skip bot messages, the agent's own messages, and non-text messages
|
|
468
|
+
if (agentUserId && msg.user === agentUserId) continue;
|
|
454
469
|
if (msg.subtype) continue; // Skip system messages
|
|
455
470
|
if (!msg.text || !msg.text.trim()) continue;
|
|
456
471
|
|
|
@@ -495,7 +510,7 @@ class HuddleServer extends EventEmitter {
|
|
|
495
510
|
// Build a single prompt from system + conversation history
|
|
496
511
|
const parts = [`[System]\n${systemPrompt}\n`];
|
|
497
512
|
for (const msg of messages) {
|
|
498
|
-
const role = msg.role === "assistant" ? "
|
|
513
|
+
const role = msg.role === "assistant" ? (loadAgent().firstName || "Agent") : "Participant";
|
|
499
514
|
const content = typeof msg.content === "string"
|
|
500
515
|
? msg.content
|
|
501
516
|
: msg.content?.map((b) => b.text || "").join("") || "";
|
|
@@ -503,7 +518,7 @@ class HuddleServer extends EventEmitter {
|
|
|
503
518
|
parts.push(`[${role}]\n${content}`);
|
|
504
519
|
}
|
|
505
520
|
}
|
|
506
|
-
parts.push("
|
|
521
|
+
parts.push(`[${loadAgent().firstName || "Agent"}]`);
|
|
507
522
|
|
|
508
523
|
const prompt = parts.join("\n\n");
|
|
509
524
|
|
|
@@ -568,7 +583,7 @@ class HuddleServer extends EventEmitter {
|
|
|
568
583
|
if (filler) {
|
|
569
584
|
await this.bridge.speak(filler);
|
|
570
585
|
if (this.session) {
|
|
571
|
-
this.session.transcript.push({ speaker:
|
|
586
|
+
this.session.transcript.push({ speaker: AGENT_SLUG, text: filler, ts: Date.now() });
|
|
572
587
|
}
|
|
573
588
|
}
|
|
574
589
|
}
|
|
@@ -616,7 +631,7 @@ class HuddleServer extends EventEmitter {
|
|
|
616
631
|
// -------------------------------------------------------------------------
|
|
617
632
|
|
|
618
633
|
/**
|
|
619
|
-
* Resolve a huddle participant's identity from
|
|
634
|
+
* Resolve a huddle participant's identity from the agent repo contacts/profiles.
|
|
620
635
|
* Used for access control (CEO gets write tools) and personalized greetings.
|
|
621
636
|
*/
|
|
622
637
|
_resolveHuddleParticipant(identifier) {
|
|
@@ -636,7 +651,7 @@ class HuddleServer extends EventEmitter {
|
|
|
636
651
|
|
|
637
652
|
// Try loading from contacts.yaml
|
|
638
653
|
try {
|
|
639
|
-
const contactsPath = join(
|
|
654
|
+
const contactsPath = join(AGENT_REPO_DIR, "config/contacts.yaml");
|
|
640
655
|
const contacts = readFileSync(contactsPath, "utf8");
|
|
641
656
|
// Simple search — look for the identifier in the contacts file
|
|
642
657
|
if (contacts.toLowerCase().includes(key)) {
|
|
@@ -695,7 +710,7 @@ class HuddleServer extends EventEmitter {
|
|
|
695
710
|
console.log(`[EVENTS] Huddle event: ${user_name || user} → ${huddle_state} in ${channel || "unknown"}`);
|
|
696
711
|
this._logEvent("events-api-huddle", event);
|
|
697
712
|
|
|
698
|
-
// Someone started a huddle and
|
|
713
|
+
// Someone started a huddle and the agent isn't in one yet
|
|
699
714
|
if (huddle_state === "in_a_huddle" && this.state === HuddleState.IDLE && this.autoJoin) {
|
|
700
715
|
// Determine the channel to join
|
|
701
716
|
const targetChannel = channel || user_name;
|
|
@@ -719,7 +734,7 @@ class HuddleServer extends EventEmitter {
|
|
|
719
734
|
}
|
|
720
735
|
|
|
721
736
|
// -------------------------------------------------------------------------
|
|
722
|
-
// Live context loader (reads from
|
|
737
|
+
// Live context loader (reads from the agent repo state files)
|
|
723
738
|
// -------------------------------------------------------------------------
|
|
724
739
|
|
|
725
740
|
_loadLiveContext() {
|
|
@@ -727,13 +742,13 @@ class HuddleServer extends EventEmitter {
|
|
|
727
742
|
|
|
728
743
|
// Load priorities
|
|
729
744
|
try {
|
|
730
|
-
const priorities = readFileSync(join(
|
|
745
|
+
const priorities = readFileSync(join(AGENT_REPO_DIR, "config/priorities.yaml"), "utf8");
|
|
731
746
|
contextParts.push(`PRIORITIES:\n${priorities.slice(0, 2000)}`);
|
|
732
747
|
} catch { /* optional */ }
|
|
733
748
|
|
|
734
749
|
// Load executive summary
|
|
735
750
|
try {
|
|
736
|
-
const summary = readFileSync(join(
|
|
751
|
+
const summary = readFileSync(join(AGENT_REPO_DIR, "state/dashboards/executive-summary.yaml"), "utf8");
|
|
737
752
|
contextParts.push(`EXECUTIVE SUMMARY:\n${summary.slice(0, 3000)}`);
|
|
738
753
|
} catch { /* optional */ }
|
|
739
754
|
|
|
@@ -835,7 +850,7 @@ class HuddleServer extends EventEmitter {
|
|
|
835
850
|
// -------------------------------------------------------------------------
|
|
836
851
|
|
|
837
852
|
/**
|
|
838
|
-
* Toggle
|
|
853
|
+
* Toggle the agent's Slack mic mute via CDP on the huddle window.
|
|
839
854
|
* Keeps mic muted between TTS outputs to prevent echo from the
|
|
840
855
|
* virtual audio loopback.
|
|
841
856
|
* @param {boolean} muted - true to mute, false to unmute
|
|
@@ -946,7 +961,7 @@ class HuddleServer extends EventEmitter {
|
|
|
946
961
|
// -------------------------------------------------------------------------
|
|
947
962
|
|
|
948
963
|
_logEvent(event, data = {}) {
|
|
949
|
-
const logDir = join(
|
|
964
|
+
const logDir = join(AGENT_REPO_DIR, "logs/huddle");
|
|
950
965
|
mkdirSync(logDir, { recursive: true });
|
|
951
966
|
|
|
952
967
|
const date = new Date().toISOString().split("T")[0];
|
|
@@ -964,7 +979,7 @@ class HuddleServer extends EventEmitter {
|
|
|
964
979
|
_saveTranscript() {
|
|
965
980
|
if (!this.session || this.session.transcript.length === 0) return;
|
|
966
981
|
|
|
967
|
-
const logDir = join(
|
|
982
|
+
const logDir = join(AGENT_REPO_DIR, "logs/huddle");
|
|
968
983
|
mkdirSync(logDir, { recursive: true });
|
|
969
984
|
|
|
970
985
|
const date = new Date().toISOString().split("T")[0];
|
|
@@ -984,7 +999,10 @@ class HuddleServer extends EventEmitter {
|
|
|
984
999
|
// Huddle-specific system prompt
|
|
985
1000
|
// ---------------------------------------------------------------------------
|
|
986
1001
|
|
|
987
|
-
|
|
1002
|
+
function buildHuddleSystemPrompt() {
|
|
1003
|
+
const a = loadAgent();
|
|
1004
|
+
const principal = a.principal || {};
|
|
1005
|
+
return `You are ${a.fullName}, ${a.title} to ${principal.fullName || "the principal"}, ${principal.title || "the principal"} of ${a.company}.
|
|
988
1006
|
You are participating in a Slack huddle — a real-time voice conversation.
|
|
989
1007
|
|
|
990
1008
|
PERSONALITY:
|
|
@@ -1026,6 +1044,9 @@ CRITICAL — DO NOT REPEAT YOURSELF:
|
|
|
1026
1044
|
- Never re-introduce yourself after the initial greeting
|
|
1027
1045
|
- Each response should add new information or move the conversation forward
|
|
1028
1046
|
- If you already answered a question, do not repeat the answer`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const HUDDLE_SYSTEM_PROMPT = buildHuddleSystemPrompt();
|
|
1029
1050
|
|
|
1030
1051
|
// ---------------------------------------------------------------------------
|
|
1031
1052
|
// Main
|
|
@@ -35,22 +35,30 @@ import time
|
|
|
35
35
|
from datetime import datetime, timedelta, timezone
|
|
36
36
|
from email.utils import parsedate_to_datetime
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
import json as _json
|
|
40
|
+
AGENT_REPO_DIR = os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent))
|
|
41
|
+
AUDIT_DIR = os.path.join(AGENT_REPO_DIR, "logs", "audit")
|
|
40
42
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
# Load agent identity (canonical SOT)
|
|
44
|
+
try:
|
|
45
|
+
with open(os.path.join(AGENT_REPO_DIR, "config", "agent.json")) as _f:
|
|
46
|
+
_AGENT = _json.load(_f)
|
|
47
|
+
except Exception:
|
|
48
|
+
_AGENT = {"email": "agent@example.com", "firstName": "agent"}
|
|
49
|
+
|
|
50
|
+
AGENT_EMAIL = _AGENT.get("email", "agent@example.com")
|
|
51
|
+
AGENT_GMAIL_PASSWORD = os.environ.get("GMAIL_APP_PASSWORD", "")
|
|
44
52
|
|
|
45
53
|
# Load from .env if not in environment
|
|
46
|
-
if not
|
|
47
|
-
_env_path = os.path.join(
|
|
54
|
+
if not AGENT_GMAIL_PASSWORD:
|
|
55
|
+
_env_path = os.path.join(AGENT_REPO_DIR, ".env")
|
|
48
56
|
if os.path.exists(_env_path):
|
|
49
57
|
with open(_env_path) as _f:
|
|
50
58
|
for _line in _f:
|
|
51
59
|
_line = _line.strip()
|
|
52
60
|
if _line.startswith("GMAIL_APP_PASSWORD="):
|
|
53
|
-
|
|
61
|
+
AGENT_GMAIL_PASSWORD = _line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
54
62
|
break
|
|
55
63
|
|
|
56
64
|
# Anthropic API key
|
|
@@ -112,9 +120,9 @@ def fetch_recent_thread_messages(to: str, subject: str, sender_email: str = "",
|
|
|
112
120
|
List of message dicts sorted by date (newest first), or empty list on error.
|
|
113
121
|
"""
|
|
114
122
|
if not sender_email:
|
|
115
|
-
sender_email =
|
|
123
|
+
sender_email = AGENT_EMAIL
|
|
116
124
|
if not sender_password:
|
|
117
|
-
sender_password =
|
|
125
|
+
sender_password = AGENT_GMAIL_PASSWORD
|
|
118
126
|
|
|
119
127
|
if not sender_email or not sender_password:
|
|
120
128
|
return []
|
|
@@ -283,7 +291,7 @@ def llm_dedup_check(to: str, subject: str, body: str,
|
|
|
283
291
|
to: Recipient email address
|
|
284
292
|
subject: Proposed email subject
|
|
285
293
|
body: Proposed email body
|
|
286
|
-
sender_email: Sender's Gmail address (default:
|
|
294
|
+
sender_email: Sender's Gmail address (default: agent.email from config/agent.json)
|
|
287
295
|
sender_password: Sender's app password (default: from env/config)
|
|
288
296
|
|
|
289
297
|
Returns:
|
|
@@ -298,8 +306,8 @@ def llm_dedup_check(to: str, subject: str, body: str,
|
|
|
298
306
|
# Fetch recent conversation history
|
|
299
307
|
recent = fetch_recent_thread_messages(
|
|
300
308
|
to, subject,
|
|
301
|
-
sender_email=sender_email or
|
|
302
|
-
sender_password=sender_password or
|
|
309
|
+
sender_email=sender_email or AGENT_EMAIL,
|
|
310
|
+
sender_password=sender_password or AGENT_GMAIL_PASSWORD,
|
|
303
311
|
)
|
|
304
312
|
|
|
305
313
|
if not recent:
|
|
@@ -315,7 +323,7 @@ def llm_dedup_check(to: str, subject: str, body: str,
|
|
|
315
323
|
# Build context for LLM assessment
|
|
316
324
|
conversation_summary = []
|
|
317
325
|
for i, msg in enumerate(recent):
|
|
318
|
-
direction = "SENT" if
|
|
326
|
+
direction = "SENT" if AGENT_EMAIL.lower() in msg.get("from", "").lower() or (sender_email and sender_email.lower() in msg.get("from", "").lower()) else "RECEIVED"
|
|
319
327
|
conversation_summary.append(
|
|
320
328
|
f"[{i+1}] {direction} | Date: {msg['date']} | Subject: {msg['subject']}\n"
|
|
321
329
|
f" Preview: {msg['body_preview'][:300]}"
|
|
@@ -404,7 +412,7 @@ def main():
|
|
|
404
412
|
parser.add_argument("to", help="Recipient email address")
|
|
405
413
|
parser.add_argument("subject", help="Proposed email subject")
|
|
406
414
|
parser.add_argument("body", help="Proposed email body")
|
|
407
|
-
parser.add_argument("--sender-email", default="", help="Sender email (default:
|
|
415
|
+
parser.add_argument("--sender-email", default="", help="Sender email (default: agent.email from config/agent.json)")
|
|
408
416
|
parser.add_argument("--sender-password", default="", help="Sender app password (default: from env)")
|
|
409
417
|
parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output")
|
|
410
418
|
|
|
@@ -40,7 +40,8 @@ echo "[generate-plists] User: $AGENT_USER"
|
|
|
40
40
|
mkdir -p "$PLIST_DIR"
|
|
41
41
|
|
|
42
42
|
# Clear old plists before generating — prevents stale plists from a previous
|
|
43
|
-
# agent identity (e.g.
|
|
43
|
+
# agent identity (e.g. an old AGENT_LAUNCHD_PREFIX) from being installed
|
|
44
|
+
# alongside new ones
|
|
44
45
|
rm -f "$PLIST_DIR"/*.plist 2>/dev/null
|
|
45
46
|
|
|
46
47
|
# ── Helper: generate a single plist ──────────────────────────────────────────
|
|
@@ -9,7 +9,7 @@ Ported from `~/adapticai/app/scripts/media-generation/` and `~/ai-born/`.
|
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
# Install dependencies
|
|
12
|
-
cd
|
|
12
|
+
cd ~/<agent-name> && npm install
|
|
13
13
|
|
|
14
14
|
# Set API key in .env or config/environment.yaml
|
|
15
15
|
echo "GEMINI_API_KEY=your-key-here" >> .env
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
# Also cleans the legacy Slack locks in state/locks/slack-response/
|
|
6
6
|
#
|
|
7
7
|
# Intended to be run by launchd or cron every 5 minutes:
|
|
8
|
-
# */5 * * * *
|
|
8
|
+
# */5 * * * * <AGENT_REPO_DIR>/scripts/outbound-dedup-cleanup.sh
|
|
9
9
|
#
|
|
10
10
|
# Or via launchd plist with StartInterval: 300
|
|
11
11
|
set -e
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
LOG_DIR="$
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
15
|
+
LOG_DIR="$AGENT_REPO_DIR/logs/audit"
|
|
16
16
|
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
17
17
|
|
|
18
18
|
mkdir -p "$LOG_DIR"
|
|
@@ -44,10 +44,11 @@
|
|
|
44
44
|
# - Full audit trail to logs/audit/
|
|
45
45
|
set -e
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
SCRIPT_DIR_INIT="$(cd "$(dirname "$0")" && pwd)"
|
|
48
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR_INIT/.." && pwd)}"
|
|
49
|
+
LOCK_BASE_DIR="$AGENT_REPO_DIR/state/locks/outbound"
|
|
49
50
|
LOCK_TTL_MINUTES=720 # 12 hours — email dedup needs long TTL to prevent cross-session duplicates
|
|
50
|
-
AUDIT_DIR="$
|
|
51
|
+
AUDIT_DIR="$AGENT_REPO_DIR/logs/audit"
|
|
51
52
|
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
52
53
|
|
|
53
54
|
# Ensure base directories exist
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
\renewcommand{\footrulewidth}{0.3pt}
|
|
50
50
|
\fancyhead[L]{\includegraphics[height=10mm]{public/assets/adaptic-icon-dark.png}}
|
|
51
51
|
\fancyhead[R]{\footnotesize\sffamily\color{adaptic-text-secondary} Internal — $if(title)$$title$$endif$}
|
|
52
|
-
\fancyfoot[L]{\footnotesize\color{adaptic-text-secondary} Prepared by
|
|
52
|
+
\fancyfoot[L]{\footnotesize\color{adaptic-text-secondary} $if(author)$Prepared by $author$$endif$}
|
|
53
53
|
\fancyfoot[R]{\footnotesize\color{adaptic-text-secondary} \thepage}
|
|
54
54
|
|
|
55
55
|
% ─── Links ───
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
# Run via cron every 5-10 seconds for near-instant response
|
|
5
5
|
set -e
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
9
|
+
EVENTS_URL="${SLACK_EVENTS_RELAY_URL:-https://slack-events-server-production.up.railway.app/events}"
|
|
10
|
+
INBOX_DIR="$AGENT_REPO_DIR/state/inbox/slack"
|
|
9
11
|
mkdir -p "$INBOX_DIR"
|
|
10
12
|
|
|
11
13
|
RESPONSE=$(curl -s --max-time 5 "$EVENTS_URL" 2>/dev/null)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// imap-client.mjs — Shared native IMAP polling for
|
|
1
|
+
// imap-client.mjs — Shared native IMAP polling for the agent's inbox and
|
|
2
|
+
// any other inbox the agent has explicit access to (e.g. principal-gmail).
|
|
2
3
|
// Replaces external Python scripts with direct Node.js IMAP via imapflow.
|
|
3
4
|
//
|
|
4
5
|
// Each inbox gets its own credentials and file-naming convention, but the
|
|
@@ -9,10 +10,10 @@ import { ImapFlow } from "imapflow";
|
|
|
9
10
|
import { writeFileSync, existsSync, readFileSync, mkdirSync } from "fs";
|
|
10
11
|
import { join, dirname } from "path";
|
|
11
12
|
import { createHash } from "crypto";
|
|
12
|
-
import {
|
|
13
|
+
import { AGENT_REPO_DIR } from "./utils.mjs";
|
|
13
14
|
|
|
14
|
-
const INBOX_DIR = join(
|
|
15
|
-
const ATTACHMENTS_DIR = join(
|
|
15
|
+
const INBOX_DIR = join(AGENT_REPO_DIR, "state", "inbox", "gmail");
|
|
16
|
+
const ATTACHMENTS_DIR = join(AGENT_REPO_DIR, "state", "inbox", "attachments");
|
|
16
17
|
const MAX_EMAILS_PER_CYCLE = 20;
|
|
17
18
|
const SNIPPET_LENGTH = 500;
|
|
18
19
|
const IMAP_TIMEOUT_MS = 25_000;
|
|
@@ -55,7 +56,7 @@ function isAlreadyProcessed(midHash, suffix) {
|
|
|
55
56
|
// ── Cursor Management ───────────────────────────────────────────────────
|
|
56
57
|
|
|
57
58
|
function cursorPath(account) {
|
|
58
|
-
return join(
|
|
59
|
+
return join(AGENT_REPO_DIR, "state", "polling", `${account}-gmail-cursor.yaml`);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function readImapCursor(account) {
|
|
@@ -128,12 +129,12 @@ function saveAttachments(parsed, midHash) {
|
|
|
128
129
|
* Poll a Gmail inbox via IMAP and write new messages as JSON to the inbox dir.
|
|
129
130
|
*
|
|
130
131
|
* @param {object} opts
|
|
131
|
-
* @param {string} opts.email — Email address
|
|
132
|
+
* @param {string} opts.email — Email address to poll
|
|
132
133
|
* @param {string} opts.password — Gmail app password
|
|
133
|
-
* @param {string} opts.account — Account slug for cursor/file naming (e.g.
|
|
134
|
-
* @param {string} opts.fileSuffix — JSON file suffix
|
|
135
|
-
* @param {string} opts.eventType — Event type tag
|
|
136
|
-
* @param {string} opts.logPrefix — Console log prefix
|
|
134
|
+
* @param {string} opts.account — Account slug for cursor/file naming (e.g. agent's first name)
|
|
135
|
+
* @param {string} opts.fileSuffix — JSON file suffix
|
|
136
|
+
* @param {string} opts.eventType — Event type tag
|
|
137
|
+
* @param {string} opts.logPrefix — Console log prefix
|
|
137
138
|
* @returns {Promise<{newCount: number, errors: string[]}>}
|
|
138
139
|
*/
|
|
139
140
|
export async function pollImapInbox({
|