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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { Client, GatewayIntentBits, AttachmentBuilder } from "discord.js";
|
|
4
|
+
import type { Message as DiscordMessage } from "discord.js";
|
|
5
|
+
import type { ChannelDriver, InboundMessage, OutboundMessage } from "./types.js";
|
|
6
|
+
import { splitText } from "./types.js";
|
|
7
|
+
import { log } from "../logger.js";
|
|
8
|
+
|
|
9
|
+
export class DiscordDriver implements ChannelDriver {
|
|
10
|
+
readonly channelId = "discord";
|
|
11
|
+
|
|
12
|
+
private client: Client;
|
|
13
|
+
private botToken: string;
|
|
14
|
+
private messageHandler: ((msg: InboundMessage) => Promise<void>) | null = null;
|
|
15
|
+
private botUserId: string | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(config: Record<string, unknown>) {
|
|
18
|
+
const botToken = config.botToken as string;
|
|
19
|
+
if (!botToken) {
|
|
20
|
+
throw new Error("Discord driver requires botToken in config");
|
|
21
|
+
}
|
|
22
|
+
this.botToken = botToken;
|
|
23
|
+
this.client = new Client({
|
|
24
|
+
intents: [
|
|
25
|
+
GatewayIntentBits.Guilds,
|
|
26
|
+
GatewayIntentBits.GuildMessages,
|
|
27
|
+
GatewayIntentBits.MessageContent,
|
|
28
|
+
GatewayIntentBits.DirectMessages,
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async start(): Promise<void> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
this.client.once("ready", () => {
|
|
36
|
+
this.botUserId = this.client.user?.id ?? null;
|
|
37
|
+
log.info(`Discord bot authenticated as ${this.client.user?.tag} (${this.botUserId})`);
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.client.on("messageCreate", (msg) => this.handleMessage(msg));
|
|
42
|
+
|
|
43
|
+
this.client.on("error", (err) => {
|
|
44
|
+
log.error(`Discord error: ${err.message}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.client.login(this.botToken).catch(reject);
|
|
48
|
+
log.info("Discord driver started — listening for messages");
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async stop(): Promise<void> {
|
|
53
|
+
await this.client.destroy();
|
|
54
|
+
log.info("Discord driver stopped");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMessage(handler: (msg: InboundMessage) => Promise<void>): void {
|
|
58
|
+
this.messageHandler = handler;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async send(msg: OutboundMessage): Promise<void> {
|
|
62
|
+
const channel = await this.client.channels.fetch(msg.chatId);
|
|
63
|
+
if (!channel?.isTextBased()) return;
|
|
64
|
+
|
|
65
|
+
const textChannel = channel as any;
|
|
66
|
+
const chunks = splitText(msg.text, 2000); // Discord limit
|
|
67
|
+
for (const chunk of chunks) {
|
|
68
|
+
await textChannel.send(chunk);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async sendTyping(chatId: string): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
75
|
+
if (channel?.isTextBased()) {
|
|
76
|
+
await (channel as any).sendTyping();
|
|
77
|
+
}
|
|
78
|
+
} catch { /* ignore */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async sendFile(chatId: string, filePath: string, caption?: string): Promise<void> {
|
|
82
|
+
try {
|
|
83
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
84
|
+
if (!channel?.isTextBased()) return;
|
|
85
|
+
|
|
86
|
+
const attachment = new AttachmentBuilder(filePath);
|
|
87
|
+
await (channel as any).send({
|
|
88
|
+
content: caption || undefined,
|
|
89
|
+
files: [attachment],
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log.warn(`Failed to send file to Discord: ${err}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async handleMessage(msg: DiscordMessage): Promise<void> {
|
|
97
|
+
if (msg.author.id === this.botUserId) return;
|
|
98
|
+
if (msg.author.bot) return;
|
|
99
|
+
if (!msg.content?.trim()) return;
|
|
100
|
+
|
|
101
|
+
const isGroup = msg.guild !== null;
|
|
102
|
+
|
|
103
|
+
const inbound: InboundMessage = {
|
|
104
|
+
id: msg.id,
|
|
105
|
+
channel: this.channelId,
|
|
106
|
+
chatId: msg.channelId,
|
|
107
|
+
chatType: isGroup ? "group" : "dm",
|
|
108
|
+
sender: msg.author.id,
|
|
109
|
+
senderName: msg.author.displayName || msg.author.username,
|
|
110
|
+
text: msg.content,
|
|
111
|
+
timestamp: msg.createdTimestamp,
|
|
112
|
+
isFromMe: false,
|
|
113
|
+
isGroup,
|
|
114
|
+
groupName: msg.guild?.name,
|
|
115
|
+
replyTo: msg.reference?.messageId
|
|
116
|
+
? { id: msg.reference.messageId, text: "" }
|
|
117
|
+
: undefined,
|
|
118
|
+
raw: msg,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
log.debug(`Discord received from ${msg.author.username} in ${msg.channelId}: ${msg.content.slice(0, 100)}`);
|
|
122
|
+
|
|
123
|
+
if (this.messageHandler) {
|
|
124
|
+
this.messageHandler(inbound).catch((err) => {
|
|
125
|
+
log.error(`Discord message handler error: ${err}`);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ChannelDriver, InboundMessage, OutboundMessage } from "./types.js";
|
|
2
|
+
import { splitText } from "./types.js";
|
|
3
|
+
import { ImsgRpcClient } from "../utils/imsg-rpc.js";
|
|
4
|
+
import { getRecentMessages, getLatestRowId } from "../utils/imsg-db-reader.js";
|
|
5
|
+
import { log } from "../logger.js";
|
|
6
|
+
|
|
7
|
+
interface ImsgMessage {
|
|
8
|
+
id: number;
|
|
9
|
+
sender: string;
|
|
10
|
+
text: string;
|
|
11
|
+
chat_id: number;
|
|
12
|
+
chat_guid: string;
|
|
13
|
+
chat_name: string | null;
|
|
14
|
+
is_group: boolean;
|
|
15
|
+
is_from_me: boolean;
|
|
16
|
+
participants: string[];
|
|
17
|
+
created_at: string;
|
|
18
|
+
reply_to_text: string | null;
|
|
19
|
+
reply_to_sender: string | null;
|
|
20
|
+
reply_to_id: number | null;
|
|
21
|
+
attachments: Array<{ path: string; mime_type?: string }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DebounceEntry {
|
|
25
|
+
messages: ImsgMessage[];
|
|
26
|
+
timer: ReturnType<typeof setTimeout>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class IMessageDriver implements ChannelDriver {
|
|
30
|
+
readonly channelId = "imessage";
|
|
31
|
+
|
|
32
|
+
private client: ImsgRpcClient;
|
|
33
|
+
private messageHandler: ((msg: InboundMessage) => Promise<void>) | null = null;
|
|
34
|
+
private debounceMap = new Map<string, DebounceEntry>();
|
|
35
|
+
private debounceMs: number;
|
|
36
|
+
private dbPollInterval: ReturnType<typeof setInterval> | null = null;
|
|
37
|
+
private dbPollRowIds = new Map<number, number>(); // chatId -> lastRowId
|
|
38
|
+
private monitoredChatIds: number[] = [];
|
|
39
|
+
private recentlySent = new Map<string, number>(); // text -> timestamp (for echo filtering)
|
|
40
|
+
|
|
41
|
+
constructor(config: Record<string, unknown>) {
|
|
42
|
+
const cliPath = (config.cliPath as string) ?? "imsg";
|
|
43
|
+
this.debounceMs = (config.debounceMs as number) ?? 2000;
|
|
44
|
+
this.monitoredChatIds = ((config.monitoredChatIds as number[]) ?? []);
|
|
45
|
+
this.client = new ImsgRpcClient(cliPath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async start(): Promise<void> {
|
|
49
|
+
await this.client.start();
|
|
50
|
+
|
|
51
|
+
this.client.onNotification((notification) => {
|
|
52
|
+
if (notification.method === "message") {
|
|
53
|
+
const raw = (notification.params as { message: ImsgMessage }).message;
|
|
54
|
+
this.handleRawMessage(raw);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Subscribe to message watch (attachments enabled for image support)
|
|
59
|
+
await this.client.request("watch.subscribe", { attachments: true });
|
|
60
|
+
|
|
61
|
+
// DB polling fallback — macOS 15+ stores text in attributedBody, not text column
|
|
62
|
+
// imsg watch may miss these messages, so we poll the DB directly
|
|
63
|
+
this.startDbPolling();
|
|
64
|
+
|
|
65
|
+
log.info("iMessage driver started — watching for messages + DB polling");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async stop(): Promise<void> {
|
|
69
|
+
if (this.dbPollInterval) {
|
|
70
|
+
clearInterval(this.dbPollInterval);
|
|
71
|
+
this.dbPollInterval = null;
|
|
72
|
+
}
|
|
73
|
+
for (const [, entry] of this.debounceMap) {
|
|
74
|
+
clearTimeout(entry.timer);
|
|
75
|
+
}
|
|
76
|
+
this.debounceMap.clear();
|
|
77
|
+
|
|
78
|
+
await this.client.stop();
|
|
79
|
+
log.info("iMessage driver stopped");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private startDbPolling(): void {
|
|
83
|
+
// Discover chat IDs to monitor from config routes
|
|
84
|
+
// If monitoredChatIds not set, we'll pick them up dynamically
|
|
85
|
+
// Initialize last-seen rowid for each chat
|
|
86
|
+
for (const chatId of this.monitoredChatIds) {
|
|
87
|
+
this.dbPollRowIds.set(chatId, getLatestRowId(chatId));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Poll every 3 seconds
|
|
91
|
+
this.dbPollInterval = setInterval(() => {
|
|
92
|
+
this.pollDb();
|
|
93
|
+
}, 3000);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private pollDb(): void {
|
|
97
|
+
// Also discover chat IDs dynamically from recent messages if not configured
|
|
98
|
+
if (this.monitoredChatIds.length === 0) return;
|
|
99
|
+
|
|
100
|
+
for (const chatId of this.monitoredChatIds) {
|
|
101
|
+
const lastRowId = this.dbPollRowIds.get(chatId) || 0;
|
|
102
|
+
const messages = getRecentMessages(chatId, lastRowId);
|
|
103
|
+
|
|
104
|
+
for (const msg of messages) {
|
|
105
|
+
// Update last seen
|
|
106
|
+
if (msg.rowid > (this.dbPollRowIds.get(chatId) || 0)) {
|
|
107
|
+
this.dbPollRowIds.set(chatId, msg.rowid);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Skip empty
|
|
111
|
+
if (!msg.text.trim()) continue;
|
|
112
|
+
|
|
113
|
+
// Skip bot's own messages — check against recently sent texts
|
|
114
|
+
// Use fuzzy match: attributedBody extraction can prepend/append garbage bytes
|
|
115
|
+
const msgTrimmed = msg.text.trim();
|
|
116
|
+
let isEcho = false;
|
|
117
|
+
for (const [sentText, ts] of this.recentlySent) {
|
|
118
|
+
// Match if either contains the other, or first 30 chars match
|
|
119
|
+
if (sentText === msgTrimmed ||
|
|
120
|
+
sentText.includes(msgTrimmed) ||
|
|
121
|
+
msgTrimmed.includes(sentText) ||
|
|
122
|
+
(msgTrimmed.length > 30 && sentText.startsWith(msgTrimmed.slice(0, 30))) ||
|
|
123
|
+
(msgTrimmed.length > 30 && sentText.includes(msgTrimmed.slice(1, 31)))) {
|
|
124
|
+
this.recentlySent.delete(sentText);
|
|
125
|
+
isEcho = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (isEcho) {
|
|
130
|
+
log.debug(`iMessage DB poll: skipping echo "${msg.text.slice(0, 40)}..."`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build an InboundMessage and pass to handler
|
|
135
|
+
const inbound: InboundMessage = {
|
|
136
|
+
id: String(msg.rowid),
|
|
137
|
+
channel: this.channelId,
|
|
138
|
+
chatId: String(chatId),
|
|
139
|
+
chatType: "group",
|
|
140
|
+
sender: msg.sender || "unknown",
|
|
141
|
+
text: msg.text,
|
|
142
|
+
timestamp: new Date(msg.createdAt).getTime(),
|
|
143
|
+
// Always false — user's phone shares Apple ID with this Mac,
|
|
144
|
+
// so all messages appear as is_from_me in the DB.
|
|
145
|
+
// Echo prevention is handled in index.ts via recentBotMessages.
|
|
146
|
+
isFromMe: false,
|
|
147
|
+
isGroup: true,
|
|
148
|
+
raw: msg,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
log.debug(`iMessage DB poll: ${msg.sender} in chat ${chatId}: ${msg.text.slice(0, 80)}`);
|
|
152
|
+
|
|
153
|
+
if (this.messageHandler) {
|
|
154
|
+
this.messageHandler(inbound).catch((err) => {
|
|
155
|
+
log.error(`iMessage DB poll handler error: ${err}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onMessage(handler: (msg: InboundMessage) => Promise<void>): void {
|
|
163
|
+
this.messageHandler = handler;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async send(msg: OutboundMessage): Promise<void> {
|
|
167
|
+
// iMessage doesn't have a strict char limit but long messages are unwieldy on phones
|
|
168
|
+
const chunks = splitText(msg.text, 3000);
|
|
169
|
+
for (const chunk of chunks) {
|
|
170
|
+
// Track what we send so DB poller can filter echoes
|
|
171
|
+
this.recentlySent.set(chunk.trim(), Date.now());
|
|
172
|
+
await this.client.request("send", {
|
|
173
|
+
text: chunk,
|
|
174
|
+
chat_id: Number(msg.chatId),
|
|
175
|
+
}, 30_000);
|
|
176
|
+
}
|
|
177
|
+
// Prune old entries (>60s) to prevent memory leak
|
|
178
|
+
const cutoff = Date.now() - 60_000;
|
|
179
|
+
for (const [text, ts] of this.recentlySent) {
|
|
180
|
+
if (ts < cutoff) this.recentlySent.delete(text);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private handleRawMessage(raw: ImsgMessage): void {
|
|
185
|
+
// Note: can't filter is_from_me here because the user's phone shares
|
|
186
|
+
// the same Apple ID as this Mac, so all messages appear as is_from_me.
|
|
187
|
+
// Echo prevention is handled in index.ts via recentBotMessages tracking.
|
|
188
|
+
if (!raw.text?.trim() && !raw.attachments?.length) return;
|
|
189
|
+
|
|
190
|
+
// Debounce: coalesce rapid messages from same sender in same chat
|
|
191
|
+
const key = `${raw.chat_id}:${raw.sender}`;
|
|
192
|
+
const existing = this.debounceMap.get(key);
|
|
193
|
+
|
|
194
|
+
if (existing) {
|
|
195
|
+
clearTimeout(existing.timer);
|
|
196
|
+
existing.messages.push(raw);
|
|
197
|
+
existing.timer = setTimeout(() => this.flushDebounce(key), this.debounceMs);
|
|
198
|
+
} else {
|
|
199
|
+
this.debounceMap.set(key, {
|
|
200
|
+
messages: [raw],
|
|
201
|
+
timer: setTimeout(() => this.flushDebounce(key), this.debounceMs),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private flushDebounce(key: string): void {
|
|
207
|
+
const entry = this.debounceMap.get(key);
|
|
208
|
+
if (!entry) return;
|
|
209
|
+
this.debounceMap.delete(key);
|
|
210
|
+
|
|
211
|
+
// Coalesce multiple messages into one
|
|
212
|
+
const messages = entry.messages;
|
|
213
|
+
const first = messages[0];
|
|
214
|
+
const text = messages.map((m) => m.text).join("\n");
|
|
215
|
+
|
|
216
|
+
const inbound: InboundMessage = {
|
|
217
|
+
id: String(first.id),
|
|
218
|
+
channel: this.channelId,
|
|
219
|
+
chatId: String(first.chat_id),
|
|
220
|
+
chatType: first.is_group ? "group" : "dm",
|
|
221
|
+
sender: first.sender,
|
|
222
|
+
text,
|
|
223
|
+
timestamp: new Date(first.created_at).getTime(),
|
|
224
|
+
isFromMe: first.is_from_me,
|
|
225
|
+
isGroup: first.is_group,
|
|
226
|
+
groupName: first.chat_name ?? undefined,
|
|
227
|
+
participants: first.participants,
|
|
228
|
+
replyTo: first.reply_to_id
|
|
229
|
+
? {
|
|
230
|
+
id: String(first.reply_to_id),
|
|
231
|
+
text: first.reply_to_text ?? "",
|
|
232
|
+
sender: first.reply_to_sender ?? undefined,
|
|
233
|
+
}
|
|
234
|
+
: undefined,
|
|
235
|
+
attachments: this.collectImageAttachments(messages),
|
|
236
|
+
raw: messages.length === 1 ? first : messages,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (this.messageHandler) {
|
|
240
|
+
this.messageHandler(inbound).catch((err) => {
|
|
241
|
+
log.error(`Message handler error: ${err}`);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private collectImageAttachments(messages: ImsgMessage[]): Array<{ path: string; mimeType?: string }> | undefined {
|
|
247
|
+
const imageTypes = ["image/png", "image/jpeg", "image/gif", "image/webp", "image/heic"];
|
|
248
|
+
const attachments: Array<{ path: string; mimeType?: string }> = [];
|
|
249
|
+
|
|
250
|
+
for (const msg of messages) {
|
|
251
|
+
if (!msg.attachments?.length) continue;
|
|
252
|
+
for (const a of msg.attachments) {
|
|
253
|
+
if (a.mime_type && imageTypes.includes(a.mime_type)) {
|
|
254
|
+
attachments.push({ path: a.path, mimeType: a.mime_type });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return attachments.length > 0 ? attachments : undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { WebClient } from "@slack/web-api";
|
|
5
|
+
import { SocketModeClient } from "@slack/socket-mode";
|
|
6
|
+
import type { ChannelDriver, InboundMessage, OutboundMessage } from "./types.js";
|
|
7
|
+
import { splitText } from "./types.js";
|
|
8
|
+
import { log } from "../logger.js";
|
|
9
|
+
|
|
10
|
+
interface SlackFile {
|
|
11
|
+
id: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
mimetype?: string;
|
|
14
|
+
filetype?: string;
|
|
15
|
+
url_private_download?: string;
|
|
16
|
+
size?: number;
|
|
17
|
+
file_access?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SlackMessageEvent {
|
|
21
|
+
type: "message";
|
|
22
|
+
subtype?: string;
|
|
23
|
+
channel: string;
|
|
24
|
+
user: string;
|
|
25
|
+
text: string;
|
|
26
|
+
ts: string;
|
|
27
|
+
thread_ts?: string;
|
|
28
|
+
channel_type: "channel" | "group" | "im" | "mpim";
|
|
29
|
+
files?: SlackFile[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class SlackDriver implements ChannelDriver {
|
|
33
|
+
readonly channelId = "slack";
|
|
34
|
+
|
|
35
|
+
private web: WebClient;
|
|
36
|
+
private socket: SocketModeClient;
|
|
37
|
+
private botToken: string;
|
|
38
|
+
private messageHandler: ((msg: InboundMessage) => Promise<void>) | null = null;
|
|
39
|
+
private botUserId: string | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(config: Record<string, unknown>) {
|
|
42
|
+
const botToken = config.botToken as string;
|
|
43
|
+
const appToken = config.appToken as string;
|
|
44
|
+
|
|
45
|
+
if (!botToken || !appToken) {
|
|
46
|
+
throw new Error("Slack driver requires botToken and appToken in config");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.botToken = botToken;
|
|
50
|
+
this.web = new WebClient(botToken);
|
|
51
|
+
this.socket = new SocketModeClient({
|
|
52
|
+
appToken,
|
|
53
|
+
clientPingTimeout: 30_000,
|
|
54
|
+
serverPingTimeout: 30_000,
|
|
55
|
+
pingPongLoggingEnabled: false,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async start(): Promise<void> {
|
|
60
|
+
const auth = await this.web.auth.test();
|
|
61
|
+
this.botUserId = auth.user_id as string;
|
|
62
|
+
log.info(`Slack bot authenticated as ${auth.user} (${this.botUserId})`);
|
|
63
|
+
|
|
64
|
+
this.socket.on("message", async ({ event, ack }) => {
|
|
65
|
+
await ack();
|
|
66
|
+
this.handleEvent(event as SlackMessageEvent);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this.socket.on("connected", () => {
|
|
70
|
+
log.info("Slack socket mode connected");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.socket.on("disconnected", () => {
|
|
74
|
+
log.warn("Slack socket mode disconnected");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await this.socket.start();
|
|
78
|
+
log.info("Slack driver started — listening for messages");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async stop(): Promise<void> {
|
|
82
|
+
await this.socket.disconnect();
|
|
83
|
+
log.info("Slack driver stopped");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onMessage(handler: (msg: InboundMessage) => Promise<void>): void {
|
|
87
|
+
this.messageHandler = handler;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async send(msg: OutboundMessage): Promise<void> {
|
|
91
|
+
// Slack limit is ~4000 chars (with some overhead for formatting)
|
|
92
|
+
const chunks = splitText(msg.text, 3900);
|
|
93
|
+
for (const chunk of chunks) {
|
|
94
|
+
await this.web.chat.postMessage({
|
|
95
|
+
channel: msg.chatId,
|
|
96
|
+
text: chunk,
|
|
97
|
+
...(msg.replyToId ? { thread_ts: msg.replyToId } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async sendTyping(chatId: string): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
// Slack doesn't have a direct typing indicator API for bots,
|
|
105
|
+
// but we can use a subtle reaction or just skip
|
|
106
|
+
// The "On it..." message in index.ts serves this purpose
|
|
107
|
+
} catch { /* ignore */ }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async sendFile(chatId: string, filePath: string, caption?: string): Promise<void> {
|
|
111
|
+
try {
|
|
112
|
+
const content = readFileSync(filePath);
|
|
113
|
+
await this.web.filesUploadV2({
|
|
114
|
+
channel_id: chatId,
|
|
115
|
+
file: content,
|
|
116
|
+
filename: basename(filePath),
|
|
117
|
+
initial_comment: caption || undefined,
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.warn(`Failed to send file to Slack: ${err}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async handleEvent(event: SlackMessageEvent): Promise<void> {
|
|
125
|
+
if (event.user === this.botUserId) return;
|
|
126
|
+
if (event.subtype && event.subtype !== "file_share") return;
|
|
127
|
+
if (!event.text?.trim() && !event.files?.length) return;
|
|
128
|
+
|
|
129
|
+
const isGroup = event.channel_type === "channel" || event.channel_type === "group";
|
|
130
|
+
const attachments = await this.downloadFiles(event.files);
|
|
131
|
+
|
|
132
|
+
const inbound: InboundMessage = {
|
|
133
|
+
id: event.ts,
|
|
134
|
+
channel: this.channelId,
|
|
135
|
+
chatId: event.channel,
|
|
136
|
+
chatType: isGroup ? "group" : "dm",
|
|
137
|
+
sender: event.user,
|
|
138
|
+
text: event.text || "",
|
|
139
|
+
timestamp: Math.floor(parseFloat(event.ts) * 1000),
|
|
140
|
+
isFromMe: false,
|
|
141
|
+
isGroup,
|
|
142
|
+
replyTo: event.thread_ts
|
|
143
|
+
? { id: event.thread_ts, text: "" }
|
|
144
|
+
: undefined,
|
|
145
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
146
|
+
raw: event,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
log.debug(`Slack received from ${event.user} in ${event.channel}: ${(event.text || "").slice(0, 100)}${attachments.length ? ` [${attachments.length} image(s)]` : ""}`);
|
|
150
|
+
|
|
151
|
+
if (this.messageHandler) {
|
|
152
|
+
this.messageHandler(inbound).catch((err) => {
|
|
153
|
+
log.error(`Slack message handler error: ${err}`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async downloadFiles(files?: SlackFile[]): Promise<Array<{ path: string; mimeType?: string }>> {
|
|
159
|
+
if (!files?.length) return [];
|
|
160
|
+
|
|
161
|
+
const imageTypes = ["png", "jpg", "jpeg", "gif", "webp", "heic", "bmp"];
|
|
162
|
+
const downloadDir = join(tmpdir(), "channelToAgent-slack-images");
|
|
163
|
+
mkdirSync(downloadDir, { recursive: true });
|
|
164
|
+
|
|
165
|
+
const results: Array<{ path: string; mimeType?: string }> = [];
|
|
166
|
+
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
try {
|
|
169
|
+
const info = await this.web.files.info({ file: file.id });
|
|
170
|
+
const fullFile = info.file as Record<string, unknown> | undefined;
|
|
171
|
+
if (!fullFile) {
|
|
172
|
+
log.warn(`Slack files.info returned no file for ${file.id}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const mimetype = fullFile.mimetype as string | undefined;
|
|
177
|
+
const filetype = fullFile.filetype as string | undefined;
|
|
178
|
+
const name = (fullFile.name as string) || `${file.id}.${filetype || "png"}`;
|
|
179
|
+
const size = fullFile.size as number | undefined;
|
|
180
|
+
const downloadUrl = fullFile.url_private_download as string | undefined;
|
|
181
|
+
|
|
182
|
+
if (!mimetype?.startsWith("image/") && !imageTypes.includes(filetype || "")) continue;
|
|
183
|
+
if ((size || 0) > 10_000_000) continue;
|
|
184
|
+
if (!downloadUrl) {
|
|
185
|
+
log.warn(`No download URL for Slack file ${name}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const resp = await fetch(downloadUrl, {
|
|
190
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
191
|
+
});
|
|
192
|
+
if (!resp.ok) {
|
|
193
|
+
log.warn(`Failed to download Slack file ${name}: ${resp.status}`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
197
|
+
const localPath = join(downloadDir, `${file.id}-${name}`);
|
|
198
|
+
writeFileSync(localPath, buffer);
|
|
199
|
+
results.push({ path: localPath, mimeType: mimetype });
|
|
200
|
+
log.debug(`Downloaded Slack file: ${name} (${buffer.length} bytes)`);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
log.warn(`Error downloading Slack file ${file.id}: ${err}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
}
|