myaiforone 1.0.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/README.md +113 -0
- package/agents/_template/CLAUDE.md +18 -0
- package/agents/_template/agent.json +7 -0
- package/agents/platform/agentcreator/CLAUDE.md +300 -0
- package/agents/platform/appcreator/CLAUDE.md +158 -0
- package/agents/platform/gym/CLAUDE.md +486 -0
- package/agents/platform/gym/agent.json +40 -0
- package/agents/platform/gym/programs/agent-building/program.json +160 -0
- package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
- package/agents/platform/gym/programs/getting-started/program.json +124 -0
- package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
- package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
- package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
- package/agents/platform/gym/souls/alex.md +12 -0
- package/agents/platform/gym/souls/jordan.md +12 -0
- package/agents/platform/gym/souls/morgan.md +12 -0
- package/agents/platform/gym/souls/riley.md +12 -0
- package/agents/platform/gym/souls/sam.md +12 -0
- package/agents/platform/hub/CLAUDE.md +372 -0
- package/agents/platform/promptcreator/CLAUDE.md +130 -0
- package/agents/platform/skillcreator/CLAUDE.md +163 -0
- package/bin/cli.js +566 -0
- package/config.example.json +310 -0
- package/dist/agent-registry.d.ts +32 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +144 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/channels/discord.d.ts +17 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +114 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/imessage.d.ts +23 -0
- package/dist/channels/imessage.d.ts.map +1 -0
- package/dist/channels/imessage.js +214 -0
- package/dist/channels/imessage.js.map +1 -0
- package/dist/channels/slack.d.ts +19 -0
- package/dist/channels/slack.d.ts.map +1 -0
- package/dist/channels/slack.js +167 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/telegram.d.ts +19 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +274 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/types.d.ts +44 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +18 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/channels/whatsapp.d.ts +23 -0
- package/dist/channels/whatsapp.d.ts.map +1 -0
- package/dist/channels/whatsapp.js +189 -0
- package/dist/channels/whatsapp.js.map +1 -0
- package/dist/config.d.ts +134 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/cron.d.ts +8 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +35 -0
- package/dist/cron.js.map +1 -0
- package/dist/decrypt-keys.d.ts +7 -0
- package/dist/decrypt-keys.d.ts.map +1 -0
- package/dist/decrypt-keys.js +53 -0
- package/dist/decrypt-keys.js.map +1 -0
- package/dist/encrypt-keys.d.ts +8 -0
- package/dist/encrypt-keys.d.ts.map +1 -0
- package/dist/encrypt-keys.js +62 -0
- package/dist/encrypt-keys.js.map +1 -0
- package/dist/executor.d.ts +31 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +2009 -0
- package/dist/executor.js.map +1 -0
- package/dist/gemini-executor.d.ts +27 -0
- package/dist/gemini-executor.d.ts.map +1 -0
- package/dist/gemini-executor.js +160 -0
- package/dist/gemini-executor.js.map +1 -0
- package/dist/goals.d.ts +24 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +189 -0
- package/dist/goals.js.map +1 -0
- package/dist/gym/activity-digest.d.ts +30 -0
- package/dist/gym/activity-digest.d.ts.map +1 -0
- package/dist/gym/activity-digest.js +506 -0
- package/dist/gym/activity-digest.js.map +1 -0
- package/dist/gym/dimension-scorer.d.ts +76 -0
- package/dist/gym/dimension-scorer.d.ts.map +1 -0
- package/dist/gym/dimension-scorer.js +236 -0
- package/dist/gym/dimension-scorer.js.map +1 -0
- package/dist/gym/gym-router.d.ts +7 -0
- package/dist/gym/gym-router.d.ts.map +1 -0
- package/dist/gym/gym-router.js +718 -0
- package/dist/gym/gym-router.js.map +1 -0
- package/dist/gym/index.d.ts +11 -0
- package/dist/gym/index.d.ts.map +1 -0
- package/dist/gym/index.js +11 -0
- package/dist/gym/index.js.map +1 -0
- package/dist/heartbeat.d.ts +21 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +163 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/keystore.d.ts +22 -0
- package/dist/keystore.d.ts.map +1 -0
- package/dist/keystore.js +178 -0
- package/dist/keystore.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +45 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/daily.d.ts +22 -0
- package/dist/memory/daily.d.ts.map +1 -0
- package/dist/memory/daily.js +82 -0
- package/dist/memory/daily.js.map +1 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.d.ts.map +1 -0
- package/dist/memory/embeddings.js +154 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +159 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/search.d.ts +21 -0
- package/dist/memory/search.d.ts.map +1 -0
- package/dist/memory/search.js +77 -0
- package/dist/memory/search.js.map +1 -0
- package/dist/memory/store.d.ts +23 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +144 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/ollama-executor.d.ts +17 -0
- package/dist/ollama-executor.d.ts.map +1 -0
- package/dist/ollama-executor.js +112 -0
- package/dist/ollama-executor.js.map +1 -0
- package/dist/openai-executor.d.ts +38 -0
- package/dist/openai-executor.d.ts.map +1 -0
- package/dist/openai-executor.js +197 -0
- package/dist/openai-executor.js.map +1 -0
- package/dist/router.d.ts +11 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +185 -0
- package/dist/router.js.map +1 -0
- package/dist/test-message.d.ts +2 -0
- package/dist/test-message.d.ts.map +1 -0
- package/dist/test-message.js +60 -0
- package/dist/test-message.js.map +1 -0
- package/dist/utils/imsg-db-reader.d.ts +24 -0
- package/dist/utils/imsg-db-reader.d.ts.map +1 -0
- package/dist/utils/imsg-db-reader.js +92 -0
- package/dist/utils/imsg-db-reader.js.map +1 -0
- package/dist/utils/imsg-rpc.d.ts +25 -0
- package/dist/utils/imsg-rpc.d.ts.map +1 -0
- package/dist/utils/imsg-rpc.js +149 -0
- package/dist/utils/imsg-rpc.js.map +1 -0
- package/dist/utils/message-formatter.d.ts +3 -0
- package/dist/utils/message-formatter.d.ts.map +1 -0
- package/dist/utils/message-formatter.js +69 -0
- package/dist/utils/message-formatter.js.map +1 -0
- package/dist/web-ui.d.ts +12 -0
- package/dist/web-ui.d.ts.map +1 -0
- package/dist/web-ui.js +5784 -0
- package/dist/web-ui.js.map +1 -0
- package/dist/whatsapp-chats.d.ts +2 -0
- package/dist/whatsapp-chats.d.ts.map +1 -0
- package/dist/whatsapp-chats.js +76 -0
- package/dist/whatsapp-chats.js.map +1 -0
- package/dist/whatsapp-login.d.ts +2 -0
- package/dist/whatsapp-login.d.ts.map +1 -0
- package/dist/whatsapp-login.js +90 -0
- package/dist/whatsapp-login.js.map +1 -0
- package/dist/wiki-sync.d.ts +21 -0
- package/dist/wiki-sync.d.ts.map +1 -0
- package/dist/wiki-sync.js +147 -0
- package/dist/wiki-sync.js.map +1 -0
- package/docs/AddNewAgentGuide.md +100 -0
- package/docs/AddNewMcpGuide.md +72 -0
- package/docs/Architecture.md +795 -0
- package/docs/CLAUDE-AI-SETUP.md +166 -0
- package/docs/Setup.md +297 -0
- package/docs/ai-gym-architecture.md +1040 -0
- package/docs/ai-gym-build-plan.md +343 -0
- package/docs/ai-gym-onboarding.md +122 -0
- package/docs/appcreator_plan.md +348 -0
- package/docs/platform-mcp-audit.md +320 -0
- package/docs/server-deployment-plan.md +503 -0
- package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
- package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
- package/docs/user-guide.md +2016 -0
- package/mcp-catalog.json +628 -0
- package/package.json +63 -0
- package/public/MyAIforOne-logomark-512.svg +16 -0
- package/public/MyAIforOne-logomark-transparent.svg +15 -0
- package/public/activity.html +314 -0
- package/public/admin.html +1674 -0
- package/public/agent-dashboard.html +670 -0
- package/public/api-docs.html +1106 -0
- package/public/automations.html +722 -0
- package/public/canvas.css +223 -0
- package/public/canvas.js +588 -0
- package/public/changelog.html +231 -0
- package/public/gym.html +2766 -0
- package/public/home.html +1930 -0
- package/public/index.html +2809 -0
- package/public/lab.html +1643 -0
- package/public/library.html +1442 -0
- package/public/marketplace.html +1101 -0
- package/public/mcp-docs.html +441 -0
- package/public/mini.html +390 -0
- package/public/monitor.html +584 -0
- package/public/org.html +4304 -0
- package/public/projects.html +734 -0
- package/public/settings.html +645 -0
- package/public/tasks.html +932 -0
- package/public/trainers/alex.svg +12 -0
- package/public/trainers/jordan.svg +12 -0
- package/public/trainers/morgan.svg +12 -0
- package/public/trainers/riley.svg +12 -0
- package/public/trainers/sam.svg +12 -0
- package/public/user-guide.html +218 -0
- package/registry/agents.json +3 -0
- package/registry/apps.json +20 -0
- package/registry/installed-drafts.json +3 -0
- package/registry/mcps.json +1084 -0
- package/registry/prompts/personal/mcp-test-prompt.md +6 -0
- package/registry/prompts/personal/memory-recall.md +6 -0
- package/registry/prompts/platform/brainstorm.md +15 -0
- package/registry/prompts/platform/code-review.md +16 -0
- package/registry/prompts/platform/explain.md +16 -0
- package/registry/prompts.json +58 -0
- package/registry/skills/external/brainstorming.md +5 -0
- package/registry/skills/external/code-review.md +40 -0
- package/registry/skills/external/frontend-patterns.md +642 -0
- package/registry/skills/external/frontend-slides.md +184 -0
- package/registry/skills/external/systematic-debugging.md +5 -0
- package/registry/skills/external/tdd.md +328 -0
- package/registry/skills/external/verification-before-completion.md +5 -0
- package/registry/skills/external/writing-plans.md +5 -0
- package/registry/skills/platform/ai41_app_build.md +930 -0
- package/registry/skills/platform/ai41_app_deploy.md +168 -0
- package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
- package/registry/skills/platform/ai41_app_patterns.md +359 -0
- package/registry/skills/platform/ai41_app_register.md +85 -0
- package/registry/skills/platform/ai41_app_scaffold.md +421 -0
- package/registry/skills/platform/ai41_app_verify.md +107 -0
- package/registry/skills/platform/opProjectCreate.md +239 -0
- package/registry/skills/platform/op_devbrowser.md +136 -0
- package/registry/skills/platform/sop_brandguidelines.md +103 -0
- package/registry/skills/platform/sop_docx.md +117 -0
- package/registry/skills/platform/sop_frontenddesign.md +44 -0
- package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
- package/registry/skills/platform/sop_mcpbuilder.md +133 -0
- package/registry/skills/platform/sop_pdf.md +172 -0
- package/registry/skills/platform/sop_pptx.md +133 -0
- package/registry/skills/platform/sop_skillcreator.md +104 -0
- package/registry/skills/platform/sop_themefactory.md +128 -0
- package/registry/skills/platform/sop_webapptesting.md +75 -0
- package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
- package/registry/skills/platform/sop_xlsx.md +134 -0
- package/registry/skills.json +1055 -0
- package/scripts/discover-chats.sh +11 -0
- package/scripts/install-service-windows.ps1 +87 -0
- package/scripts/install-service.sh +52 -0
- package/scripts/seed-registry.ts +195 -0
- package/scripts/test-send.sh +5 -0
- package/scripts/tray-indicator.ps1 +35 -0
- package/scripts/uninstall-service-windows.ps1 +23 -0
- package/scripts/uninstall-service.sh +15 -0
- package/scripts/xbar-myagent.5s.sh +32 -0
- package/server/mcp-server/dist/index.d.ts +11 -0
- package/server/mcp-server/dist/index.js +1332 -0
- package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
- package/server/mcp-server/dist/lib/api-client.js +241 -0
- package/server/mcp-server/index.ts +1545 -0
- package/server/mcp-server/lib/api-client.ts +366 -0
- package/server/mcp-server/tsconfig.json +14 -0
- package/src/agent-registry.ts +180 -0
- package/src/channels/discord.ts +129 -0
- package/src/channels/imessage.ts +261 -0
- package/src/channels/slack.ts +208 -0
- package/src/channels/telegram.ts +307 -0
- package/src/channels/types.ts +62 -0
- package/src/channels/whatsapp.ts +227 -0
- package/src/config.ts +281 -0
- package/src/cron.ts +43 -0
- package/src/decrypt-keys.ts +60 -0
- package/src/encrypt-keys.ts +70 -0
- package/src/executor.ts +2190 -0
- package/src/gemini-executor.ts +212 -0
- package/src/goals.ts +240 -0
- package/src/gym/activity-digest.ts +546 -0
- package/src/gym/dimension-scorer.ts +297 -0
- package/src/gym/gym-router.ts +801 -0
- package/src/gym/index.ts +19 -0
- package/src/heartbeat.ts +220 -0
- package/src/index.ts +275 -0
- package/src/keystore.ts +190 -0
- package/src/logger.ts +51 -0
- package/src/memory/daily.ts +101 -0
- package/src/memory/embeddings.ts +185 -0
- package/src/memory/index.ts +218 -0
- package/src/memory/search.ts +124 -0
- package/src/memory/store.ts +189 -0
- package/src/ollama-executor.ts +126 -0
- package/src/openai-executor.ts +259 -0
- package/src/router.ts +230 -0
- package/src/test-message.ts +72 -0
- package/src/utils/imsg-db-reader.ts +109 -0
- package/src/utils/imsg-rpc.ts +178 -0
- package/src/utils/message-formatter.ts +90 -0
- package/src/web-ui.ts +5778 -0
- package/src/whatsapp-chats.ts +91 -0
- package/src/whatsapp-login.ts +110 -0
- package/src/wiki-sync.ts +199 -0
- package/tsconfig.json +19 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { InboundMessage } from "./channels/types.js";
|
|
4
|
+
import type { AgentConfig, RouteConfig, AppConfig } from "./config.js";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
|
|
7
|
+
export interface ResolvedRoute {
|
|
8
|
+
agentId: string;
|
|
9
|
+
agentConfig: AgentConfig;
|
|
10
|
+
route: RouteConfig;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ─── Pairing store ───────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const pairedSenders = new Set<string>();
|
|
16
|
+
let pairingStoreLoaded = false;
|
|
17
|
+
let pairingStorePath = "";
|
|
18
|
+
|
|
19
|
+
function loadPairingStore(baseDir: string): void {
|
|
20
|
+
if (pairingStoreLoaded) return;
|
|
21
|
+
pairingStorePath = join(baseDir, "data", "paired-senders.json");
|
|
22
|
+
try {
|
|
23
|
+
if (existsSync(pairingStorePath)) {
|
|
24
|
+
const data = JSON.parse(readFileSync(pairingStorePath, "utf-8")) as string[];
|
|
25
|
+
for (const s of data) pairedSenders.add(s);
|
|
26
|
+
}
|
|
27
|
+
} catch { /* fresh start */ }
|
|
28
|
+
pairingStoreLoaded = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function savePairingStore(): void {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(join(pairingStorePath, ".."), { recursive: true });
|
|
34
|
+
writeFileSync(pairingStorePath, JSON.stringify([...pairedSenders], null, 2));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
log.warn(`Failed to save pairing store: ${err}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Sticky routing ──────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
interface StickyEntry {
|
|
43
|
+
agentId: string;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Key: "channel:chatId:sender" → last agent they talked to
|
|
48
|
+
const stickyMap = new Map<string, StickyEntry>();
|
|
49
|
+
|
|
50
|
+
const DEFAULT_STICKY_TIMEOUT_MS = 300_000; // 5 minutes
|
|
51
|
+
|
|
52
|
+
// Sticky routing modes:
|
|
53
|
+
// "none" — always require @mention (default)
|
|
54
|
+
// "sticky" — mention once, then all messages route for stickyTimeoutMs
|
|
55
|
+
// "prefix" — like sticky, but follow-up messages must start with a trigger character (e.g., ! or @)
|
|
56
|
+
type StickyMode = "none" | "sticky" | "prefix";
|
|
57
|
+
|
|
58
|
+
function getStickyMode(config: AppConfig, channel: string): { mode: StickyMode; prefix: string; timeoutMs: number } {
|
|
59
|
+
const channelCfg = config.channels[channel];
|
|
60
|
+
if (!channelCfg) return { mode: "none", prefix: "!", timeoutMs: DEFAULT_STICKY_TIMEOUT_MS };
|
|
61
|
+
|
|
62
|
+
const raw = (channelCfg.config as any).stickyRouting;
|
|
63
|
+
let mode: StickyMode = "prefix"; // default: prefix mode
|
|
64
|
+
if (raw === "sticky") mode = "sticky";
|
|
65
|
+
else if (raw === "none") mode = "none";
|
|
66
|
+
else if (raw === "prefix") mode = "prefix";
|
|
67
|
+
|
|
68
|
+
const prefix = (channelCfg.config as any).stickyPrefix ?? "!";
|
|
69
|
+
const timeoutMs = (channelCfg.config as any).stickyTimeoutMs ?? DEFAULT_STICKY_TIMEOUT_MS;
|
|
70
|
+
return { mode, prefix, timeoutMs };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getStickyAgent(
|
|
74
|
+
msg: InboundMessage,
|
|
75
|
+
config: AppConfig,
|
|
76
|
+
): ResolvedRoute | null {
|
|
77
|
+
const { mode, prefix, timeoutMs } = getStickyMode(config, msg.channel);
|
|
78
|
+
if (mode === "none") return null;
|
|
79
|
+
|
|
80
|
+
const key = `${msg.channel}:${msg.chatId}:${msg.sender}`;
|
|
81
|
+
const entry = stickyMap.get(key);
|
|
82
|
+
|
|
83
|
+
if (!entry) return null;
|
|
84
|
+
|
|
85
|
+
// Check if expired
|
|
86
|
+
if (Date.now() - entry.timestamp > timeoutMs) {
|
|
87
|
+
stickyMap.delete(key);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Prefix mode: message must start with the trigger character
|
|
92
|
+
if (mode === "prefix") {
|
|
93
|
+
const trimmed = msg.text.trim();
|
|
94
|
+
if (!trimmed.startsWith(prefix)) {
|
|
95
|
+
return null; // No prefix — don't route via sticky
|
|
96
|
+
}
|
|
97
|
+
// Strip the prefix from the message text for the agent
|
|
98
|
+
msg.text = trimmed.slice(prefix.length).trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Verify the agent still exists
|
|
102
|
+
const agent = config.agents[entry.agentId];
|
|
103
|
+
if (!agent) {
|
|
104
|
+
stickyMap.delete(key);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Find the matching route for this channel + chatId
|
|
109
|
+
const route = agent.routes.find(
|
|
110
|
+
r => r.channel === msg.channel && String(r.match.value) === msg.chatId
|
|
111
|
+
);
|
|
112
|
+
if (!route) return null;
|
|
113
|
+
|
|
114
|
+
// Permission check
|
|
115
|
+
if (!isAllowed(msg, route)) return null;
|
|
116
|
+
|
|
117
|
+
log.debug(`Sticky routing (${mode}): ${msg.sender} → ${entry.agentId} (${Math.round((Date.now() - entry.timestamp) / 1000)}s ago)`);
|
|
118
|
+
return { agentId: entry.agentId, agentConfig: agent, route };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setStickyAgent(msg: InboundMessage, agentId: string): void {
|
|
122
|
+
const key = `${msg.channel}:${msg.chatId}:${msg.sender}`;
|
|
123
|
+
stickyMap.set(key, { agentId, timestamp: Date.now() });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Route resolver ──────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export function resolveRoute(
|
|
129
|
+
msg: InboundMessage,
|
|
130
|
+
config: AppConfig,
|
|
131
|
+
baseDir?: string,
|
|
132
|
+
): ResolvedRoute | null {
|
|
133
|
+
// Feature 4: DM pairing gate
|
|
134
|
+
if (config.service.pairingCode && baseDir) {
|
|
135
|
+
loadPairingStore(baseDir);
|
|
136
|
+
|
|
137
|
+
const senderKey = `${msg.channel}:${msg.sender}`;
|
|
138
|
+
if (!pairedSenders.has(senderKey)) {
|
|
139
|
+
if (msg.text.trim() === config.service.pairingCode) {
|
|
140
|
+
pairedSenders.add(senderKey);
|
|
141
|
+
savePairingStore();
|
|
142
|
+
log.info(`Paired sender: ${senderKey}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
log.debug(`Unpaired sender ${senderKey} — ignoring`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Try explicit mention routing first
|
|
151
|
+
for (const [agentId, agent] of Object.entries(config.agents)) {
|
|
152
|
+
for (const route of agent.routes) {
|
|
153
|
+
if (route.channel !== msg.channel) continue;
|
|
154
|
+
|
|
155
|
+
const matchValue = String(route.match.value);
|
|
156
|
+
if (msg.chatId !== matchValue) continue;
|
|
157
|
+
|
|
158
|
+
if (!isAllowed(msg, route)) {
|
|
159
|
+
log.debug(`Blocked: ${msg.sender} not in allowFrom for ${agentId}`);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (route.permissions.requireMention) {
|
|
164
|
+
if (!hasMention(msg.text, agent)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Explicit mention found — update sticky and return
|
|
170
|
+
setStickyAgent(msg, agentId);
|
|
171
|
+
return { agentId, agentConfig: agent, route };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// No explicit mention — try sticky routing
|
|
176
|
+
const sticky = getStickyAgent(msg, config);
|
|
177
|
+
if (sticky) {
|
|
178
|
+
// Refresh the timestamp on each sticky hit
|
|
179
|
+
setStickyAgent(msg, sticky.agentId);
|
|
180
|
+
return sticky;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fall back to default agent — only for the web channel
|
|
184
|
+
if (msg.channel === "web" && config.defaultAgent && config.agents[config.defaultAgent]) {
|
|
185
|
+
const agent = config.agents[config.defaultAgent];
|
|
186
|
+
const matchingRoute = agent.routes.find(r => r.channel === msg.channel);
|
|
187
|
+
if (matchingRoute) {
|
|
188
|
+
return {
|
|
189
|
+
agentId: config.defaultAgent,
|
|
190
|
+
agentConfig: agent,
|
|
191
|
+
route: matchingRoute,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
log.debug(`No route for ${msg.channel}:${msg.chatId} from ${msg.senderName || msg.sender}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Exported for pairing check in index.ts
|
|
201
|
+
export function isPairingAttempt(msg: InboundMessage, config: AppConfig, baseDir: string): boolean {
|
|
202
|
+
if (!config.service.pairingCode) return false;
|
|
203
|
+
loadPairingStore(baseDir);
|
|
204
|
+
const senderKey = `${msg.channel}:${msg.sender}`;
|
|
205
|
+
if (pairedSenders.has(senderKey)) return false;
|
|
206
|
+
return msg.text.trim() === config.service.pairingCode;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function pairSender(msg: InboundMessage, baseDir: string): void {
|
|
210
|
+
loadPairingStore(baseDir);
|
|
211
|
+
const senderKey = `${msg.channel}:${msg.sender}`;
|
|
212
|
+
pairedSenders.add(senderKey);
|
|
213
|
+
savePairingStore();
|
|
214
|
+
log.info(`Paired sender: ${senderKey}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function hasMention(text: string, agent: AgentConfig): boolean {
|
|
218
|
+
const lower = text.toLowerCase();
|
|
219
|
+
if (agent.mentionAliases?.length) {
|
|
220
|
+
return agent.mentionAliases.some((alias) => lower.includes(alias.toLowerCase()));
|
|
221
|
+
}
|
|
222
|
+
return lower.includes(agent.name.toLowerCase());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isAllowed(msg: InboundMessage, route: RouteConfig): boolean {
|
|
226
|
+
const { allowFrom } = route.permissions;
|
|
227
|
+
if (!allowFrom || allowFrom.length === 0) return true;
|
|
228
|
+
if (allowFrom.includes("*")) return true;
|
|
229
|
+
return allowFrom.includes(msg.sender);
|
|
230
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { resolve, dirname } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { configureLogger, log } from "./logger.js";
|
|
5
|
+
import { executeAgent } from "./executor.js";
|
|
6
|
+
import type { InboundMessage } from "./channels/types.js";
|
|
7
|
+
import type { ResolvedRoute } from "./router.js";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const baseDir = resolve(__dirname, "..");
|
|
11
|
+
|
|
12
|
+
async function main(): Promise<void> {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
let agentId: string | null = null;
|
|
16
|
+
let text: string | null = null;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
if (args[i] === "--agent" && args[i + 1]) {
|
|
20
|
+
agentId = args[++i];
|
|
21
|
+
} else if (args[i] === "--text" && args[i + 1]) {
|
|
22
|
+
text = args[++i];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!agentId || !text) {
|
|
27
|
+
console.log("Usage: npm run test-message -- --agent <agentId> --text <message>");
|
|
28
|
+
console.log('Example: npm run test-message -- --agent fic-show --text "List all episodes"');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const configPath = resolve(baseDir, "config.json");
|
|
33
|
+
const config = loadConfig(configPath);
|
|
34
|
+
|
|
35
|
+
configureLogger("debug");
|
|
36
|
+
|
|
37
|
+
const agentConfig = config.agents[agentId];
|
|
38
|
+
if (!agentConfig) {
|
|
39
|
+
console.error(`Agent "${agentId}" not found. Available: ${Object.keys(config.agents).join(", ")}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const msg: InboundMessage = {
|
|
44
|
+
id: "test-" + Date.now(),
|
|
45
|
+
channel: "test",
|
|
46
|
+
chatId: "test",
|
|
47
|
+
chatType: "dm",
|
|
48
|
+
sender: "test-user",
|
|
49
|
+
text,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
isFromMe: false,
|
|
52
|
+
isGroup: false,
|
|
53
|
+
raw: {},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const route: ResolvedRoute = {
|
|
57
|
+
agentId,
|
|
58
|
+
agentConfig,
|
|
59
|
+
route: agentConfig.routes[0],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
log.info(`Testing agent "${agentId}" with message: "${text}"`);
|
|
63
|
+
const response = await executeAgent(route, msg, baseDir);
|
|
64
|
+
console.log("\n--- Agent Response ---");
|
|
65
|
+
console.log(response);
|
|
66
|
+
console.log("--- End ---\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main().catch((err) => {
|
|
70
|
+
console.error("Fatal error:", err);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct iMessage database reader.
|
|
3
|
+
* Reads from ~/Library/Messages/chat.db to get message text from attributedBody
|
|
4
|
+
* when the text column is empty (macOS 15+ behavior).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const DB_PATH = join(homedir(), "Library", "Messages", "chat.db");
|
|
12
|
+
|
|
13
|
+
interface DBMessage {
|
|
14
|
+
rowid: number;
|
|
15
|
+
text: string;
|
|
16
|
+
chatId: number;
|
|
17
|
+
isFromMe: boolean;
|
|
18
|
+
sender: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
guid: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract plain text from NSAttributedString binary blob (NSArchiver/typedstream format).
|
|
25
|
+
* The text is stored after a \x01\x2B marker followed by a length byte, then raw UTF-8 bytes.
|
|
26
|
+
*/
|
|
27
|
+
function extractTextFromAttributedBody(hexData: string): string {
|
|
28
|
+
try {
|
|
29
|
+
const buf = Buffer.from(hexData, "hex");
|
|
30
|
+
|
|
31
|
+
// Search for the \x01\x2B (NSArchiver string marker "+") pattern
|
|
32
|
+
for (let i = 0; i < buf.length - 2; i++) {
|
|
33
|
+
if (buf[i] === 0x01 && buf[i + 1] === 0x2b) {
|
|
34
|
+
const len = buf[i + 2];
|
|
35
|
+
if (len > 0 && i + 3 + len <= buf.length) {
|
|
36
|
+
const text = buf.toString("utf-8", i + 3, i + 3 + len);
|
|
37
|
+
// Sanity check: should contain at least some printable chars
|
|
38
|
+
if (/[\x20-\x7E]/.test(text)) {
|
|
39
|
+
return text.trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return "";
|
|
45
|
+
} catch {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get recent messages from a specific chat, with attributedBody fallback.
|
|
52
|
+
*/
|
|
53
|
+
export function getRecentMessages(chatId: number, sinceRowId: number, limit: number = 20): DBMessage[] {
|
|
54
|
+
try {
|
|
55
|
+
const query = `
|
|
56
|
+
SELECT m.ROWID, m.text, m.is_from_me, m.handle_id,
|
|
57
|
+
datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') as created_at,
|
|
58
|
+
m.guid, hex(m.attributedBody) as attr_hex,
|
|
59
|
+
h.id as sender_id
|
|
60
|
+
FROM message m
|
|
61
|
+
JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
62
|
+
LEFT JOIN handle h ON m.handle_id = h.ROWID
|
|
63
|
+
WHERE cmj.chat_id = ${chatId}
|
|
64
|
+
AND m.ROWID > ${sinceRowId}
|
|
65
|
+
AND m.associated_message_type = 0
|
|
66
|
+
ORDER BY m.date ASC
|
|
67
|
+
LIMIT ${limit};
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const result = execSync(`sqlite3 -json "${DB_PATH}" "${query.replace(/"/g, '\\"')}"`, {
|
|
71
|
+
timeout: 5000,
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const rows = JSON.parse(result || "[]");
|
|
76
|
+
return rows.map((row: any) => {
|
|
77
|
+
let text = row.text || "";
|
|
78
|
+
if (!text && row.attr_hex) {
|
|
79
|
+
text = extractTextFromAttributedBody(row.attr_hex);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
rowid: row.ROWID,
|
|
83
|
+
text,
|
|
84
|
+
chatId,
|
|
85
|
+
isFromMe: row.is_from_me === 1,
|
|
86
|
+
sender: row.sender_id || (row.is_from_me === 1 ? "me" : "unknown"),
|
|
87
|
+
createdAt: row.created_at,
|
|
88
|
+
guid: row.guid,
|
|
89
|
+
};
|
|
90
|
+
}).filter((m: DBMessage) => m.text.trim().length > 0);
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the latest ROWID for a chat (for polling).
|
|
98
|
+
*/
|
|
99
|
+
export function getLatestRowId(chatId: number): number {
|
|
100
|
+
try {
|
|
101
|
+
const result = execSync(
|
|
102
|
+
`sqlite3 "${DB_PATH}" "SELECT MAX(m.ROWID) FROM message m JOIN chat_message_join cmj ON m.ROWID = cmj.message_id WHERE cmj.chat_id = ${chatId};"`,
|
|
103
|
+
{ timeout: 5000, encoding: "utf-8" }
|
|
104
|
+
);
|
|
105
|
+
return parseInt(result.trim()) || 0;
|
|
106
|
+
} catch {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { createInterface, type Interface } from "node:readline";
|
|
3
|
+
import { log } from "../logger.js";
|
|
4
|
+
|
|
5
|
+
interface PendingRequest {
|
|
6
|
+
resolve: (value: unknown) => void;
|
|
7
|
+
reject: (reason: Error) => void;
|
|
8
|
+
timer: ReturnType<typeof setTimeout>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface JsonRpcNotification {
|
|
12
|
+
jsonrpc: "2.0";
|
|
13
|
+
method: string;
|
|
14
|
+
params: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ImsgRpcClient {
|
|
18
|
+
private proc: ChildProcess | null = null;
|
|
19
|
+
private rl: Interface | null = null;
|
|
20
|
+
private nextId = 1;
|
|
21
|
+
private pending = new Map<number, PendingRequest>();
|
|
22
|
+
private notificationHandler: ((notification: JsonRpcNotification) => void) | null = null;
|
|
23
|
+
private cliPath: string;
|
|
24
|
+
private stopping = false;
|
|
25
|
+
private restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
26
|
+
|
|
27
|
+
constructor(cliPath = "imsg") {
|
|
28
|
+
this.cliPath = cliPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async start(): Promise<void> {
|
|
32
|
+
// Verify imsg exists
|
|
33
|
+
try {
|
|
34
|
+
const probe = spawn(this.cliPath, ["rpc", "--help"], { stdio: "pipe" });
|
|
35
|
+
await new Promise<void>((resolve, reject) => {
|
|
36
|
+
probe.on("close", () => resolve());
|
|
37
|
+
probe.on("error", (err) => reject(err));
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
const hint = process.platform === "darwin"
|
|
41
|
+
? `Install with: brew install steipete/tap/imsg`
|
|
42
|
+
: `iMessage is only available on macOS`;
|
|
43
|
+
throw new Error(
|
|
44
|
+
`"${this.cliPath}" not found. ${hint}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await this.startProcess();
|
|
49
|
+
log.info("imsg RPC client started");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private handleLine(line: string): void {
|
|
53
|
+
if (!line.trim()) return;
|
|
54
|
+
|
|
55
|
+
let parsed: Record<string, unknown>;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(line);
|
|
58
|
+
} catch {
|
|
59
|
+
log.warn(`imsg: unparseable line: ${line.slice(0, 200)}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Response to a request (has "id")
|
|
64
|
+
if ("id" in parsed && typeof parsed.id === "number") {
|
|
65
|
+
const req = this.pending.get(parsed.id);
|
|
66
|
+
if (req) {
|
|
67
|
+
clearTimeout(req.timer);
|
|
68
|
+
this.pending.delete(parsed.id);
|
|
69
|
+
|
|
70
|
+
if ("error" in parsed) {
|
|
71
|
+
req.reject(new Error(JSON.stringify(parsed.error)));
|
|
72
|
+
} else {
|
|
73
|
+
req.resolve(parsed.result);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Notification (has "method" but no "id")
|
|
80
|
+
if ("method" in parsed && !("id" in parsed)) {
|
|
81
|
+
this.notificationHandler?.(parsed as unknown as JsonRpcNotification);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
log.debug(`imsg: unhandled message: ${line.slice(0, 200)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async request<T>(method: string, params?: object, timeoutMs = 10_000): Promise<T> {
|
|
89
|
+
if (!this.proc?.stdin?.writable) {
|
|
90
|
+
throw new Error("imsg RPC client not started");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const id = this.nextId++;
|
|
94
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
95
|
+
|
|
96
|
+
return new Promise<T>((resolve, reject) => {
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
this.pending.delete(id);
|
|
99
|
+
reject(new Error(`imsg RPC timeout: ${method} (${timeoutMs}ms)`));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
|
|
102
|
+
this.pending.set(id, {
|
|
103
|
+
resolve: resolve as (value: unknown) => void,
|
|
104
|
+
reject,
|
|
105
|
+
timer,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.proc!.stdin!.write(payload + "\n");
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onNotification(handler: (notification: JsonRpcNotification) => void): void {
|
|
113
|
+
this.notificationHandler = handler;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async restart(): Promise<void> {
|
|
117
|
+
try {
|
|
118
|
+
this.rl?.close();
|
|
119
|
+
this.proc = null;
|
|
120
|
+
await this.startProcess();
|
|
121
|
+
// Re-subscribe to watch
|
|
122
|
+
await this.request("watch.subscribe", { attachments: false });
|
|
123
|
+
log.info("imsg RPC client restarted successfully");
|
|
124
|
+
} catch (err) {
|
|
125
|
+
log.error(`imsg restart failed: ${err} — retrying in 10s`);
|
|
126
|
+
this.restartTimer = setTimeout(() => this.restart(), 10_000);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async startProcess(): Promise<void> {
|
|
131
|
+
this.proc = spawn(this.cliPath, ["rpc"], {
|
|
132
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.proc.on("error", (err) => {
|
|
136
|
+
log.error(`imsg process error: ${err.message}`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.proc.on("close", (code) => {
|
|
140
|
+
log.warn(`imsg process exited with code ${code}`);
|
|
141
|
+
for (const [id, req] of this.pending) {
|
|
142
|
+
clearTimeout(req.timer);
|
|
143
|
+
req.reject(new Error("imsg process exited"));
|
|
144
|
+
this.pending.delete(id);
|
|
145
|
+
}
|
|
146
|
+
if (!this.stopping) {
|
|
147
|
+
log.info("imsg process died — restarting in 3s...");
|
|
148
|
+
this.restartTimer = setTimeout(() => this.restart(), 3000);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (this.proc.stderr) {
|
|
153
|
+
const stderrRl = createInterface({ input: this.proc.stderr });
|
|
154
|
+
stderrRl.on("line", (line) => {
|
|
155
|
+
if (line.trim()) log.warn(`imsg stderr: ${line}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.proc.stdout) {
|
|
160
|
+
this.rl = createInterface({ input: this.proc.stdout });
|
|
161
|
+
this.rl.on("line", (line) => this.handleLine(line));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async stop(): Promise<void> {
|
|
166
|
+
this.stopping = true;
|
|
167
|
+
if (this.restartTimer) clearTimeout(this.restartTimer);
|
|
168
|
+
for (const [id, req] of this.pending) {
|
|
169
|
+
clearTimeout(req.timer);
|
|
170
|
+
req.reject(new Error("Client stopping"));
|
|
171
|
+
this.pending.delete(id);
|
|
172
|
+
}
|
|
173
|
+
this.rl?.close();
|
|
174
|
+
this.proc?.kill("SIGTERM");
|
|
175
|
+
this.proc = null;
|
|
176
|
+
log.info("imsg RPC client stopped");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { InboundMessage } from "../channels/types.js";
|
|
3
|
+
|
|
4
|
+
interface ConversationEntry {
|
|
5
|
+
ts: string;
|
|
6
|
+
from: string;
|
|
7
|
+
text: string;
|
|
8
|
+
response: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatMessage(
|
|
12
|
+
msg: InboundMessage,
|
|
13
|
+
memoryContextPath?: string,
|
|
14
|
+
conversationLogPath?: string,
|
|
15
|
+
historyLimit = 5,
|
|
16
|
+
): string {
|
|
17
|
+
const parts: string[] = [];
|
|
18
|
+
|
|
19
|
+
// 1. Memory context
|
|
20
|
+
if (memoryContextPath) {
|
|
21
|
+
try {
|
|
22
|
+
const context = readFileSync(memoryContextPath, "utf-8").trim();
|
|
23
|
+
if (context) {
|
|
24
|
+
parts.push(`[Agent Memory]\n${context}\n[/Agent Memory]`);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// no memory file yet, that's fine
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Conversation history
|
|
32
|
+
if (conversationLogPath) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(conversationLogPath, "utf-8").trim();
|
|
35
|
+
if (raw) {
|
|
36
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
37
|
+
const recent = lines.slice(-historyLimit);
|
|
38
|
+
const entries: string[] = [];
|
|
39
|
+
|
|
40
|
+
for (const line of recent) {
|
|
41
|
+
try {
|
|
42
|
+
const entry = JSON.parse(line) as ConversationEntry;
|
|
43
|
+
entries.push(` ${entry.from}: ${entry.text}`);
|
|
44
|
+
entries.push(` Agent: ${entry.response}`);
|
|
45
|
+
} catch {
|
|
46
|
+
// skip malformed lines
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (entries.length > 0) {
|
|
51
|
+
parts.push(
|
|
52
|
+
`[Conversation History - last ${recent.length} exchanges]\n${entries.join("\n")}\n[/Conversation History]`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// no log file yet
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. The message itself
|
|
62
|
+
const ts = new Date(msg.timestamp).toISOString();
|
|
63
|
+
const sender = msg.senderName || msg.sender;
|
|
64
|
+
|
|
65
|
+
let header: string;
|
|
66
|
+
if (msg.isGroup && msg.groupName) {
|
|
67
|
+
header = `[${msg.channel} group '${msg.groupName}' from ${sender} at ${ts}]`;
|
|
68
|
+
} else if (msg.isGroup) {
|
|
69
|
+
header = `[${msg.channel} group from ${sender} at ${ts}]`;
|
|
70
|
+
} else {
|
|
71
|
+
header = `[${msg.channel} DM from ${sender} at ${ts}]`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let body = msg.text;
|
|
75
|
+
|
|
76
|
+
// Reply context
|
|
77
|
+
if (msg.replyTo) {
|
|
78
|
+
const replySender = msg.replyTo.sender || "unknown";
|
|
79
|
+
body += `\n\n[Replying to ${replySender}]\n${msg.replyTo.text}\n[/Replying]`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Attachment note
|
|
83
|
+
if (msg.attachments?.length) {
|
|
84
|
+
body += `\n\n[${msg.attachments.length} image(s) attached — visible in this message. Save them to the appropriate episode folder and reference in the episode JSON.]`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
parts.push(`${header}\n${body}\n[/${msg.channel}]`);
|
|
88
|
+
|
|
89
|
+
return parts.join("\n\n");
|
|
90
|
+
}
|