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.
Files changed (315) hide show
  1. package/README.md +113 -0
  2. package/agents/_template/CLAUDE.md +18 -0
  3. package/agents/_template/agent.json +7 -0
  4. package/agents/platform/agentcreator/CLAUDE.md +300 -0
  5. package/agents/platform/appcreator/CLAUDE.md +158 -0
  6. package/agents/platform/gym/CLAUDE.md +486 -0
  7. package/agents/platform/gym/agent.json +40 -0
  8. package/agents/platform/gym/programs/agent-building/program.json +160 -0
  9. package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
  10. package/agents/platform/gym/programs/getting-started/program.json +124 -0
  11. package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
  12. package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
  13. package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
  14. package/agents/platform/gym/souls/alex.md +12 -0
  15. package/agents/platform/gym/souls/jordan.md +12 -0
  16. package/agents/platform/gym/souls/morgan.md +12 -0
  17. package/agents/platform/gym/souls/riley.md +12 -0
  18. package/agents/platform/gym/souls/sam.md +12 -0
  19. package/agents/platform/hub/CLAUDE.md +372 -0
  20. package/agents/platform/promptcreator/CLAUDE.md +130 -0
  21. package/agents/platform/skillcreator/CLAUDE.md +163 -0
  22. package/bin/cli.js +566 -0
  23. package/config.example.json +310 -0
  24. package/dist/agent-registry.d.ts +32 -0
  25. package/dist/agent-registry.d.ts.map +1 -0
  26. package/dist/agent-registry.js +144 -0
  27. package/dist/agent-registry.js.map +1 -0
  28. package/dist/channels/discord.d.ts +17 -0
  29. package/dist/channels/discord.d.ts.map +1 -0
  30. package/dist/channels/discord.js +114 -0
  31. package/dist/channels/discord.js.map +1 -0
  32. package/dist/channels/imessage.d.ts +23 -0
  33. package/dist/channels/imessage.d.ts.map +1 -0
  34. package/dist/channels/imessage.js +214 -0
  35. package/dist/channels/imessage.js.map +1 -0
  36. package/dist/channels/slack.d.ts +19 -0
  37. package/dist/channels/slack.d.ts.map +1 -0
  38. package/dist/channels/slack.js +167 -0
  39. package/dist/channels/slack.js.map +1 -0
  40. package/dist/channels/telegram.d.ts +19 -0
  41. package/dist/channels/telegram.d.ts.map +1 -0
  42. package/dist/channels/telegram.js +274 -0
  43. package/dist/channels/telegram.js.map +1 -0
  44. package/dist/channels/types.d.ts +44 -0
  45. package/dist/channels/types.d.ts.map +1 -0
  46. package/dist/channels/types.js +18 -0
  47. package/dist/channels/types.js.map +1 -0
  48. package/dist/channels/whatsapp.d.ts +23 -0
  49. package/dist/channels/whatsapp.d.ts.map +1 -0
  50. package/dist/channels/whatsapp.js +189 -0
  51. package/dist/channels/whatsapp.js.map +1 -0
  52. package/dist/config.d.ts +134 -0
  53. package/dist/config.d.ts.map +1 -0
  54. package/dist/config.js +127 -0
  55. package/dist/config.js.map +1 -0
  56. package/dist/cron.d.ts +8 -0
  57. package/dist/cron.d.ts.map +1 -0
  58. package/dist/cron.js +35 -0
  59. package/dist/cron.js.map +1 -0
  60. package/dist/decrypt-keys.d.ts +7 -0
  61. package/dist/decrypt-keys.d.ts.map +1 -0
  62. package/dist/decrypt-keys.js +53 -0
  63. package/dist/decrypt-keys.js.map +1 -0
  64. package/dist/encrypt-keys.d.ts +8 -0
  65. package/dist/encrypt-keys.d.ts.map +1 -0
  66. package/dist/encrypt-keys.js +62 -0
  67. package/dist/encrypt-keys.js.map +1 -0
  68. package/dist/executor.d.ts +31 -0
  69. package/dist/executor.d.ts.map +1 -0
  70. package/dist/executor.js +2009 -0
  71. package/dist/executor.js.map +1 -0
  72. package/dist/gemini-executor.d.ts +27 -0
  73. package/dist/gemini-executor.d.ts.map +1 -0
  74. package/dist/gemini-executor.js +160 -0
  75. package/dist/gemini-executor.js.map +1 -0
  76. package/dist/goals.d.ts +24 -0
  77. package/dist/goals.d.ts.map +1 -0
  78. package/dist/goals.js +189 -0
  79. package/dist/goals.js.map +1 -0
  80. package/dist/gym/activity-digest.d.ts +30 -0
  81. package/dist/gym/activity-digest.d.ts.map +1 -0
  82. package/dist/gym/activity-digest.js +506 -0
  83. package/dist/gym/activity-digest.js.map +1 -0
  84. package/dist/gym/dimension-scorer.d.ts +76 -0
  85. package/dist/gym/dimension-scorer.d.ts.map +1 -0
  86. package/dist/gym/dimension-scorer.js +236 -0
  87. package/dist/gym/dimension-scorer.js.map +1 -0
  88. package/dist/gym/gym-router.d.ts +7 -0
  89. package/dist/gym/gym-router.d.ts.map +1 -0
  90. package/dist/gym/gym-router.js +718 -0
  91. package/dist/gym/gym-router.js.map +1 -0
  92. package/dist/gym/index.d.ts +11 -0
  93. package/dist/gym/index.d.ts.map +1 -0
  94. package/dist/gym/index.js +11 -0
  95. package/dist/gym/index.js.map +1 -0
  96. package/dist/heartbeat.d.ts +21 -0
  97. package/dist/heartbeat.d.ts.map +1 -0
  98. package/dist/heartbeat.js +163 -0
  99. package/dist/heartbeat.js.map +1 -0
  100. package/dist/index.d.ts +2 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +254 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/keystore.d.ts +22 -0
  105. package/dist/keystore.d.ts.map +1 -0
  106. package/dist/keystore.js +178 -0
  107. package/dist/keystore.js.map +1 -0
  108. package/dist/logger.d.ts +9 -0
  109. package/dist/logger.d.ts.map +1 -0
  110. package/dist/logger.js +45 -0
  111. package/dist/logger.js.map +1 -0
  112. package/dist/memory/daily.d.ts +22 -0
  113. package/dist/memory/daily.d.ts.map +1 -0
  114. package/dist/memory/daily.js +82 -0
  115. package/dist/memory/daily.js.map +1 -0
  116. package/dist/memory/embeddings.d.ts +15 -0
  117. package/dist/memory/embeddings.d.ts.map +1 -0
  118. package/dist/memory/embeddings.js +154 -0
  119. package/dist/memory/embeddings.js.map +1 -0
  120. package/dist/memory/index.d.ts +32 -0
  121. package/dist/memory/index.d.ts.map +1 -0
  122. package/dist/memory/index.js +159 -0
  123. package/dist/memory/index.js.map +1 -0
  124. package/dist/memory/search.d.ts +21 -0
  125. package/dist/memory/search.d.ts.map +1 -0
  126. package/dist/memory/search.js +77 -0
  127. package/dist/memory/search.js.map +1 -0
  128. package/dist/memory/store.d.ts +23 -0
  129. package/dist/memory/store.d.ts.map +1 -0
  130. package/dist/memory/store.js +144 -0
  131. package/dist/memory/store.js.map +1 -0
  132. package/dist/ollama-executor.d.ts +17 -0
  133. package/dist/ollama-executor.d.ts.map +1 -0
  134. package/dist/ollama-executor.js +112 -0
  135. package/dist/ollama-executor.js.map +1 -0
  136. package/dist/openai-executor.d.ts +38 -0
  137. package/dist/openai-executor.d.ts.map +1 -0
  138. package/dist/openai-executor.js +197 -0
  139. package/dist/openai-executor.js.map +1 -0
  140. package/dist/router.d.ts +11 -0
  141. package/dist/router.d.ts.map +1 -0
  142. package/dist/router.js +185 -0
  143. package/dist/router.js.map +1 -0
  144. package/dist/test-message.d.ts +2 -0
  145. package/dist/test-message.d.ts.map +1 -0
  146. package/dist/test-message.js +60 -0
  147. package/dist/test-message.js.map +1 -0
  148. package/dist/utils/imsg-db-reader.d.ts +24 -0
  149. package/dist/utils/imsg-db-reader.d.ts.map +1 -0
  150. package/dist/utils/imsg-db-reader.js +92 -0
  151. package/dist/utils/imsg-db-reader.js.map +1 -0
  152. package/dist/utils/imsg-rpc.d.ts +25 -0
  153. package/dist/utils/imsg-rpc.d.ts.map +1 -0
  154. package/dist/utils/imsg-rpc.js +149 -0
  155. package/dist/utils/imsg-rpc.js.map +1 -0
  156. package/dist/utils/message-formatter.d.ts +3 -0
  157. package/dist/utils/message-formatter.d.ts.map +1 -0
  158. package/dist/utils/message-formatter.js +69 -0
  159. package/dist/utils/message-formatter.js.map +1 -0
  160. package/dist/web-ui.d.ts +12 -0
  161. package/dist/web-ui.d.ts.map +1 -0
  162. package/dist/web-ui.js +5784 -0
  163. package/dist/web-ui.js.map +1 -0
  164. package/dist/whatsapp-chats.d.ts +2 -0
  165. package/dist/whatsapp-chats.d.ts.map +1 -0
  166. package/dist/whatsapp-chats.js +76 -0
  167. package/dist/whatsapp-chats.js.map +1 -0
  168. package/dist/whatsapp-login.d.ts +2 -0
  169. package/dist/whatsapp-login.d.ts.map +1 -0
  170. package/dist/whatsapp-login.js +90 -0
  171. package/dist/whatsapp-login.js.map +1 -0
  172. package/dist/wiki-sync.d.ts +21 -0
  173. package/dist/wiki-sync.d.ts.map +1 -0
  174. package/dist/wiki-sync.js +147 -0
  175. package/dist/wiki-sync.js.map +1 -0
  176. package/docs/AddNewAgentGuide.md +100 -0
  177. package/docs/AddNewMcpGuide.md +72 -0
  178. package/docs/Architecture.md +795 -0
  179. package/docs/CLAUDE-AI-SETUP.md +166 -0
  180. package/docs/Setup.md +297 -0
  181. package/docs/ai-gym-architecture.md +1040 -0
  182. package/docs/ai-gym-build-plan.md +343 -0
  183. package/docs/ai-gym-onboarding.md +122 -0
  184. package/docs/appcreator_plan.md +348 -0
  185. package/docs/platform-mcp-audit.md +320 -0
  186. package/docs/server-deployment-plan.md +503 -0
  187. package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
  188. package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
  189. package/docs/user-guide.md +2016 -0
  190. package/mcp-catalog.json +628 -0
  191. package/package.json +63 -0
  192. package/public/MyAIforOne-logomark-512.svg +16 -0
  193. package/public/MyAIforOne-logomark-transparent.svg +15 -0
  194. package/public/activity.html +314 -0
  195. package/public/admin.html +1674 -0
  196. package/public/agent-dashboard.html +670 -0
  197. package/public/api-docs.html +1106 -0
  198. package/public/automations.html +722 -0
  199. package/public/canvas.css +223 -0
  200. package/public/canvas.js +588 -0
  201. package/public/changelog.html +231 -0
  202. package/public/gym.html +2766 -0
  203. package/public/home.html +1930 -0
  204. package/public/index.html +2809 -0
  205. package/public/lab.html +1643 -0
  206. package/public/library.html +1442 -0
  207. package/public/marketplace.html +1101 -0
  208. package/public/mcp-docs.html +441 -0
  209. package/public/mini.html +390 -0
  210. package/public/monitor.html +584 -0
  211. package/public/org.html +4304 -0
  212. package/public/projects.html +734 -0
  213. package/public/settings.html +645 -0
  214. package/public/tasks.html +932 -0
  215. package/public/trainers/alex.svg +12 -0
  216. package/public/trainers/jordan.svg +12 -0
  217. package/public/trainers/morgan.svg +12 -0
  218. package/public/trainers/riley.svg +12 -0
  219. package/public/trainers/sam.svg +12 -0
  220. package/public/user-guide.html +218 -0
  221. package/registry/agents.json +3 -0
  222. package/registry/apps.json +20 -0
  223. package/registry/installed-drafts.json +3 -0
  224. package/registry/mcps.json +1084 -0
  225. package/registry/prompts/personal/mcp-test-prompt.md +6 -0
  226. package/registry/prompts/personal/memory-recall.md +6 -0
  227. package/registry/prompts/platform/brainstorm.md +15 -0
  228. package/registry/prompts/platform/code-review.md +16 -0
  229. package/registry/prompts/platform/explain.md +16 -0
  230. package/registry/prompts.json +58 -0
  231. package/registry/skills/external/brainstorming.md +5 -0
  232. package/registry/skills/external/code-review.md +40 -0
  233. package/registry/skills/external/frontend-patterns.md +642 -0
  234. package/registry/skills/external/frontend-slides.md +184 -0
  235. package/registry/skills/external/systematic-debugging.md +5 -0
  236. package/registry/skills/external/tdd.md +328 -0
  237. package/registry/skills/external/verification-before-completion.md +5 -0
  238. package/registry/skills/external/writing-plans.md +5 -0
  239. package/registry/skills/platform/ai41_app_build.md +930 -0
  240. package/registry/skills/platform/ai41_app_deploy.md +168 -0
  241. package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
  242. package/registry/skills/platform/ai41_app_patterns.md +359 -0
  243. package/registry/skills/platform/ai41_app_register.md +85 -0
  244. package/registry/skills/platform/ai41_app_scaffold.md +421 -0
  245. package/registry/skills/platform/ai41_app_verify.md +107 -0
  246. package/registry/skills/platform/opProjectCreate.md +239 -0
  247. package/registry/skills/platform/op_devbrowser.md +136 -0
  248. package/registry/skills/platform/sop_brandguidelines.md +103 -0
  249. package/registry/skills/platform/sop_docx.md +117 -0
  250. package/registry/skills/platform/sop_frontenddesign.md +44 -0
  251. package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
  252. package/registry/skills/platform/sop_mcpbuilder.md +133 -0
  253. package/registry/skills/platform/sop_pdf.md +172 -0
  254. package/registry/skills/platform/sop_pptx.md +133 -0
  255. package/registry/skills/platform/sop_skillcreator.md +104 -0
  256. package/registry/skills/platform/sop_themefactory.md +128 -0
  257. package/registry/skills/platform/sop_webapptesting.md +75 -0
  258. package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
  259. package/registry/skills/platform/sop_xlsx.md +134 -0
  260. package/registry/skills.json +1055 -0
  261. package/scripts/discover-chats.sh +11 -0
  262. package/scripts/install-service-windows.ps1 +87 -0
  263. package/scripts/install-service.sh +52 -0
  264. package/scripts/seed-registry.ts +195 -0
  265. package/scripts/test-send.sh +5 -0
  266. package/scripts/tray-indicator.ps1 +35 -0
  267. package/scripts/uninstall-service-windows.ps1 +23 -0
  268. package/scripts/uninstall-service.sh +15 -0
  269. package/scripts/xbar-myagent.5s.sh +32 -0
  270. package/server/mcp-server/dist/index.d.ts +11 -0
  271. package/server/mcp-server/dist/index.js +1332 -0
  272. package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
  273. package/server/mcp-server/dist/lib/api-client.js +241 -0
  274. package/server/mcp-server/index.ts +1545 -0
  275. package/server/mcp-server/lib/api-client.ts +366 -0
  276. package/server/mcp-server/tsconfig.json +14 -0
  277. package/src/agent-registry.ts +180 -0
  278. package/src/channels/discord.ts +129 -0
  279. package/src/channels/imessage.ts +261 -0
  280. package/src/channels/slack.ts +208 -0
  281. package/src/channels/telegram.ts +307 -0
  282. package/src/channels/types.ts +62 -0
  283. package/src/channels/whatsapp.ts +227 -0
  284. package/src/config.ts +281 -0
  285. package/src/cron.ts +43 -0
  286. package/src/decrypt-keys.ts +60 -0
  287. package/src/encrypt-keys.ts +70 -0
  288. package/src/executor.ts +2190 -0
  289. package/src/gemini-executor.ts +212 -0
  290. package/src/goals.ts +240 -0
  291. package/src/gym/activity-digest.ts +546 -0
  292. package/src/gym/dimension-scorer.ts +297 -0
  293. package/src/gym/gym-router.ts +801 -0
  294. package/src/gym/index.ts +19 -0
  295. package/src/heartbeat.ts +220 -0
  296. package/src/index.ts +275 -0
  297. package/src/keystore.ts +190 -0
  298. package/src/logger.ts +51 -0
  299. package/src/memory/daily.ts +101 -0
  300. package/src/memory/embeddings.ts +185 -0
  301. package/src/memory/index.ts +218 -0
  302. package/src/memory/search.ts +124 -0
  303. package/src/memory/store.ts +189 -0
  304. package/src/ollama-executor.ts +126 -0
  305. package/src/openai-executor.ts +259 -0
  306. package/src/router.ts +230 -0
  307. package/src/test-message.ts +72 -0
  308. package/src/utils/imsg-db-reader.ts +109 -0
  309. package/src/utils/imsg-rpc.ts +178 -0
  310. package/src/utils/message-formatter.ts +90 -0
  311. package/src/web-ui.ts +5778 -0
  312. package/src/whatsapp-chats.ts +91 -0
  313. package/src/whatsapp-login.ts +110 -0
  314. package/src/wiki-sync.ts +199 -0
  315. 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
+ }